← 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.
| Item | Defined | Worked example | Call sites |
|---|---|---|---|
CborValue enum | 26 | CBR-002 | 32 38 39 156 192 263 … (7 total) |
CborEntry struct | 37 | — | 33 107 276 287 48 49 … (25 total) |
key const | 38 | — | 115 287 48 49 55 56 … (24 total) |
value const | 39 | — | 28 29 30 31 140 159 … (85 total) |
encode fn | 156 | CBR-004 | 115 140 178 14 16 21 … (33 total) |
Decoded struct | 191 | — | 207 209 247 249 250 255 … (19 total) |
value const | 192 | — | 28 29 30 31 39 140 … (85 total) |
next const | 193 | — | 209 247 249 250 255 256 … (21 total) |
ok const | 194 | — | 209 247 249 250 255 256 … (35 total) |
MAX_DEPTH const | 201 | — | 209 92 93 |
decode fn | 312 | CBR-004 | 14 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");
}