cbor source

← cbor · API reference · version 0.1.13

3 file(s), 559 lines. This is the source the published version ships, and it is loft — the library is written in the same language you call it from.

Public items

A worked example is a call site the author chose as the thing to read first. Call sites are every line naming the item, found mechanically over the source with comments excluded — so an item with no worked example is not an item nobody uses.

ItemDefinedWorked exampleCall sites
CborValue enum26CBR-00232 38 39 156 192 263 … (7 total)
CborEntry struct37—33 107 276 287 48 49 … (25 total)
key const38—115 287 48 49 55 56 … (24 total)
value const39—28 29 30 31 140 159 … (85 total)
encode fn156CBR-004115 140 178 14 16 21 … (33 total)
Decoded struct191—207 209 247 249 250 255 … (19 total)
value const192—28 29 30 31 39 140 … (85 total)
next const193—209 247 249 250 255 256 … (21 total)
ok const194—209 247 249 250 255 256 … (35 total)
MAX_DEPTH const201—209 92 93
decode fn312CBR-00414 86 87 88 92 93 … (14 total)

src/cbor.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// --- CBOR ---
// Canonical CBOR (RFC 8949) for loft, in pure loft.
//
// CBOR is a compact, binary, self-describing serialisation — like JSON but
// with native byte strings and a *deterministic* (canonical) form.  This
// library targets the signable-record use case: the same value always encodes
// to byte-identical output, so a signature over those bytes is stable across
// implementations and across targets.
//
// Pure loft by design (loft-lang/plans#83): the byte format is simple enough to
// assemble directly over `vector<u8>`, so there is no native crate, no FFI
// bridge, and no external-crate trust surface — and the codec runs identically
// native and in the browser (native == wasm) by construction.
//
// Build a `CborValue` and `encode` it: the canonical encoder picks shortest-form
// integer/length heads and sorts a map's entries by their encoded keys.  `decode`
// reads canonical bytes back and refuses anything else.

/// A dynamic CBOR value.  Mirrors the stdlib `JsonValue`, with CBOR's extra
/// `CBytes` (native byte string) and the integer split that CBOR makes at the
/// wire level folded into one signed `CInt`.
// Example: @CBR-002 — which variant you pick is part of the wire form: the same
// two payload bytes are a different record as `CBytes` than as `CText`.
pub enum CborValue {
  CNull,
  CBool  { pub value: boolean },
  CInt   { pub value: integer },   // >=0 -> major 0 (uint); <0 -> major 1 (negint)
  CBytes { pub value: vector<u8> },
  CText  { pub value: text },
  CArray { pub items: vector<CborValue> },
  CMap   { pub entries: vector<CborEntry> },
}

/// One key/value pair of a `CMap`.
pub struct CborEntry {
  pub const key: CborValue,
  pub const value: CborValue,
}

// Canonical head: the major type (0..7) plus the SHORTEST-form encoding of
// `arg` (RFC 8949 §4.2.1).  Lengths and integer payloads share this routine.
// `major` is one of the eight CBOR major types, and saying so in the type is what lets the
// native build emit `major * 32 + …` as plain arithmetic: a value the compiler can bound
// cannot overflow, so no check is owed (every caller passes a literal 0..7).
fn head(major: integer limit(0, 7), arg: integer) -> vector<u8> {
  buf: vector<u8> = [];
  if arg < 24 {
    buf += [((major * 32 + arg) & 255) as u8];
  } else if arg < 256 {
    buf += [((major * 32 + 24) & 255) as u8];
    buf += [(arg & 255) as u8];
  } else if arg < 65536 {
    buf += [((major * 32 + 25) & 255) as u8];
    buf += [((arg / 256) & 255) as u8];
    buf += [((arg % 256) & 255) as u8];
  } else if arg < 4294967296 {
    buf += [((major * 32 + 26) & 255) as u8];
    buf += [((arg / 16777216 % 256) & 255) as u8];
    buf += [((arg / 65536 % 256) & 255) as u8];
    buf += [((arg / 256 % 256) & 255) as u8];
    buf += [((arg % 256) & 255) as u8];
  } else {
    buf += [((major * 32 + 27) & 255) as u8];
    buf += [((arg / 72057594037927936 % 256) & 255) as u8];
    buf += [((arg / 281474976710656 % 256) & 255) as u8];
    buf += [((arg / 1099511627776 % 256) & 255) as u8];
    buf += [((arg / 4294967296 % 256) & 255) as u8];
    buf += [((arg / 16777216 % 256) & 255) as u8];
    buf += [((arg / 65536 % 256) & 255) as u8];
    buf += [((arg / 256 % 256) & 255) as u8];
    buf += [((arg % 256) & 255) as u8];
  }
  return buf;
}

// Bytewise lexicographic less-than (RFC 8949 §4.2.1 canonical map-key order)
// between two keys held as slices [oa,oa+la) and [ob,ob+lb) of the shared flat
// buffer `kb`: at the first differing byte the smaller byte wins, and a proper
// prefix sorts before the longer string. Slice form (no materialised vectors), so
// the ranking loop compares precomputed key bytes without allocating.
fn key_lt(kb: vector<u8>, oa: integer, la: integer, ob: integer, lb: integer) -> boolean {
  m = if la < lb { la } else { lb };
  for t in 0..m {
    ca = kb[oa + t] ?? 0;
    cb = kb[ob + t] ?? 0;
    if ca != cb { return ca < cb; }
  }
  return la < lb;
}

// Encode a map with its entries in canonical key order: keys compared by their
// deterministic encodings, bytewise (RFC 8949 §4.2.1).  Emits each entry at the
// position equal to its key's RANK (how many keys sort before it) — a formulation
// that uses only append + read (no marker array, no vector element assignment,
// no nested vectors), so it codegens identically on the interpreter and --native.
//
// O(n^2): each key is encoded ONCE into a flat buffer (`keybuf` + offset/length
// tables), then ranks are computed by comparing those precomputed bytes in place.
// The earlier form re-encoded every key inside a triple loop (O(n^3) key-encodings)
// — fine for a 3-field record, but a directory / oplog-snapshot map with hundreds
// of entries hit a wall (measured 100 entries 4 ms, 200 entries 33 ms). Ties are
// broken by original index so the rank is a strict total order (a well-formed
// canonical map has distinct keys; this just keeps a malformed one deterministic
// instead of dropping or duplicating an entry).
fn encode_map(entries: vector<CborEntry>) -> vector<u8> {
  n = len(entries);
  buf = head(5, n);
  // 1) encode each key exactly once into a shared flat buffer + offset/length tables
  keybuf: vector<u8> = [];
  koff: vector<integer> = [];
  klen: vector<integer> = [];
  for i in 0..n {
    k = encode(entries[i].key);
    koff += [ len(keybuf) ];
    klen += [ len(k) ];
    keybuf += k;
  }
  // 2) rank[i] = how many keys sort strictly before key i (index breaks ties)
  ranks: vector<integer> = [];
  for i in 0..n {
    oi = koff[i] ?? 0; li = klen[i] ?? 0;
    r = 0;
    for j in 0..n {
      if j != i {
        oj = koff[j] ?? 0; lj = klen[j] ?? 0;
        if key_lt(keybuf, oj, lj, oi, li) { r = r + 1; }
        else if (j < i) && !key_lt(keybuf, oi, li, oj, lj) { r = r + 1; }  // equal keys: lower index first
      }
    }
    ranks += [ r ];
  }
  // 3) emit entries in ascending rank (append-only; key bytes reused from keybuf)
  for target in 0..n {
    for i in 0..n {
      if (ranks[i] ?? 0) == target {
        oi = koff[i] ?? 0; li = klen[i] ?? 0;
        for t in 0..li { buf += [ keybuf[oi + t] ?? 0 ]; }
        buf += encode(entries[i].value);
      }
    }
  }
  return buf;
}

/// Encode a `CborValue` to canonical CBOR bytes.
/// A map's entries are SORTED here by their encoded key bytes, so the order you
/// built the map in never reaches the wire.
// Example: @CBR-001 — two maps built in opposite orders encode identically, which
// is what keeps a signature over the bytes valid.
// Example: @CBR-002 — `CBytes` and `CText` are different wire types, so the
// loft-side choice is part of what gets signed.
// Example: @CBR-004 — canonical is about ORDER, not uniqueness: duplicate keys
// encode without complaint.
pub fn encode(v: CborValue) -> vector<u8> {
  match v {
    CNull => head(7, 22),                                   // 0xf6
    CBool { value } => head(7, if value { 21 } else { 20 }), // 0xf5 / 0xf4
    CInt { value } => if value >= 0 { head(0, value) } else { head(1, 0 - value - 1) },
    CBytes { value } => {
      buf = head(2, len(value));
      buf += value;
      buf
    },
    CText { value } => {
      // RFC 8949 major type 3 is length-prefixed in BYTES, so this must be `size` (bytes),
      // not `len` (characters, since @PLN110). With `len` a multi-byte character wrote a short
      // header and truncated the copy — "héllo" (5 chars, 6 bytes) lost its last byte and the
      // round-trip failed. ASCII was unaffected, which is why it survived unnoticed.
      n = size(value);
      buf = head(3, n);
      for i in 0..n { buf += [(value.byte_at(i) & 255) as u8]; }
      buf
    },
    CArray { items } => {
      buf = head(4, len(items));
      for i in 0..len(items) { buf += encode(items[i]); }
      buf
    },
    CMap { entries } => encode_map(entries),
  }
}

// --- decode (A3) ---

/// The result of decoding one CBOR value: the `value`, the byte position just
/// past it (`next`), and `ok` — false on any malformed / non-canonical /
/// truncated input, and on nesting deeper than `MAX_DEPTH`.  Decoding never
/// crashes (loft-safe): a bad byte stream yields ok=false rather than a trap.
pub struct Decoded {
  pub const value: CborValue,
  pub const next:  integer,
  pub const ok:    boolean,
}

/// How deep arrays and maps may nest in bytes `decode` accepts — a value inside
/// `MAX_DEPTH` enclosing arrays or maps is the deepest that decodes.  Deeper input is
/// refused with ok=false: each level is one call, and a hostile byte stream of
/// nothing but array heads would otherwise exhaust the call stack.
pub const MAX_DEPTH = 256;

// Decode one CBOR value starting at `pos`, `depth` levels inside the top-level one.
// Recursive: arrays/maps thread the running position through their children.
// Enforces canonical shortest-form heads (an overlong argument is rejected),
// definite lengths only, and `MAX_DEPTH`.
fn read_value(bytes: vector<u8>, pos: integer, depth: integer) -> Decoded {
  n = len(bytes);
  if pos >= n || depth > MAX_DEPTH { return Decoded { value: CNull, next: pos, ok: false }; }
  // Every `bytes[…]` read below is already bounds-guarded (the `>= n` checks set
  // `argok = false`), so `?? 0` only discharges the nullable-index type the current
  // loft gives `bytes[i]` (@PLN25) — the default is never actually reached.
  b = bytes[pos] ?? 0;
  major = b / 32;
  info = b % 32;
  // --- argument (length / integer payload), canonical shortest-form ---
  arg = 0;
  argpos = pos + 1;
  argok = true;
  if info < 24 {
    arg = info;
  } else if info == 24 {
    if argpos >= n { argok = false; }
    else { arg = bytes[argpos] ?? 0; argpos = argpos + 1; if arg < 24 { argok = false; } }
  } else if info == 25 {
    if argpos + 1 >= n { argok = false; }
    else { arg = (bytes[argpos] ?? 0) * 256 + (bytes[argpos + 1] ?? 0); argpos = argpos + 2; if arg < 256 { argok = false; } }
  } else if info == 26 {
    if argpos + 3 >= n { argok = false; }
    else {
      arg = (((bytes[argpos] ?? 0) * 256 + (bytes[argpos + 1] ?? 0)) * 256 + (bytes[argpos + 2] ?? 0)) * 256 + (bytes[argpos + 3] ?? 0);
      argpos = argpos + 4;
      if arg < 65536 { argok = false; }
    }
  } else if info == 27 {
    if argpos + 7 >= n { argok = false; }
    else if (bytes[argpos] ?? 0) >= 128 { argok = false; }   // exceeds i64 range
    else {
      arg = bytes[argpos] ?? 0;
      for k in 1..8 { arg = arg * 256 + (bytes[argpos + k] ?? 0); }
      argpos = argpos + 8;
      if arg < 4294967296 { argok = false; }
    }
  } else {
    argok = false;   // 28/29/30 reserved, 31 indefinite — not canonical
  }
  if !argok { return Decoded { value: CNull, next: pos, ok: false }; }
  // --- dispatch on major type ---
  if major == 0 { return Decoded { value: CInt { value: arg }, next: argpos, ok: true }; }
  if major == 1 { return Decoded { value: CInt { value: 0 - arg - 1 }, next: argpos, ok: true }; }
  // The payload arms take their span of the frame in ONE block (`bytes[argpos..argpos + arg]`,
  // a slice at a field and at an argument position): no per-byte loop, no buffer of their own.
  // On a frame the host hands over as read-only bytes the span is read in place.
  if major == 2 {
    if argpos + arg > n { return Decoded { value: CNull, next: pos, ok: false }; }
    return Decoded { value: CBytes { value: bytes[argpos..argpos + arg] }, next: argpos + arg, ok: true };
  }
  if major == 3 {
    if argpos + arg > n { return Decoded { value: CNull, next: pos, ok: false }; }
    return Decoded { value: CText { value: text_from_bytes(bytes[argpos..argpos + arg]) }, next: argpos + arg, ok: true };
  }
  if major == 4 {
    items: vector<CborValue> = [];
    p = argpos;
    aok = true;
    for _ in 0..arg {
      if aok {
        sub = read_value(bytes, p, depth + 1);
        if sub.ok { items += [sub.value]; p = sub.next; } else { aok = false; }
      }
    }
    if !aok { return Decoded { value: CNull, next: pos, ok: false }; }
    return Decoded { value: CArray { items: items }, next: p, ok: true };
  }
  if major == 5 {
    entries: vector<CborEntry> = [];
    p = argpos;
    mok = true;
    for _ in 0..arg {
      if mok {
        kd = read_value(bytes, p, depth + 1);
        if kd.ok {
          vd = read_value(bytes, kd.next, depth + 1);
          if vd.ok {
            kv = kd.value;
            vv = vd.value;
            entries += [CborEntry { key: kv, value: vv }];
            p = vd.next;
          } else { mok = false; }
        } else { mok = false; }
      }
    }
    if !mok { return Decoded { value: CNull, next: pos, ok: false }; }
    return Decoded { value: CMap { entries: entries }, next: p, ok: true };
  }
  if major == 7 {
    if info == 20 { return Decoded { value: CBool { value: false }, next: argpos, ok: true }; }
    if info == 21 { return Decoded { value: CBool { value: true },  next: argpos, ok: true }; }
    if info == 22 { return Decoded { value: CNull, next: argpos, ok: true }; }
  }
  // major 6 (tags), float / undefined simples — unsupported in the signable subset
  return Decoded { value: CNull, next: pos, ok: false };
}

/// Decode canonical CBOR bytes into a `CborValue`.  Returns `ok=false` on any
/// malformed, non-canonical, or truncated input, on nesting deeper than
/// `MAX_DEPTH` — and rejects trailing bytes after the top-level value.  Never
/// crashes.
// Example: @CBR-003 — `ok=false` is one answer for four different problems, and
// the surprising one is that well-formed but NON-canonical CBOR is refused.
// Example: @CBR-004 — a map carrying two entries under one key decodes ok=true.
pub fn decode(bytes: vector<u8>) -> Decoded {
  d = read_value(bytes, 0, 0);
  if d.ok {
    if d.next != len(bytes) { return Decoded { value: CNull, next: d.next, ok: false }; }
  }
  return d;
}

tests/cbor.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
// CBOR encoder — RFC 8949 Appendix A vectors (canonical, byte-identical).
use cbor::*;

fn bytes_eq(a: vector<u8>, b: vector<u8>) -> boolean {
  if len(a) != len(b) { return false; }
  for i in 0..len(a) { if a[i] != b[i] { return false; } }
  return true;
}

// encode -> decode -> encode must be byte-identical (A3 round-trip property).
fn roundtrip(v: CborValue) -> boolean {
  d = decode(encode(v));
  if !d.ok { return false; }
  return bytes_eq(encode(d.value), encode(v));
}

fn main() {
  // unsigned integers (major 0), shortest-form heads
  assert(bytes_eq(encode(CInt { value: 0 }),    [0x00 as u8]), "uint 0");
  assert(bytes_eq(encode(CInt { value: 23 }),   [0x17 as u8]), "uint 23");
  assert(bytes_eq(encode(CInt { value: 24 }),   [0x18 as u8, 0x18 as u8]), "uint 24");
  assert(bytes_eq(encode(CInt { value: 1000 }), [0x19 as u8, 0x03 as u8, 0xe8 as u8]), "uint 1000");
  assert(bytes_eq(encode(CInt { value: 1000000 }),
    [0x1a as u8, 0x00 as u8, 0x0f as u8, 0x42 as u8, 0x40 as u8]), "uint 1000000");

  // negative integers (major 1)
  assert(bytes_eq(encode(CInt { value: 0 - 1 }),   [0x20 as u8]), "negint -1");
  assert(bytes_eq(encode(CInt { value: 0 - 100 }), [0x38 as u8, 0x63 as u8]), "negint -100");

  // simple values (major 7)
  assert(bytes_eq(encode(CBool { value: false }), [0xf4 as u8]), "false");
  assert(bytes_eq(encode(CBool { value: true }),  [0xf5 as u8]), "true");
  assert(bytes_eq(encode(CNull), [0xf6 as u8]), "null");

  // text string (major 3) + byte string (major 2)
  assert(bytes_eq(encode(CText { value: "IETF" }),
    [0x64 as u8, 0x49 as u8, 0x45 as u8, 0x54 as u8, 0x46 as u8]), "text IETF");
  assert(bytes_eq(encode(CBytes { value: [1 as u8, 2 as u8, 3 as u8, 4 as u8] }),
    [0x44 as u8, 0x01 as u8, 0x02 as u8, 0x03 as u8, 0x04 as u8]), "bytes 01020304");

  // array (major 4) + map (major 5), nesting
  assert(bytes_eq(encode(CArray { items: [CInt { value: 1 }, CInt { value: 2 }, CInt { value: 3 }] }),
    [0x83 as u8, 0x01 as u8, 0x02 as u8, 0x03 as u8]), "array 1 2 3");
  assert(bytes_eq(
    encode(CMap { entries: [
      CborEntry { key: CInt { value: 1 }, value: CInt { value: 2 } },
      CborEntry { key: CInt { value: 3 }, value: CInt { value: 4 } } ] }),
    [0xa2 as u8, 0x01 as u8, 0x02 as u8, 0x03 as u8, 0x04 as u8]), "map 1to2 3to4");

  // canonical map-key ordering (RFC 8949 §4.2.1): out-of-order keys encode sorted
  assert(bytes_eq(
    encode(CMap { entries: [
      CborEntry { key: CInt { value: 3 }, value: CInt { value: 4 } },
      CborEntry { key: CInt { value: 1 }, value: CInt { value: 2 } } ] }),
    [0xa2 as u8, 0x01 as u8, 0x02 as u8, 0x03 as u8, 0x04 as u8]), "map canonical reorder");
  // mixed-length keys sort BYTEWISE: 10 (0x0a) < 24 (0x18 18) < 100 (0x18 64)
  assert(bytes_eq(
    encode(CMap { entries: [
      CborEntry { key: CInt { value: 100 }, value: CInt { value: 0 } },
      CborEntry { key: CInt { value: 10 },  value: CInt { value: 1 } },
      CborEntry { key: CInt { value: 24 },  value: CInt { value: 2 } } ] }),
    [0xa3 as u8, 0x0a as u8, 0x01 as u8, 0x18 as u8, 0x18 as u8, 0x02 as u8,
     0x18 as u8, 0x64 as u8, 0x00 as u8]), "map canonical mixed-length");

  // --- decode (A3): encode -> decode -> encode byte-identity, all value types ---
  assert(roundtrip(CInt { value: 0 }),         "rt uint 0");
  assert(roundtrip(CInt { value: 1000000 }),   "rt uint 1000000");
  assert(roundtrip(CInt { value: 0 - 100 }),   "rt negint -100");
  assert(roundtrip(CBool { value: true }),     "rt true");
  assert(roundtrip(CNull),                     "rt null");
  assert(roundtrip(CBytes { value: [1 as u8, 2 as u8, 3 as u8, 4 as u8] }), "rt bytes");
  assert(roundtrip(CArray { items: [CInt { value: 1 }, CInt { value: 2 }, CInt { value: 3 }] }), "rt array");
  // TEXT decode now works via the stdlib `text_from_bytes` primitive
  assert(roundtrip(CText { value: "IETF" }),  "rt text");
  assert(roundtrip(CText { value: "héllo" }), "rt text utf8");
  // MAP round-trip (loft#406 fixed): out-of-order keys re-sort canonically on re-encode
  assert(roundtrip(CMap { entries: [
    CborEntry { key: CInt { value: 1 }, value: CInt { value: 2 } },
    CborEntry { key: CInt { value: 3 }, value: CText { value: "x" } } ] }), "rt map");
  assert(roundtrip(CMap { entries: [
    CborEntry { key: CInt { value: 3 }, value: CInt { value: 4 } },
    CborEntry { key: CInt { value: 1 }, value: CInt { value: 2 } } ] }), "rt map canonical");
  // malformed / non-canonical / trailing — ok=false, never a crash
  assert(!decode([0x18 as u8]).ok,             "truncated rejected");
  assert(!decode([0x18 as u8, 0x00 as u8]).ok, "overlong head rejected");
  assert(!decode([0x00 as u8, 0x00 as u8]).ok, "trailing bytes rejected");

  // Nesting: MAX_DEPTH levels decode, one more is refused, and a hostile stream of
  // array heads far past it answers ok=false instead of exhausting the call stack.
  assert(decode(nested_arrays(MAX_DEPTH)).ok, "MAX_DEPTH levels decode");
  assert(!decode(nested_arrays(MAX_DEPTH + 1)).ok, "one level more is refused");
  assert(!decode(nested_arrays(100000)).ok, "100000 levels are refused, not a crash");
  assert(roundtrip(CArray { items: [CArray { items: [CInt { value: 1 }] }] }), "shallow nesting still round-trips");

  println("cbor: all RFC 8949 vectors pass");
}

// A null inside `depth` one-element arrays: a value at nesting depth `depth`.
fn nested_arrays(depth: integer) -> vector<u8> {
  b: vector<u8> = [];
  for _i in 0..depth { b += [0x81 as u8]; }
  b += [0xf6 as u8];
  b
}

tests/worked-examples.loft

// Copyright (c) 2026 Jurjen Stellingwerff
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Worked examples for the `cbor` library (@PLN141).  Each `test_*` below is a
// REAL, run-every-CI demonstration of a contract the signature cannot state; the
// function documents itself by CITING the tag with `// Example: @CBR-00N`, and
// `check_doc_drift.sh examples` fails if a citation goes dangling (no test carries
// the tag).  These are teaching examples, distinct from the round-trip suite in
// `cbor.loft`.
//
// This codec exists for signable records: the same value must always produce the
// same bytes, because a signature is over the BYTES.  So the contracts that matter
// are all about which loft-side choices reach the wire and which do not — and the
// answers are not the obvious ones in either direction.
use cbor::*;

// Render bytes as hex so an expected encoding can be written out in full rather
// than asserted one index at a time.
fn wex_hex(b: vector<u8>) -> text {
  s = "";
  for x in b { s += "{x:02x}"; }
  s
}

// @CBR-001 — canonical means the ORDER YOU BUILT THE MAP IN DOES NOT REACH THE
// WIRE.  `encode` sorts a map's entries by their encoded key bytes (RFC 8949
// §4.2.1) before emitting, so two maps built in different orders are byte-identical
// afterwards.  That is the property the whole library is for: a signature over the
// bytes stays valid no matter how the record was assembled, on any target, in any
// version of your code that happens to add fields in a different order.  It also
// means you must not treat the encoded order as your order — read a decoded map by
// key, never by position.
fn test_the_order_you_build_a_map_in_does_not_reach_the_wire() {
  b_then_a = CMap { entries: [
    CborEntry { key: CText { value: "b" }, value: CInt { value: 2 } },
    CborEntry { key: CText { value: "a" }, value: CInt { value: 1 } },
  ] };
  a_then_b = CMap { entries: [
    CborEntry { key: CText { value: "a" }, value: CInt { value: 1 } },
    CborEntry { key: CText { value: "b" }, value: CInt { value: 2 } },
  ] };
  // a2 = map(2) ; 6161 = text "a" ; 01 ; 6162 = text "b" ; 02
  assert(wex_hex(encode(a_then_b)) == "a2616101616202", "the canonical byte string");
  assert(wex_hex(encode(b_then_a)) == wex_hex(encode(a_then_b)),
         "and building it the other way round produces exactly the same bytes");

  // Sorting is by the encoded KEY BYTES, so it is length-then-value, not the
  // alphabetical order of the loft text: a shorter key sorts before a longer one.
  short_long = CMap { entries: [
    CborEntry { key: CText { value: "bb" }, value: CInt { value: 2 } },
    CborEntry { key: CText { value: "a" }, value: CInt { value: 1 } },
  ] };
  // a2 | 6161 "a" | 01 | 626262 "bb" | 02
  assert(wex_hex(encode(short_long)) == "a261610162626202",
         "the one-byte key comes first because its ENCODING sorts first");
}

// @CBR-002 — `CBytes` and `CText` are different wire types, so the loft-side
// choice is part of what gets signed.  The same two payload bytes encode with a
// different head — `42` (major 2, a byte string of length 2) versus `62` (major 3,
// a text string of length 2) — and a peer decoding the record sees a different
// type, not a different spelling of the same thing.  This is the whole reason to
// reach for CBOR over JSON: a byte string is native here rather than base64 in a
// text field, and picking the wrong one produces a valid record that means
// something else.
fn test_bytes_and_text_are_different_wire_types() {
  as_text = CText { value: "hi" };
  as_bytes = CBytes { value: [104 as u8, 105 as u8] };   // the same two bytes
  assert(wex_hex(encode(as_text)) == "626869", "text: major 3, length 2, then 'hi'");
  assert(wex_hex(encode(as_bytes)) == "426869", "bytes: major 2, same length, same payload");
  assert(wex_hex(encode(as_text)) != wex_hex(encode(as_bytes)),
         "identical payload, different records — a signature does not carry across");

  // Integers use the shortest head that fits, which is what makes the form
  // canonical, and negatives are their own major type rather than a sign bit.
  assert(wex_hex(encode(CInt { value: 23 })) == "17", "0..23 ride inside the head byte");
  assert(wex_hex(encode(CInt { value: 24 })) == "1818", "24 needs one more byte, and takes exactly one");
  assert(wex_hex(encode(CInt { value: 0 - 1 })) == "20", "-1 is major 1, not a signed 0");
}

// @CBR-003 — `decode` answers one `ok=false` for four different problems, and one
// of them will surprise you: it REJECTS well-formed CBOR that is not CANONICAL.
// A permissive encoder elsewhere may write `1` as `18 01` (a valid long form);
// this decoder refuses it, because accepting it would mean two byte strings decode
// to one value and the signable-record property is gone.  Truncated input,
// trailing bytes after the top-level value, and empty input are the other three,
// and `ok=false` does not say which — so a failure is "these are not my bytes",
// never a diagnosis.  Check `ok` before `value`; on failure `value` means nothing.
fn test_decode_rejects_valid_but_non_canonical_input() {
  canonical: vector<u8> = [1 as u8];
  ok_one = decode(canonical);
  assert(ok_one.ok, "the canonical encoding of 1 decodes");
  assert(ok_one.next == 1, "and reports one byte consumed");

  long_form: vector<u8> = [0x18 as u8, 1 as u8];   // valid CBOR for 1, not canonical
  assert(!decode(long_form).ok, "the long form of 1 is REFUSED, not accepted as 1");

  trailing: vector<u8> = [1 as u8, 1 as u8];
  assert(!decode(trailing).ok, "a second value after the top-level one is refused");

  truncated: vector<u8> = [0x18 as u8];            // head promises a byte that is not there
  assert(!decode(truncated).ok, "truncated input is refused");

  empty: vector<u8> = [];
  assert(!decode(empty).ok, "and so is nothing at all");

  // Whatever this codec writes, it reads.
  round = decode(encode(CText { value: "hi" }));
  assert(round.ok, "its own output always round-trips");
}

// @CBR-004 — canonical is about ORDER, not UNIQUENESS: a map with two entries
// under the same key encodes without complaint and DECODES `ok=true`.  Nothing in
// the codec is going to tell you that a record carries two values for one field,
// and the duplicate survives a signature perfectly happily, so a reader that takes
// the first match and a reader that takes the last disagree about a record both
// consider valid.  Uniqueness is the caller's to enforce, before encoding.
fn test_duplicate_keys_are_encoded_and_decoded_without_complaint() {
  dup = CMap { entries: [
    CborEntry { key: CText { value: "k" }, value: CInt { value: 1 } },
    CborEntry { key: CText { value: "k" }, value: CInt { value: 2 } },
  ] };
  enc = encode(dup);
  assert(wex_hex(enc) == "a2616b01616b02", "both entries are on the wire, in a stable order");
  assert(decode(enc).ok, "and the decoder accepts them");

  // The stable order is the point of the tie-break: a malformed map still encodes
  // deterministically rather than dropping or duplicating an entry at random.
  dup_other_way = CMap { entries: [
    CborEntry { key: CText { value: "k" }, value: CInt { value: 2 } },
    CborEntry { key: CText { value: "k" }, value: CInt { value: 1 } },
  ] };
  assert(wex_hex(encode(dup_other_way)) == "a2616b02616b01",
         "equal keys keep their original relative order — deterministic, still wrong data");
}