From 468855eef1db0ff73c62be2d1bf176ffa0e1478e Mon Sep 17 00:00:00 2001 From: David Legrand Date: Thu, 7 May 2026 13:44:25 +0200 Subject: [PATCH] encoding.cbor: add CBOR (RFC 8949) module (#27018) --- examples/cbor.v | 78 ++ vlib/encoding/cbor/README.md | 218 ++++ vlib/encoding/cbor/cbor.v | 54 + vlib/encoding/cbor/decoder.v | 934 ++++++++++++++++++ vlib/encoding/cbor/encoder.v | 702 +++++++++++++ vlib/encoding/cbor/errors.v | 165 ++++ vlib/encoding/cbor/generic.v | 777 +++++++++++++++ vlib/encoding/cbor/half.v | 114 +++ vlib/encoding/cbor/marshaler.v | 31 + vlib/encoding/cbor/raw.v | 41 + vlib/encoding/cbor/stream.v | 86 ++ vlib/encoding/cbor/tags.v | 22 + vlib/encoding/cbor/tests/appendix_a.json | 636 ++++++++++++ vlib/encoding/cbor/tests/canonical_test.v | 148 +++ vlib/encoding/cbor/tests/cbor_wg/appA_mt0.edn | 61 ++ vlib/encoding/cbor/tests/cbor_wg/appA_mt1.edn | 31 + vlib/encoding/cbor/tests/cbor_wg/appA_mt2.edn | 16 + vlib/encoding/cbor/tests/cbor_wg/appA_mt3.edn | 41 + vlib/encoding/cbor/tests/cbor_wg/appA_mt4.edn | 26 + vlib/encoding/cbor/tests/cbor_wg/appA_mt5.edn | 31 + vlib/encoding/cbor/tests/cbor_wg/appA_mt6.edn | 51 + .../cbor/tests/cbor_wg/appA_mt7-float.edn | 131 +++ .../cbor/tests/cbor_wg/appA_mt7-simple.edn | 36 + .../cbor/tests/cbor_wg/appA_streaming.edn | 72 ++ .../cbor/tests/cbor_wg/rfc8949_bad.edn | 197 ++++ .../cbor/tests/cbor_wg/rfc8949_good.edn | 526 ++++++++++ vlib/encoding/cbor/tests/cbor_wg_test.v | 152 +++ vlib/encoding/cbor/tests/cose_cwt_test.v | 258 +++++ vlib/encoding/cbor/tests/generic_test.v | 284 ++++++ vlib/encoding/cbor/tests/hardening_test.v | 831 ++++++++++++++++ .../cbor/tests/rfc8949_appendix_a_test.v | 781 +++++++++++++++ vlib/encoding/cbor/tests/security_test.v | 251 +++++ vlib/encoding/cbor/tests/smoke_test.v | 8 + vlib/encoding/cbor/tests/time_test.v | 39 + .../cbor/tests/upstream_appendix_a_test.v | 348 +++++++ vlib/encoding/cbor/value.v | 338 +++++++ 36 files changed, 8515 insertions(+) create mode 100644 examples/cbor.v create mode 100644 vlib/encoding/cbor/README.md create mode 100644 vlib/encoding/cbor/cbor.v create mode 100644 vlib/encoding/cbor/decoder.v create mode 100644 vlib/encoding/cbor/encoder.v create mode 100644 vlib/encoding/cbor/errors.v create mode 100644 vlib/encoding/cbor/generic.v create mode 100644 vlib/encoding/cbor/half.v create mode 100644 vlib/encoding/cbor/marshaler.v create mode 100644 vlib/encoding/cbor/raw.v create mode 100644 vlib/encoding/cbor/stream.v create mode 100644 vlib/encoding/cbor/tags.v create mode 100644 vlib/encoding/cbor/tests/appendix_a.json create mode 100644 vlib/encoding/cbor/tests/canonical_test.v create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt0.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt1.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt2.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt3.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt4.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt5.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt6.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt7-float.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_mt7-simple.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/appA_streaming.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/rfc8949_bad.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg/rfc8949_good.edn create mode 100644 vlib/encoding/cbor/tests/cbor_wg_test.v create mode 100644 vlib/encoding/cbor/tests/cose_cwt_test.v create mode 100644 vlib/encoding/cbor/tests/generic_test.v create mode 100644 vlib/encoding/cbor/tests/hardening_test.v create mode 100644 vlib/encoding/cbor/tests/rfc8949_appendix_a_test.v create mode 100644 vlib/encoding/cbor/tests/security_test.v create mode 100644 vlib/encoding/cbor/tests/smoke_test.v create mode 100644 vlib/encoding/cbor/tests/time_test.v create mode 100644 vlib/encoding/cbor/tests/upstream_appendix_a_test.v create mode 100644 vlib/encoding/cbor/value.v diff --git a/examples/cbor.v b/examples/cbor.v new file mode 100644 index 000000000..c28e628aa --- /dev/null +++ b/examples/cbor.v @@ -0,0 +1,78 @@ +module main + +import encoding.cbor +import encoding.hex +import time + +struct Address { + street string + city string + zip string @[cbor: 'postal_code'] +} + +struct User { + name string + age u32 + email ?string + tags []string + address Address + signed_up time.Time + internal string @[skip] +} + +fn main() { + user := User{ + name: 'Alice' + age: 30 + email: 'alice@example.com' + tags: ['admin', 'beta'] + address: Address{ + street: '1 Test Lane' + city: 'Paris' + zip: '75000' + } + signed_up: time.parse_iso8601('2025-01-15T10:00:00Z') or { time.now() } + internal: 'will not be encoded' + } + + // 1. Generic typed encode/decode + bytes := cbor.encode[User](user, cbor.EncodeOpts{})! + println('encoded ${bytes.len} bytes: ${hex.encode(bytes)}') + + back := cbor.decode[User](bytes, cbor.DecodeOpts{})! + println('round-trip name=${back.name} age=${back.age} city=${back.address.city}') + + // 2. Canonical encoding for stable hashing/signing + mut m := map[string]int{} + m['z'] = 26 + m['a'] = 1 + m['m'] = 13 + canonical := cbor.encode[map[string]int](m, cbor.EncodeOpts{ + canonical: true + })! + println('canonical map: ${hex.encode(canonical)}') + + // 3. Decode an unknown payload into a Value tree + v := cbor.decode[cbor.Value](bytes, cbor.DecodeOpts{})! + if name_val := v.get('name') { + if s := name_val.as_string() { + println('peeked name from Value tree: ${s}') + } + } + + // 4. Manual streaming: build a CBOR array of mixed types + mut p := cbor.new_packer(cbor.EncodeOpts{}) + p.pack_array_header(3) + p.pack_uint(42) + p.pack_text('hello') + p.pack_bool(true) + stream_bytes := p.bytes() + println('manual stream: ${hex.encode(stream_bytes)}') + + mut up := cbor.new_unpacker(stream_bytes, cbor.DecodeOpts{}) + n := up.unpack_array_header()! + first := up.unpack_uint()! + second := up.unpack_text()! + third := up.unpack_bool()! + println('unpacked array of ${n}: ${first}, "${second}", ${third}') +} diff --git a/vlib/encoding/cbor/README.md b/vlib/encoding/cbor/README.md new file mode 100644 index 000000000..b8e0fe498 --- /dev/null +++ b/vlib/encoding/cbor/README.md @@ -0,0 +1,218 @@ +## Description + +`encoding.cbor` is an RFC 8949 Concise Binary Object Representation codec. + +CBOR is a compact, schema-free binary format that supports the same value +model as JSON (numbers, strings, arrays, maps) plus byte strings, tagged +items, IEEE 754 floats at three widths, and a small set of "simple" +values (`true`, `false`, `null`, `undefined`). It is used by COSE/CWT +(IETF security stack), WebAuthn/FIDO2, the Matter smart-home protocol, +and many IoT stacks because messages are typically 30–60 % smaller than +JSON and parse without quoting/escaping. + +Three layers of API are available: + +* `encode[T]` / `decode[T]` — comptime-driven generic API. Works on + primitives, strings, arrays, maps, structs (with `@[cbor: 'name']`, + `@[skip]`, `@[cbor_rename_all: 'snake_case']`), enums, `time.Time` + (auto-tagged), and any type implementing `Marshaler` / `Unmarshaler`. +* `Packer` / `Unpacker` — manual streaming API. Use when the schema + isn't known at compile time, or when you need full control over tags, + indefinite-length items and simple values. +* `Value` sumtype — dynamic representation for round-tripping unknown + payloads or inspecting tagged data. + +Defaults follow RFC 8949 *preferred serialisation* (§4.2.2): floats +shrink to the shortest IEEE 754 width that preserves their value, and +every length argument uses the shortest encoding. Set +`EncodeOpts.canonical = true` to additionally sort map keys for +hash/signature stability (§4.2.1, deterministic encoding). Set +`EncodeOpts.validate_utf8 = true` if callers may build strings from raw +bytes (e.g. `bytestr()`) — the streaming `pack_text` trusts its input +for performance, but `encode[T]` will then refuse to emit non-UTF-8 +text strings the strict-by-default decoder would reject on the way back. + +## Usage + +### encode[T] / decode[T] + +```v +import encoding.cbor +import time + +struct Person { + name string + age int + email ?string + birthday time.Time +} + +fn main() { + bob := Person{ + name: 'Bob' + age: 30 + birthday: time.now() + } + + bytes := cbor.encode[Person](bob, cbor.EncodeOpts{})! + // bytes is []u8 — wire-ready CBOR + + back := cbor.decode[Person](bytes, cbor.DecodeOpts{})! + assert back.name == 'Bob' +} +``` + +Optional fields (`?T`) encode as CBOR `null` when set to `none`. Enums +encode as their underlying integer. + +### Struct attributes + +```v oksyntax +@[cbor_rename_all: 'kebab-case'] +struct Login { + user_name string @[cbor: 'u'] // emit/read key "u" (overrides rename_all) + password string @[skip] // never serialise + remember bool // becomes "remember" + is_admin bool // becomes "is-admin" +} +``` + +The `@[cbor_rename_all: '...']` attribute on a struct applies a global +rename strategy to every field that doesn't have an explicit +`@[cbor: '...']` override — supported strategies: `snake_case`, +`camelCase`, `PascalCase`, `kebab-case`, `SCREAMING_SNAKE_CASE`. Use +`@[cbor: '-']` as an alternative to `@[skip]`. + +### Manual streaming with Packer / Unpacker + +Use this when the schema is dynamic or when you need access to CBOR +features that don't map directly to V types (tags, indefinite-length +strings, custom simple values): + +```v +import encoding.cbor + +fn main() { + mut p := cbor.new_packer(cbor.EncodeOpts{}) + p.pack_array_header(3) + p.pack_uint(42) + p.pack_text('hello') + p.pack_bool(true) + bytes := p.bytes() + + mut u := cbor.new_unpacker(bytes, cbor.DecodeOpts{}) + n := u.unpack_array_header()! // 3 + a := u.unpack_uint()! // 42 + b := u.unpack_text()! // 'hello' + c := u.unpack_bool()! // true + _ = n + _ = a + _ = b + _ = c +} +``` + +### Dynamic values with `Value` + +When the payload schema is unknown at compile time, decode into +`cbor.Value` and walk the sumtype: + +```v +import encoding.cbor + +fn main() { + bytes := cbor.encode[map[string]int]({ + 'a': 1 + 'b': 2 + }, cbor.EncodeOpts{})! + + v := cbor.decode[cbor.Value](bytes, cbor.DecodeOpts{})! + if val := v.get('a') { + if i := val.as_int() { + assert i == 1 + } + } +} +``` + +`Value` covers every CBOR type: `IntNum`, `FloatNum`, `Text`, `Bytes`, +`Array`, `Map`, `Tag`, `Bool`, `Null`, `Undefined`, `Simple`. Re-encoding +a `Value` round-trips bit-for-bit when the source was already in +preferred form. + +### Custom Marshaler / Unmarshaler + +For types that need a custom on-wire representation, implement either +side of the interface: + +```v oksyntax +import encoding.cbor + +struct Color { +mut: + r u8 + g u8 + b u8 +} + +pub fn (c Color) to_cbor() []u8 { + mut p := cbor.new_packer(cbor.EncodeOpts{}) + p.pack_array_header(3) + p.pack_uint(c.r) + p.pack_uint(c.g) + p.pack_uint(c.b) + return p.bytes().clone() +} + +pub fn (mut c Color) from_cbor(data []u8) ! { + mut u := cbor.new_unpacker(data, cbor.DecodeOpts{}) + n := u.unpack_array_header()! + if n != 3 { + return error('Color expects 3 elements') + } + c.r = u8(u.unpack_uint()!) + c.g = u8(u.unpack_uint()!) + c.b = u8(u.unpack_uint()!) +} +``` + +`to_cbor` must return exactly one well-formed CBOR data item — the +generic encoder copies the bytes verbatim. `from_cbor` receives a slice +already trimmed to one item. + +### Canonical (deterministic) encoding + +For hashing or signing, set `canonical: true` so that map keys are +sorted by length-then-lexicographic order (RFC 8949 §4.2.1): + +```v oksyntax +import encoding.cbor + +bytes := cbor.encode[map[string]int]({ + 'b': 2 + 'a': 1 +}, cbor.EncodeOpts{ canonical: true })! +// keys are emitted in the order "a", "b" regardless of input order +``` + +### Tags and `time.Time` + +Values of type `time.Time` round-trip losslessly: whole-second values +use tag 1 (epoch seconds, integer) for the smallest canonical wire, +and sub-second values use tag 0 (RFC 3339 string with nanosecond +precision) — necessary because a tag-1 float can't carry both a +10-digit unix epoch and 9 fractional digits. Decode accepts tag 0 +(RFC 3339 text, any sub-second precision) or tag 1 (integer or float). +Custom tags can be emitted/read via `pack_tag` / `unpack_tag` or by +constructing a `Value` with `cbor.new_tag(number, content)`. + +## Conformance + +The test suite (`vlib/encoding/cbor/tests/`) covers every vector from +RFC 8949 Appendix A, plus indefinite-length strings, depth limits, +malformed-input rejection, UTF-8 validation, canonical ordering, and +tagged time round-trips. + +```bash +v test vlib/encoding/cbor/tests/ +``` \ No newline at end of file diff --git a/vlib/encoding/cbor/cbor.v b/vlib/encoding/cbor/cbor.v new file mode 100644 index 000000000..43f4432f1 --- /dev/null +++ b/vlib/encoding/cbor/cbor.v @@ -0,0 +1,54 @@ +// Package cbor implements RFC 8949 (Concise Binary Object Representation). +// +// Three layers of API are available: +// +// * `encode[T]` / `decode[T]` — comptime-driven generic API. Works on +// primitives, strings, arrays, maps with any scalar key, structs +// (with `@[cbor: 'name']` and `@[skip]` attributes), enums, +// `time.Time` (auto-tagged), and any type implementing +// `Marshaler` / `Unmarshaler`. +// +// * `Packer` / `Unpacker` — manual streaming API. Use when the schema +// isn't known at compile time, or when you need full control over +// tags, indefinite-length items and simple values. +// +// * `Value` sumtype — dynamic representation for round-tripping +// unknown payloads or inspecting tagged data. +// +// Defaults follow RFC 8949 *preferred serialisation* (§4.2.2): floats +// shrink to the shortest IEEE 754 width that preserves their value, and +// every length argument uses the shortest encoding. Set +// `EncodeOpts.canonical = true` to additionally sort map keys for +// hash/signature stability (§4.2.1, deterministic encoding). +module cbor + +// encode serialises any V value into CBOR bytes. The returned slice +// owns its backing buffer (V's GC tracks it) — no copy, so the returned +// bytes are safe to keep across calls and to pass to other modules. +pub fn encode[T](val T, opts EncodeOpts) ![]u8 { + mut p := new_packer(opts) + p.pack[T](val)! + return p.bytes() +} + +// decode parses CBOR bytes into a value of type T. Rejects extra bytes +// after the top-level item by default — callers feeding a buffer that +// holds multiple concatenated items (or that may carry an unrelated +// suffix) must opt in via `DecodeOpts.allow_trailing_bytes = true` and +// drive an `Unpacker` themselves. +// +// A leading self-describe tag (`d9 d9 f7`, RFC 8949 §3.4.6) is stripped +// transparently so payloads encoded with `EncodeOpts.self_describe` +// round-trip through `decode[T]` without the caller having to peel it. +pub fn decode[T](data []u8, opts DecodeOpts) !T { + mut u := new_unpacker(data, opts) + if u.data.len - u.pos >= 3 && u.data[u.pos] == 0xd9 && u.data[u.pos + 1] == 0xd9 + && u.data[u.pos + 2] == 0xf7 { + u.pos += 3 + } + value := u.unpack[T]()! + if !opts.allow_trailing_bytes && !u.done() { + return malformed(u.pos, '${u.remaining()} trailing byte(s) after top-level item') + } + return value +} diff --git a/vlib/encoding/cbor/decoder.v b/vlib/encoding/cbor/decoder.v new file mode 100644 index 000000000..7404f4f4b --- /dev/null +++ b/vlib/encoding/cbor/decoder.v @@ -0,0 +1,934 @@ +module cbor + +import math + +// DecodeOpts tunes the decoder. Defaults are conservative: UTF-8 is +// validated, depth is capped to fend off stack-blow-up payloads, and +// duplicate map keys are tolerated (callers that need detection turn +// `deny_duplicate_keys` on). +pub struct DecodeOpts { +pub: + max_depth int = 256 + max_stream_bytes int // 0 = unbounded for stream readers + validate_utf8 bool = true + deny_unknown_fields bool // struct decode rejects unmapped keys + deny_duplicate_keys bool // Map decode rejects repeated keys + allow_trailing_bytes bool // accept extra bytes after the top-level item +} + +// Kind classifies the next item without consuming it. Useful to branch +// before committing to a typed read. +pub enum Kind { + unsigned // major type 0 + negative // major type 1 + bytes // major type 2 (definite or indefinite) + text // major type 3 (definite or indefinite) + array_val // major type 4 (definite or indefinite) + map_val // major type 5 (definite or indefinite) + tag_val // major type 6 + bool_val // simple 20/21 + null_val // simple 22 + undefined // simple 23 + simple_val // other simple values + float_val // half/single/double + break_code // 0xff outside a definite header +} + +// Unpacker walks a CBOR byte slice. Operates non-allocating where +// possible; strings and bytes returned by `unpack_text` / `unpack_bytes` +// always own their storage so they outlive the input buffer. +pub struct Unpacker { +pub mut: + data []u8 + pos int + opts DecodeOpts +} + +// new_unpacker constructs an Unpacker over the given byte slice. +pub fn new_unpacker(data []u8, opts DecodeOpts) Unpacker { + cap := if opts.max_depth > 0 { + opts + } else { + DecodeOpts{ + ...opts + max_depth: 256 + } + } + return Unpacker{ + data: data + pos: 0 + opts: cap + } +} + +// remaining returns the number of unread bytes. +@[inline] +pub fn (u &Unpacker) remaining() int { + return u.data.len - u.pos +} + +// done reports whether the unpacker has consumed every byte. +@[inline] +pub fn (u &Unpacker) done() bool { + return u.pos >= u.data.len +} + +// -------------------------------------------------------------------- +// Low-level byte reads +// -------------------------------------------------------------------- + +@[direct_array_access; inline] +fn (mut u Unpacker) read_byte() !u8 { + if u.pos >= u.data.len { + return eof_at(u.pos) + } + b := u.data[u.pos] + u.pos++ + return b +} + +@[direct_array_access; inline] +fn (u &Unpacker) peek_byte() !u8 { + if u.pos >= u.data.len { + return eof_at(u.pos) + } + return u.data[u.pos] +} + +@[direct_array_access; inline] +fn (mut u Unpacker) read_be_u16() !u16 { + if u.pos + 2 > u.data.len { + return eof_needing(u.pos, 2, u.data.len - u.pos) + } + v := u16(u.data[u.pos]) << 8 | u16(u.data[u.pos + 1]) + u.pos += 2 + return v +} + +@[direct_array_access; inline] +fn (mut u Unpacker) read_be_u32() !u32 { + if u.pos + 4 > u.data.len { + return eof_needing(u.pos, 4, u.data.len - u.pos) + } + v := u32(u.data[u.pos]) << 24 | u32(u.data[u.pos + 1]) << 16 | u32(u.data[u.pos + 2]) << 8 | u32(u.data[ + u.pos + 3]) + u.pos += 4 + return v +} + +@[direct_array_access; inline] +fn (mut u Unpacker) read_be_u64() !u64 { + if u.pos + 8 > u.data.len { + return eof_needing(u.pos, 8, u.data.len - u.pos) + } + v := u64(u.data[u.pos]) << 56 | u64(u.data[u.pos + 1]) << 48 | u64(u.data[u.pos + 2]) << 40 | u64(u.data[ + u.pos + 3]) << 32 | u64(u.data[u.pos + 4]) << 24 | u64(u.data[u.pos + 5]) << 16 | u64(u.data[ + u.pos + 6]) << 8 | u64(u.data[u.pos + 7]) + u.pos += 8 + return v +} + +// read_arg reads the additional-info argument for the given initial +// byte. Returns -1 to signal indefinite-length (info == 31) for major +// types that allow it; the caller decides whether that's legal. +fn (mut u Unpacker) read_arg(info u8) !u64 { + match info { + 0...23 { return u64(info) } + 24 { return u64(u.read_byte()!) } + 25 { return u64(u.read_be_u16()!) } + 26 { return u64(u.read_be_u32()!) } + 27 { return u.read_be_u64()! } + else { return malformed(u.pos - 1, 'reserved additional info ${info}') } + } +} + +// -------------------------------------------------------------------- +// Public peek +// -------------------------------------------------------------------- + +// peek_kind classifies the next item without consuming any input. +pub fn (u &Unpacker) peek_kind() !Kind { + if u.pos >= u.data.len { + return eof_at(u.pos) + } + b := u.data[u.pos] + major := b >> 5 + info := b & 0x1f + match major { + 0 { + return .unsigned + } + 1 { + return .negative + } + 2 { + return .bytes + } + 3 { + return .text + } + 4 { + return .array_val + } + 5 { + return .map_val + } + 6 { + return .tag_val + } + else { + match info { + 20, 21 { return .bool_val } + 22 { return .null_val } + 23 { return .undefined } + 25, 26, 27 { return .float_val } + 31 { return .break_code } + else { return .simple_val } + } + } + } +} + +// -------------------------------------------------------------------- +// High-level typed reads +// -------------------------------------------------------------------- + +// unpack_uint reads a non-negative integer (major type 0). Errors on +// negatives, floats, or other major types. Position is rolled back on +// any error so callers can branch on `peek_kind` and try a different +// read (same convention as `unpack_bool` / `unpack_text`). +pub fn (mut u Unpacker) unpack_uint() !u64 { + start := u.pos + b := u.read_byte()! + major := b >> 5 + if major != 0 { + u.pos = start + return type_mismatch(start, 'unsigned', b) + } + return u.read_arg(b & 0x1f) or { + u.pos = start + return err + } +} + +// unpack_int reads any CBOR integer (major type 0 or 1) into i64. Errors +// when the magnitude exceeds i64 range; use `unpack_int_full` to pull +// values as u64 with a separate sign flag. +pub fn (mut u Unpacker) unpack_int() !i64 { + start := u.pos + b := u.read_byte()! + major := b >> 5 + arg := u.read_arg(b & 0x1f)! + if major == 0 { + if arg > u64(max_i64) { + u.pos = start + return int_range(start, 'i64', arg.str()) + } + return i64(arg) + } + if major == 1 { + // Represented integer = -1 - arg. + if arg > u64(max_i64) { + u.pos = start + return int_range(start, 'i64', '-1 - ${arg}') + } + return -1 - i64(arg) + } + u.pos = start + return type_mismatch(start, 'integer', b) +} + +// unpack_int_full returns (negative, magnitude). For unsigned values +// negative=false and magnitude is the raw u64. For negative values +// negative=true and magnitude is the encoded argument (the integer +// itself is `-1 - magnitude`). +pub fn (mut u Unpacker) unpack_int_full() !(bool, u64) { + start := u.pos + b := u.read_byte()! + major := b >> 5 + arg := u.read_arg(b & 0x1f)! + if major == 0 { + return false, arg + } + if major == 1 { + return true, arg + } + u.pos = start + return type_mismatch(start, 'integer', b) +} + +// unpack_bool reads a CBOR boolean (simple 20/21). Position is rolled +// back on a type mismatch so callers can branch on `peek_kind` and try +// a different read. +pub fn (mut u Unpacker) unpack_bool() !bool { + start := u.pos + b := u.read_byte()! + if b == 0xf4 { + return false + } + if b == 0xf5 { + return true + } + u.pos = start + return type_mismatch(start, 'bool', b) +} + +// unpack_null consumes a CBOR null (0xf6) or errors with type mismatch. +// Position is rolled back on mismatch (same convention as unpack_bool). +pub fn (mut u Unpacker) unpack_null() ! { + start := u.pos + b := u.read_byte()! + if b != 0xf6 { + u.pos = start + return type_mismatch(start, 'null', b) + } +} + +// unpack_float reads a CBOR float of any width (half/single/double) and +// returns it as f64. +pub fn (mut u Unpacker) unpack_float() !f64 { + start := u.pos + b := u.read_byte()! + match b { + 0xf9 { + h := u.read_be_u16()! + return half_to_f64(h) + } + 0xfa { + bits := u.read_be_u32()! + return f64(math.f32_from_bits(bits)) + } + 0xfb { + bits := u.read_be_u64()! + return math.f64_from_bits(bits) + } + else { + u.pos = start + return type_mismatch(start, 'float', b) + } + } +} + +// unpack_simple reads a simple value (0..255). Bool/null/undefined are +// also simple values; this method returns the raw u8. +pub fn (mut u Unpacker) unpack_simple() !u8 { + start := u.pos + b := u.read_byte()! + if b >= 0xe0 && b <= 0xf3 { + return b & 0x1f + } + match b { + 0xf4 { + return 20 + } + 0xf5 { + return 21 + } + 0xf6 { + return 22 + } + 0xf7 { + return 23 + } + 0xf8 { + v := u.read_byte()! + if v < 32 { + u.pos = start + return malformed(start, 'simple value < 32 must use 1-byte form') + } + return v + } + else { + u.pos = start + return type_mismatch(start, 'simple', b) + } + } +} + +// unpack_text reads a definite or indefinite-length text string. The +// returned string owns its bytes (it's a clone of the input slice). +// UTF-8 validation runs unless `DecodeOpts.validate_utf8` is false. +pub fn (mut u Unpacker) unpack_text() !string { + start := u.pos + b := u.read_byte()! + major := b >> 5 + if major != 3 { + u.pos = start + return type_mismatch(start, 'text', b) + } + info := b & 0x1f + if info == 31 { + return u.read_indef_text()! + } + size := u.read_arg(info)! + return u.read_text_chunk(size)! +} + +// read_text_chunk consumes `size` bytes as a UTF-8 text fragment. The +// argument is u64 because CBOR allows lengths up to 2^64-1 in the wire +// format; the function rejects any length that the host can't represent +// or that exceeds the available payload, so neither the bounds check +// nor the slice can panic on adversarial input. +@[direct_array_access] +fn (mut u Unpacker) read_text_chunk(size u64) !string { + if size > u64(u.data.len - u.pos) { + return eof_oversized(u.pos, size, u.data.len - u.pos) + } + size_int := int(size) + bytes_start := u.pos + u.pos += size_int + if u.opts.validate_utf8 && !utf8_validate_slice(u.data, bytes_start, size_int) { + return InvalidUtf8Error{ + pos: bytes_start + } + } + return u.data[bytes_start..u.pos].bytestr() +} + +fn (mut u Unpacker) read_indef_text() !string { + mut acc := strings_builder_new() + for { + b := u.read_byte()! + if b == 0xff { + break + } + major := b >> 5 + info := b & 0x1f + if major != 3 || info == 31 { + return malformed(u.pos - 1, + 'indefinite-length text chunk must be a definite-length text string') + } + size := u.read_arg(info)! + s := u.read_text_chunk(size)! + acc.write_string(s) + } + return acc.str() +} + +// unpack_bytes reads a definite or indefinite-length byte string. The +// returned slice is a clone, safe to retain after the unpacker is freed. +pub fn (mut u Unpacker) unpack_bytes() ![]u8 { + start := u.pos + b := u.read_byte()! + major := b >> 5 + if major != 2 { + u.pos = start + return type_mismatch(start, 'bytes', b) + } + info := b & 0x1f + if info == 31 { + return u.read_indef_bytes()! + } + size := u.read_arg(info)! + return u.read_bytes_chunk(size)! +} + +// read_bytes_chunk consumes `size` bytes as a byte string fragment. +// See read_text_chunk for why size is u64. +@[direct_array_access] +fn (mut u Unpacker) read_bytes_chunk(size u64) ![]u8 { + if size > u64(u.data.len - u.pos) { + return eof_oversized(u.pos, size, u.data.len - u.pos) + } + size_int := int(size) + out := u.data[u.pos..u.pos + size_int].clone() + u.pos += size_int + return out +} + +fn (mut u Unpacker) read_indef_bytes() ![]u8 { + mut acc := []u8{cap: 64} + for { + b := u.read_byte()! + if b == 0xff { + break + } + major := b >> 5 + info := b & 0x1f + if major != 2 || info == 31 { + return malformed(u.pos - 1, + 'indefinite-length bytes chunk must be a definite-length byte string') + } + size := u.read_arg(info)! + acc << u.read_bytes_chunk(size)! + } + return acc +} + +// unpack_array_header reads the prefix of an array. Returns the count +// for definite-length arrays, or -1 for indefinite-length arrays (the +// caller then loops until peek_kind() == .break_code and consumes the +// break with `expect_break`). +pub fn (mut u Unpacker) unpack_array_header() !i64 { + start := u.pos + b := u.read_byte()! + major := b >> 5 + if major != 4 { + u.pos = start + return type_mismatch(start, 'array', b) + } + info := b & 0x1f + if info == 31 { + return -1 + } + arg := u.read_arg(info)! + if arg > u64(max_i64) { + u.pos = start + return int_range(start, 'i64', arg.str()) + } + return i64(arg) +} + +// unpack_map_header reads the prefix of a map. Returns pair count or -1 +// for indefinite-length maps. +pub fn (mut u Unpacker) unpack_map_header() !i64 { + start := u.pos + b := u.read_byte()! + major := b >> 5 + if major != 5 { + u.pos = start + return type_mismatch(start, 'map', b) + } + info := b & 0x1f + if info == 31 { + return -1 + } + arg := u.read_arg(info)! + if arg > u64(max_i64) { + u.pos = start + return int_range(start, 'i64', arg.str()) + } + return i64(arg) +} + +// unpack_tag reads a tag header and returns the tag number. The caller +// must follow up by reading the tag content. Position is rolled back +// on any error so callers can branch on `peek_kind` and try a different +// read. +pub fn (mut u Unpacker) unpack_tag() !u64 { + start := u.pos + b := u.read_byte()! + major := b >> 5 + if major != 6 { + u.pos = start + return type_mismatch(start, 'tag', b) + } + return u.read_arg(b & 0x1f) or { + u.pos = start + return err + } +} + +// peek_break reports whether the next byte is the break stop code. +@[inline] +pub fn (u &Unpacker) peek_break() bool { + return u.pos < u.data.len && u.data[u.pos] == 0xff +} + +// consume_break advances past a break stop code if one is at the +// cursor, returning true. Useful for the indef-length loop pattern: +// `for { if u.consume_break() { break } ... }`. +@[inline] +fn (mut u Unpacker) consume_break() bool { + if u.peek_break() { + u.pos++ + return true + } + return false +} + +// check_container_len rejects a definite-length array or map header +// whose item count can't fit a host `int` or whose minimum byte cost +// (1 byte/item for arrays, 2 bytes/pair for maps) already exceeds the +// remaining payload. Callers use the `int(n)` cast safely after. +// +// Comparison uses `remaining / bytes_per_item` rather than +// `n * bytes_per_item` so the multiplication can't overflow at +// n ≈ i64::max. +@[inline] +fn (u &Unpacker) check_container_len(start int, n u64, bytes_per_item int, kind string) ! { + if n > u64(max_i64) || i64(n) > i64(u.data.len - u.pos) / i64(bytes_per_item) { + return malformed(start, '${kind} length ${n} exceeds remaining input') + } +} + +// expect_break consumes a single 0xff break code; errors otherwise. +pub fn (mut u Unpacker) expect_break() ! { + b := u.read_byte()! + if b != 0xff { + return malformed(u.pos - 1, 'expected break code, got 0x${b:02x}') + } +} + +// -------------------------------------------------------------------- +// Skip +// -------------------------------------------------------------------- + +// skip_value advances past one complete CBOR value without allocating. +// Honours the depth cap so adversarial deeply-nested input cannot blow +// the stack. +pub fn (mut u Unpacker) skip_value() ! { + u.skip_inner(0)! +} + +fn (mut u Unpacker) skip_inner(depth int) ! { + if depth > u.opts.max_depth { + return MaxDepthError{ + pos: u.pos + max_depth: u.opts.max_depth + } + } + b := u.read_byte()! + major := b >> 5 + info := b & 0x1f + match major { + 0, 1 { + u.read_arg(info)! + } + 2, 3 { + if info == 31 { + // RFC 8949 §3.2.3: each chunk MUST be a definite-length + // string of the same major type — no nested indefinite, + // no cross-type chunks. Mirror unpack_text/unpack_bytes. + for { + if u.consume_break() { + break + } + cb := u.read_byte()! + cmajor := cb >> 5 + cinfo := cb & 0x1f + if cmajor != major || cinfo == 31 { + return malformed(u.pos - 1, + 'indefinite-length string chunk must be a definite-length string of the same major type') + } + csize := u.read_arg(cinfo)! + if csize > u64(u.data.len - u.pos) { + return eof_oversized(u.pos, csize, u.data.len - u.pos) + } + u.pos += int(csize) + } + } else { + size := u.read_arg(info)! + if size > u64(u.data.len - u.pos) { + return eof_oversized(u.pos, size, u.data.len - u.pos) + } + u.pos += int(size) + } + } + 4 { + if info == 31 { + for { + if u.consume_break() { + break + } + u.skip_inner(depth + 1)! + } + } else { + n := u.read_arg(info)! + u.check_container_len(u.pos - 1, n, 1, 'array')! + for _ in 0 .. n { + u.skip_inner(depth + 1)! + } + } + } + 5 { + if info == 31 { + for { + if u.consume_break() { + break + } + u.skip_inner(depth + 1)! // key + u.skip_inner(depth + 1)! // value + } + } else { + n := u.read_arg(info)! + u.check_container_len(u.pos - 1, n, 2, 'map')! + for _ in 0 .. n { + u.skip_inner(depth + 1)! + u.skip_inner(depth + 1)! + } + } + } + 6 { + u.read_arg(info)! + u.skip_inner(depth + 1)! + } + else { + // Major type 7 (floats / simple). + match info { + 0...23 {} // simple values 0..23 inline + 24 { + // Per RFC 8949 §3.3, a simple value < 32 must use the + // inline form (info 0..23) — the 1-byte form is only + // well-formed for 32..255. unpack_simple already enforces + // this; skip_value must too, otherwise malformed CBOR + // slips through RawMessage / Unmarshaler / unknown-field + // skipping and lands in downstream consumers. + sv := u.read_byte()! + if sv < 32 { + return malformed(u.pos - 1, 'simple value < 32 must use 1-byte form') + } + } + 25 { + u.pos += 2 + } // half + 26 { + u.pos += 4 + } // single + 27 { + u.pos += 8 + } // double + 31 { + return malformed(u.pos - 1, 'unexpected break stop code') + } + else { + return malformed(u.pos - 1, 'reserved additional info ${info}') + } + } + + if u.pos > u.data.len { + return eof_at(u.data.len) + } + } + } +} + +// -------------------------------------------------------------------- +// Value tree decoder +// -------------------------------------------------------------------- + +// unpack_value materialises one CBOR data item as a Value. +pub fn (mut u Unpacker) unpack_value() !Value { + return u.unpack_value_inner(0)! +} + +fn (mut u Unpacker) unpack_value_inner(depth int) !Value { + if depth > u.opts.max_depth { + return MaxDepthError{ + pos: u.pos + max_depth: u.opts.max_depth + } + } + start := u.pos + b := u.read_byte()! + major := b >> 5 + info := b & 0x1f + match major { + 0 { + arg := u.read_arg(info)! + return IntNum{ + negative: false + magnitude: arg + } + } + 1 { + arg := u.read_arg(info)! + return IntNum{ + negative: true + magnitude: arg + } + } + 2 { + u.pos = start + data := u.unpack_bytes()! + return Bytes{ + data: data + } + } + 3 { + u.pos = start + s := u.unpack_text()! + return Text{ + value: s + } + } + 4 { + if info == 31 { + mut elements := []Value{cap: 4} + for { + if u.consume_break() { + break + } + elements << u.unpack_value_inner(depth + 1)! + } + return Array{ + elements: elements + } + } + n := u.read_arg(info)! + u.check_container_len(start, n, 1, 'array')! + mut elements := []Value{cap: int(n)} + for _ in 0 .. n { + elements << u.unpack_value_inner(depth + 1)! + } + return Array{ + elements: elements + } + } + 5 { + // Dedup tracking hashes the raw on-wire bytes of each key (per + // RFC 8949 §5.6 "encoded data items are equal iff their byte + // representations match") in a V map → O(1) lookup vs the + // previous O(n) linear scan, so adversarial inputs with + // thousands of distinct keys decode in linear time. Built + // only when the option is set. + mut seen := map[string]bool{} + if info == 31 { + mut pairs := []MapPair{cap: 4} + for { + if u.consume_break() { + break + } + key_start := u.pos + key := u.unpack_value_inner(depth + 1)! + if u.opts.deny_duplicate_keys { + k := u.data[key_start..u.pos].bytestr() + if k in seen { + return malformed(key_start, 'duplicate map key') + } + seen[k] = true + } + val := u.unpack_value_inner(depth + 1)! + pairs << MapPair{ + key: key + value: val + } + } + return Map{ + pairs: pairs + } + } + n := u.read_arg(info)! + u.check_container_len(start, n, 2, 'map')! + mut pairs := []MapPair{cap: int(n)} + for _ in 0 .. n { + key_start := u.pos + key := u.unpack_value_inner(depth + 1)! + if u.opts.deny_duplicate_keys { + k := u.data[key_start..u.pos].bytestr() + if k in seen { + return malformed(key_start, 'duplicate map key') + } + seen[k] = true + } + val := u.unpack_value_inner(depth + 1)! + pairs << MapPair{ + key: key + value: val + } + } + return Map{ + pairs: pairs + } + } + 6 { + number := u.read_arg(info)! + content := u.unpack_value_inner(depth + 1)! + // Native validation per RFC 8949 §3.4.1: tag 0 wraps an RFC 3339 + // text string; tag 1 wraps a numeric value (int or float). + // QCBOR does the same — accepting wrong content types here would + // allow well-formed-but-invalid payloads through. + if number == 0 && content !is Text { + return malformed(u.pos, 'tag 0 (date/time) must wrap a text string') + } + if number == 1 && content !is IntNum && content !is FloatNum { + return malformed(u.pos, 'tag 1 (epoch) must wrap a number') + } + return Tag{ + number: number + content_box: [content] + } + } + else { + match info { + 20 { + return Bool{ + value: false + } + } + 21 { + return Bool{ + value: true + } + } + 22 { + return Null{} + } + 23 { + return Undefined{} + } + 24 { + v := u.read_byte()! + if v < 32 { + u.pos = start + return malformed(start, 'simple value < 32 must use 1-byte form') + } + return Simple{ + value: v + } + } + 25 { + h := u.read_be_u16()! + return FloatNum{ + value: half_to_f64(h) + bits: .half + } + } + 26 { + bits := u.read_be_u32()! + return FloatNum{ + value: f64(math.f32_from_bits(bits)) + bits: .single + } + } + 27 { + bits := u.read_be_u64()! + return FloatNum{ + value: math.f64_from_bits(bits) + bits: .double + } + } + 31 { + u.pos = start + return malformed(start, 'unexpected break stop code') + } + else { + if info <= 19 { + return Simple{ + value: info + } + } + u.pos = start + return malformed(start, 'reserved additional info ${info}') + } + } + } + } +} + +// strings_builder_new is a small alias to keep the import surface tight +// (we only need the strings module for indefinite-length text accumulation). +@[inline] +fn strings_builder_new() StringsBuilder { + return StringsBuilder{ + buf: []u8{cap: 32} + } +} + +struct StringsBuilder { +mut: + buf []u8 +} + +@[inline] +fn (mut b StringsBuilder) write_string(s string) { + if s == '' { + return + } + unsafe { b.buf.push_many(s.str, s.len) } +} + +@[inline] +fn (mut b StringsBuilder) str() string { + return b.buf.bytestr() +} diff --git a/vlib/encoding/cbor/encoder.v b/vlib/encoding/cbor/encoder.v new file mode 100644 index 000000000..60dacd56d --- /dev/null +++ b/vlib/encoding/cbor/encoder.v @@ -0,0 +1,702 @@ +module cbor + +import math + +// EncodeOpts tunes the encoder. Defaults yield RFC 8949 *preferred* +// serialisation: floats shrink to the shortest IEEE 754 width that +// preserves their value, headers use the shortest length encoding. +// +// Setting `canonical = true` additionally sorts map keys per RFC 8949 +// §4.2.1 (deterministic encoding) — useful for hashing/signing. +pub struct EncodeOpts { +pub: + initial_cap int = 64 + canonical bool // sort map keys, definite-length only + self_describe bool // prepend tag 55799 (`d9 d9 f7`) + // validate_utf8 makes encode[T] reject V `string` payloads that + // contain non-UTF-8 bytes. Off by default to match the conventional + // V invariant ("strings are UTF-8") and avoid paying for validation + // on hot paths. Turn on at trust boundaries when callers may build + // strings from raw bytes (e.g. `bytestr()`), so the wire stays + // round-trip-safe against the strict-by-default decoder. + validate_utf8 bool +} + +// Packer accumulates CBOR bytes into an internal buffer. Use `bytes()` +// to retrieve the wire output, or `reset()` to reuse the buffer for the +// next message — that's the cheapest way to emit many small frames. +// +// `indef_string_open` and `indef_other_depth` track open indefinite-length +// items so the encoder can reject malformed compositions: nested indef +// strings, indef array/map inside an indef string (RFC 8949 §3.2.3), or +// a stray break code. +pub struct Packer { +pub mut: + buf []u8 + opts EncodeOpts +mut: + indef_string_open bool // top of the indef "stack" is text or bytes + indef_other_depth int // count of currently open indef arrays/maps +} + +// new_packer builds a Packer with the given options. `opts.initial_cap` +// reserves the buffer up-front; oversize is harmless, undersize triggers +// the usual growth policy. +pub fn new_packer(opts EncodeOpts) Packer { + cap := if opts.initial_cap > 0 { opts.initial_cap } else { 64 } + mut p := Packer{ + buf: []u8{cap: cap} + opts: opts + } + if opts.self_describe { + p.buf << self_describe_prefix + } + return p +} + +// bytes returns the encoded buffer. The returned slice aliases the +// Packer's storage — clone it if you keep using the Packer. This is a +// low-level accessor that does NOT verify the buffer holds a complete +// item; if you opened an indefinite-length container without closing +// it, the bytes will be malformed. Use `pack_to` (or `encode[T]`) for +// the validated path, or call `is_complete()` yourself. +@[inline] +pub fn (mut p Packer) bytes() []u8 { + return p.buf +} + +// is_complete reports whether the buffer holds a sequence of fully +// closed items. False while an indefinite-length array, map, text, or +// bytes container is still open (waiting for `pack_break`). +@[inline] +pub fn (p &Packer) is_complete() bool { + return !p.indef_string_open && p.indef_other_depth == 0 +} + +// reset clears the buffer for reuse. The capacity is preserved, so this +// is the fast path for high-throughput senders. +@[inline] +pub fn (mut p Packer) reset() { + unsafe { + p.buf.len = 0 + } + p.indef_string_open = false + p.indef_other_depth = 0 + if p.opts.self_describe { + p.buf << self_describe_prefix + } +} + +// reserve grows the buffer's capacity by at least `n` bytes. Useful +// before a string/binary write of known length to skip per-byte growth. +@[inline] +pub fn (mut p Packer) reserve(n int) { + if n <= 0 { + return + } + needed := p.buf.len + n + if needed > p.buf.cap { + mut new_cap := if p.buf.cap == 0 { 64 } else { p.buf.cap * 2 } + for new_cap < needed { + new_cap *= 2 + } + mut grown := []u8{cap: new_cap} + grown << p.buf + p.buf = grown + } +} + +// extend_unchecked grows the buffer's length by `n`. The caller must +// have already ensured enough capacity via `reserve`. Returns the +// position at which the new bytes start. +@[direct_array_access; inline] +fn (mut p Packer) extend_unchecked(n int) int { + pos := p.buf.len + unsafe { + p.buf.len = pos + n + } + return pos +} + +// -------------------------------------------------------------------- +// Low-level head writer +// -------------------------------------------------------------------- + +// write_head emits an initial byte (major type | additional info) plus +// the appropriate big-endian argument. Always uses the shortest encoding +// (RFC 8949 §4.2.1, "preferred serialization"). Hot path: avoid the +// `<<` operator (which carries cap-grow checks per byte) by reserving +// once, then using direct unsafe index writes. +@[direct_array_access; inline] +fn (mut p Packer) write_head(major u8, arg u64) { + if arg < 24 { + p.reserve(1) + pos := p.extend_unchecked(1) + unsafe { + p.buf[pos] = major | u8(arg) + } + return + } + if arg <= 0xff { + p.reserve(2) + pos := p.extend_unchecked(2) + unsafe { + p.buf[pos] = major | 24 + p.buf[pos + 1] = u8(arg) + } + return + } + if arg <= 0xffff { + p.reserve(3) + pos := p.extend_unchecked(3) + unsafe { + p.buf[pos] = major | 25 + p.buf[pos + 1] = u8(arg >> 8) + p.buf[pos + 2] = u8(arg) + } + return + } + if arg <= 0xffffffff { + p.reserve(5) + pos := p.extend_unchecked(5) + unsafe { + p.buf[pos] = major | 26 + p.buf[pos + 1] = u8(arg >> 24) + p.buf[pos + 2] = u8(arg >> 16) + p.buf[pos + 3] = u8(arg >> 8) + p.buf[pos + 4] = u8(arg) + } + return + } + p.reserve(9) + pos := p.extend_unchecked(9) + unsafe { + p.buf[pos] = major | 27 + p.buf[pos + 1] = u8(arg >> 56) + p.buf[pos + 2] = u8(arg >> 48) + p.buf[pos + 3] = u8(arg >> 40) + p.buf[pos + 4] = u8(arg >> 32) + p.buf[pos + 5] = u8(arg >> 24) + p.buf[pos + 6] = u8(arg >> 16) + p.buf[pos + 7] = u8(arg >> 8) + p.buf[pos + 8] = u8(arg) + } +} + +@[direct_array_access; inline] +fn (mut p Packer) write_be_u16(v u16) { + p.reserve(2) + pos := p.extend_unchecked(2) + unsafe { + p.buf[pos] = u8(v >> 8) + p.buf[pos + 1] = u8(v) + } +} + +@[direct_array_access; inline] +fn (mut p Packer) write_be_u32(v u32) { + p.reserve(4) + pos := p.extend_unchecked(4) + unsafe { + p.buf[pos] = u8(v >> 24) + p.buf[pos + 1] = u8(v >> 16) + p.buf[pos + 2] = u8(v >> 8) + p.buf[pos + 3] = u8(v) + } +} + +@[direct_array_access; inline] +fn (mut p Packer) write_be_u64(v u64) { + p.reserve(8) + pos := p.extend_unchecked(8) + unsafe { + p.buf[pos] = u8(v >> 56) + p.buf[pos + 1] = u8(v >> 48) + p.buf[pos + 2] = u8(v >> 40) + p.buf[pos + 3] = u8(v >> 32) + p.buf[pos + 4] = u8(v >> 24) + p.buf[pos + 5] = u8(v >> 16) + p.buf[pos + 6] = u8(v >> 8) + p.buf[pos + 7] = u8(v) + } +} + +// -------------------------------------------------------------------- +// High-level packers — primitives +// -------------------------------------------------------------------- + +// pack_uint emits a CBOR unsigned-integer (major type 0). Covers the +// full u64 range, including values above i64.max. +@[inline] +pub fn (mut p Packer) pack_uint(v u64) { + p.write_head(0x00, v) +} + +// pack_int picks the right major type for a signed integer. +// For values below i64.min that can still fit -1-u64, prefer +// `pack_negative_arg`. +@[inline] +pub fn (mut p Packer) pack_int(v i64) { + if v >= 0 { + p.write_head(0x00, u64(v)) + } else { + p.write_head(0x20, u64(-1 - v)) + } +} + +// pack_negative_arg writes a major type 1 value where the encoded +// argument is `arg` and the represented integer is `-1 - arg`. Lets you +// emit values down to -2^64 (the lower bound of CBOR negative ints). +@[inline] +pub fn (mut p Packer) pack_negative_arg(arg u64) { + p.write_head(0x20, arg) +} + +// pack_bool emits the simple value 20 (false) or 21 (true). +@[direct_array_access; inline] +pub fn (mut p Packer) pack_bool(v bool) { + p.reserve(1) + pos := p.extend_unchecked(1) + unsafe { + p.buf[pos] = if v { u8(0xf5) } else { u8(0xf4) } + } +} + +// pack_null emits CBOR null (simple value 22, byte 0xf6). +@[direct_array_access; inline] +pub fn (mut p Packer) pack_null() { + p.reserve(1) + pos := p.extend_unchecked(1) + unsafe { + p.buf[pos] = 0xf6 + } +} + +// pack_undefined emits CBOR undefined (simple value 23, byte 0xf7). +@[direct_array_access; inline] +pub fn (mut p Packer) pack_undefined() { + p.reserve(1) + pos := p.extend_unchecked(1) + unsafe { + p.buf[pos] = 0xf7 + } +} + +// pack_simple emits a CBOR simple value. Values 0..23 use the inline +// form, values 32..255 use the 1-byte trailer form. Values 24..31 are +// not well-formed per RFC 8949 §3.3 and are rejected here. +@[direct_array_access] +pub fn (mut p Packer) pack_simple(v u8) ! { + // RFC 8949 §3.3 assigns simple values 20..23 to false/true/null/ + // undefined; encoding them through pack_simple would silently produce + // wire-equivalent bytes that decode back as Bool/Null/Undefined, not + // as a Simple — surprising and ambiguous. Force the caller through + // the dedicated typed packers. + if v >= 20 && v < 24 { + return error('cbor: simple values 20..23 must be packed via pack_bool / pack_null / pack_undefined (RFC 8949 §3.3)') + } + if v < 24 { + p.reserve(1) + pos := p.extend_unchecked(1) + unsafe { + p.buf[pos] = 0xe0 | v + } + return + } + if v < 32 { + return error('cbor: simple values 24..31 are not well-formed (RFC 8949 §3.3)') + } + p.reserve(2) + pos := p.extend_unchecked(2) + unsafe { + p.buf[pos] = 0xf8 + p.buf[pos + 1] = v + } +} + +// -------------------------------------------------------------------- +// High-level packers — strings and bytes +// -------------------------------------------------------------------- + +// pack_text writes a UTF-8 text string (major type 3). Single-shot +// reservation: the head + payload bytes are appended via one capacity +// check and one memcpy. +@[direct_array_access; inline] +pub fn (mut p Packer) pack_text(s string) { + if s.len < 24 { + // Short string: head + payload fit in s.len + 1 bytes. + total := s.len + 1 + p.reserve(total) + pos := p.extend_unchecked(total) + unsafe { + p.buf[pos] = u8(0x60) | u8(s.len) + if s.len > 0 { + vmemcpy(&p.buf[pos + 1], s.str, s.len) + } + } + return + } + p.write_head(0x60, u64(s.len)) + p.reserve(s.len) + unsafe { p.buf.push_many(s.str, s.len) } +} + +// pack_bytes writes a byte string (major type 2). +@[direct_array_access] +pub fn (mut p Packer) pack_bytes(b []u8) { + if b.len < 24 { + total := b.len + 1 + p.reserve(total) + pos := p.extend_unchecked(total) + unsafe { + p.buf[pos] = u8(0x40) | u8(b.len) + if b.len > 0 { + vmemcpy(&p.buf[pos + 1], b.data, b.len) + } + } + return + } + p.write_head(0x40, u64(b.len)) + p.reserve(b.len) + unsafe { p.buf.push_many(b.data, b.len) } +} + +// -------------------------------------------------------------------- +// High-level packers — arrays, maps, tags +// -------------------------------------------------------------------- + +// pack_array_header writes the prefix for a definite-length array. +@[inline] +pub fn (mut p Packer) pack_array_header(n u64) { + p.write_head(0x80, n) +} + +// pack_map_header writes the prefix for a definite-length map. The +// argument is the number of *pairs*, not items. +@[inline] +pub fn (mut p Packer) pack_map_header(n u64) { + p.write_head(0xa0, n) +} + +// pack_tag writes a tag header (major type 6). The next packed item is +// the tag's content. +@[inline] +pub fn (mut p Packer) pack_tag(number u64) { + p.write_head(0xc0, number) +} + +// open_indef_or_error rejects opening any indef container inside an +// open indef text/bytes context (RFC 8949 §3.2.3 only allows definite +// chunks of the matching major type), then writes `head` and updates +// the tracking state. +@[direct_array_access; inline] +fn (mut p Packer) open_indef_or_error(head u8, is_string bool) ! { + if p.indef_string_open { + return error('cbor: indefinite-length string chunks must be definite-length strings of the same major type') + } + p.reserve(1) + pos := p.extend_unchecked(1) + unsafe { + p.buf[pos] = head + } + if is_string { + p.indef_string_open = true + } else { + p.indef_other_depth++ + } +} + +// pack_array_indef opens an indefinite-length array. Close with `pack_break`. +@[inline] +pub fn (mut p Packer) pack_array_indef() ! { + p.open_indef_or_error(0x9f, false)! +} + +// pack_map_indef opens an indefinite-length map. Close with `pack_break`. +@[inline] +pub fn (mut p Packer) pack_map_indef() ! { + p.open_indef_or_error(0xbf, false)! +} + +// pack_text_indef opens an indefinite-length text string. Each chunk +// must be a definite-length text string; close with `pack_break`. +@[inline] +pub fn (mut p Packer) pack_text_indef() ! { + p.open_indef_or_error(0x7f, true)! +} + +// pack_bytes_indef opens an indefinite-length byte string. Each chunk +// must be a definite-length byte string; close with `pack_break`. +@[inline] +pub fn (mut p Packer) pack_bytes_indef() ! { + p.open_indef_or_error(0x5f, true)! +} + +// pack_break writes the break stop code 0xff that terminates the most +// recently opened indefinite-length item. Errors when no item is open +// (the byte 0xff is otherwise reserved and emitting one would corrupt +// the stream). +@[direct_array_access; inline] +pub fn (mut p Packer) pack_break() ! { + if p.indef_string_open { + p.indef_string_open = false + } else if p.indef_other_depth > 0 { + p.indef_other_depth-- + } else { + return error('cbor: pack_break called with no open indefinite-length item') + } + p.reserve(1) + pos := p.extend_unchecked(1) + unsafe { + p.buf[pos] = 0xff + } +} + +// -------------------------------------------------------------------- +// High-level packers — floats with preferred serialisation +// -------------------------------------------------------------------- + +// pack_float64 always emits an 8-byte IEEE 754 float. +@[direct_array_access; inline] +pub fn (mut p Packer) pack_float64(v f64) { + p.reserve(9) + pos := p.extend_unchecked(9) + bits := math.f64_bits(v) + unsafe { + p.buf[pos] = 0xfb + p.buf[pos + 1] = u8(bits >> 56) + p.buf[pos + 2] = u8(bits >> 48) + p.buf[pos + 3] = u8(bits >> 40) + p.buf[pos + 4] = u8(bits >> 32) + p.buf[pos + 5] = u8(bits >> 24) + p.buf[pos + 6] = u8(bits >> 16) + p.buf[pos + 7] = u8(bits >> 8) + p.buf[pos + 8] = u8(bits) + } +} + +// pack_float32 always emits a 4-byte IEEE 754 float. +@[direct_array_access; inline] +pub fn (mut p Packer) pack_float32(v f32) { + p.reserve(5) + pos := p.extend_unchecked(5) + bits := math.f32_bits(v) + unsafe { + p.buf[pos] = 0xfa + p.buf[pos + 1] = u8(bits >> 24) + p.buf[pos + 2] = u8(bits >> 16) + p.buf[pos + 3] = u8(bits >> 8) + p.buf[pos + 4] = u8(bits) + } +} + +// pack_float16_bits always emits a 2-byte IEEE 754 float. +@[direct_array_access; inline] +pub fn (mut p Packer) pack_float16_bits(bits u16) { + p.reserve(3) + pos := p.extend_unchecked(3) + unsafe { + p.buf[pos] = 0xf9 + p.buf[pos + 1] = u8(bits >> 8) + p.buf[pos + 2] = u8(bits) + } +} + +// pack_float emits the shortest IEEE 754 width that preserves the value, +// per RFC 8949 §4.2.2. NaN serialises as the canonical quiet NaN +// (0xf97e00), not the original payload. +@[direct_array_access] +pub fn (mut p Packer) pack_float(v f64) { + if math.is_nan(v) { + p.pack_float16_bits(half_qnan_bits) + return + } + if math.is_inf(v, 1) { + p.pack_float16_bits(half_pos_inf_bits) + return + } + if math.is_inf(v, -1) { + p.pack_float16_bits(half_neg_inf_bits) + return + } + // Try f32: lossless conversion? + f32_v := f32(v) + if f64(f32_v) == v { + bits16, ok := f32_to_half(f32_v) + if ok { + p.pack_float16_bits(bits16) + return + } + p.pack_float32(f32_v) + return + } + p.pack_float64(v) +} + +// -------------------------------------------------------------------- +// Value tree encoder +// -------------------------------------------------------------------- + +// pack_value emits an arbitrary `Value` tree, honouring the original +// float width hint. Map keys are sorted when `opts.canonical` is set. +// Returns an error if the tree is malformed (e.g. a `Tag` with no +// content) — silently emitting a placeholder would corrupt round-trips. +pub fn (mut p Packer) pack_value(v Value) ! { + match v { + IntNum { + if v.negative { + p.write_head(0x20, v.magnitude) + } else { + p.write_head(0x00, v.magnitude) + } + } + Bytes { + p.pack_bytes(v.data) + } + Text { + p.pack_text(v.value) + } + Array { + p.pack_array_header(u64(v.elements.len)) + for el in v.elements { + p.pack_value(el)! + } + } + Map { + p.pack_map_header(u64(v.pairs.len)) + if p.opts.canonical { + p.pack_map_canonical(v.pairs)! + } else { + for pair in v.pairs { + p.pack_value(pair.key)! + p.pack_value(pair.value)! + } + } + } + Tag { + if v.content_box.len == 0 { + return error('cbor: Tag(${v.number}) has no content — use new_tag() or set content_box') + } + p.pack_tag(v.number) + p.pack_value(v.content_box[0])! + } + Bool { + p.pack_bool(v.value) + } + Null { + p.pack_null() + } + Undefined { + p.pack_undefined() + } + FloatNum { + // RFC 8949 §4.2.1 deterministic encoding requires the shortest + // IEEE 754 form (§4.2.2) regardless of the original wire width. + // Drop the bits hint when canonical so re-encoded `Value`s + // match the rule, even if the producer copied a too-wide hint + // from a non-canonical source. + if p.opts.canonical { + p.pack_float(v.value) + } else { + match v.bits { + .half { + // NaN/±Inf bypass the lossless check (NaN != NaN + // breaks the f32 round-trip equality test). + if math.is_nan(v.value) { + p.pack_float16_bits(half_qnan_bits) + } else if math.is_inf(v.value, 1) { + p.pack_float16_bits(half_pos_inf_bits) + } else if math.is_inf(v.value, -1) { + p.pack_float16_bits(half_neg_inf_bits) + } else { + bits16, ok := f64_to_half(v.value) + if ok { + p.pack_float16_bits(bits16) + } else { + p.pack_float64(v.value) + } + } + } + .single { + p.pack_float32(f32(v.value)) + } + .double { + p.pack_float64(v.value) + } + .@none { + p.pack_float(v.value) + } + } + } + } + Simple { + p.pack_simple(v.value)! + } + } +} + +// pack_map_canonical sorts pairs by encoded-key bytes per RFC 8949 +// §4.2.1 (length-first lexicographic, "bytewise lexicographic of the +// deterministic encodings of the keys") before emitting them. +fn (mut p Packer) pack_map_canonical(pairs []MapPair) ! { + if pairs.len == 0 { + return + } + // Encode each key once, sort indices by the encoded key bytes, then emit. + // Sub-encoders inherit `validate_utf8` so a strict-encode caller still + // gets the guarantee on text-typed keys in canonical mode. + sub_opts := EncodeOpts{ + initial_cap: 16 + canonical: true + validate_utf8: p.opts.validate_utf8 + } + mut encoded_keys := [][]u8{cap: pairs.len} + for pair in pairs { + mut sub := new_packer(sub_opts) + sub.pack_value(pair.key)! + encoded_keys << sub.bytes().clone() + } + for i in sort_canonical_indices(encoded_keys) { + p.reserve(encoded_keys[i].len) + unsafe { p.buf.push_many(encoded_keys[i].data, encoded_keys[i].len) } + p.pack_value(pairs[i].value)! + } +} + +// compare_canonical_keys orders byte slices by length first, then +// bytewise; this matches RFC 8949 §4.2.1 "Core Deterministic Encoding". +@[direct_array_access] +fn compare_canonical_keys(a []u8, b []u8) int { + if a.len != b.len { + return if a.len < b.len { -1 } else { 1 } + } + for i in 0 .. a.len { + if a[i] != b[i] { + return if a[i] < b[i] { -1 } else { 1 } + } + } + return 0 +} + +// sort_canonical_indices returns indices into `keys` ordered by RFC +// 8949 §4.2.1 (length-first lexicographic on the encoded key bytes). +// Shared by the three canonical-emit paths (Value Map, generic $map, +// generic $struct) so the closure literal lives in one place. +fn sort_canonical_indices(keys [][]u8) []int { + mut idx := []int{len: keys.len, init: index} + idx.sort_with_compare(fn [keys] (a &int, b &int) int { + return compare_canonical_keys(keys[*a], keys[*b]) + }) + return idx +} + +// -------------------------------------------------------------------- +// Module-level convenience wrappers +// -------------------------------------------------------------------- + +// encode_value emits a `Value` tree to a fresh byte slice with default opts. +pub fn encode_value(v Value, opts EncodeOpts) ![]u8 { + mut p := new_packer(opts) + p.pack_value(v)! + return p.bytes().clone() +} diff --git a/vlib/encoding/cbor/errors.v b/vlib/encoding/cbor/errors.v new file mode 100644 index 000000000..608e549a7 --- /dev/null +++ b/vlib/encoding/cbor/errors.v @@ -0,0 +1,165 @@ +module cbor + +// Typed errors for CBOR decode failures. Pattern-match in callers: +// +// cbor.decode[User](bad) or { +// if err is cbor.UnexpectedEofError { ... } +// } + +// UnexpectedEofError fires when the decoder runs past the end of its input. +// `need` is i64 so it can represent the full CBOR length range (which is +// u64 on the wire); huge values are clamped to i64::max for reporting. +pub struct UnexpectedEofError { + Error +pub: + pos int // position at which the read began + need i64 // bytes the decoder was trying to read + remaining int // bytes actually available +} + +// msg formats an UnexpectedEofError for `IError.msg()`. +pub fn (e &UnexpectedEofError) msg() string { + if e.need == max_i64 { + return 'cbor: unexpected EOF at pos ${e.pos}: declared length exceeds available input (have ${e.remaining})' + } + return 'cbor: unexpected EOF at pos ${e.pos}: need ${e.need} bytes, have ${e.remaining}' +} + +// MalformedError fires when the byte stream violates RFC 8949 well-formedness. +pub struct MalformedError { + Error +pub: + pos int + reason string +} + +// msg formats a MalformedError for `IError.msg()`. +pub fn (e &MalformedError) msg() string { + return 'cbor: malformed at pos ${e.pos}: ${e.reason}' +} + +// TypeMismatchError fires when a typed read finds a different major type. +pub struct TypeMismatchError { + Error +pub: + pos int + expected string + got u8 // initial byte +} + +// msg formats a TypeMismatchError for `IError.msg()`. +pub fn (e &TypeMismatchError) msg() string { + return 'cbor: type mismatch at pos ${e.pos}: expected ${e.expected}, got initial byte 0x${e.got:02x}' +} + +// MaxDepthError fires when nested arrays/maps exceed the configured cap. +pub struct MaxDepthError { + Error +pub: + pos int + max_depth int +} + +// msg formats a MaxDepthError for `IError.msg()`. +pub fn (e &MaxDepthError) msg() string { + return 'cbor: max nesting depth ${e.max_depth} exceeded at pos ${e.pos}' +} + +// UnknownFieldError fires when a struct decoded with `deny_unknown_fields` +// encounters an unmapped key. +pub struct UnknownFieldError { + Error +pub: + pos int + name string +} + +// msg formats an UnknownFieldError for `IError.msg()`. +pub fn (e &UnknownFieldError) msg() string { + return 'cbor: unknown field "${e.name}" at pos ${e.pos}' +} + +// IntRangeError fires when a decoded integer doesn't fit the target type. +pub struct IntRangeError { + Error +pub: + pos int + target string + value string +} + +// msg formats an IntRangeError for `IError.msg()`. +pub fn (e &IntRangeError) msg() string { + return 'cbor: integer ${e.value} at pos ${e.pos} out of range for ${e.target}' +} + +// InvalidUtf8Error fires when a text-string payload isn't valid UTF-8 and +// the decoder is configured to validate strings. +pub struct InvalidUtf8Error { + Error +pub: + pos int +} + +// msg formats an InvalidUtf8Error for `IError.msg()`. +pub fn (e &InvalidUtf8Error) msg() string { + return 'cbor: invalid UTF-8 in text string at pos ${e.pos}' +} + +@[cold; inline] +fn eof_at(pos int) IError { + return UnexpectedEofError{ + pos: pos + need: 1 + remaining: 0 + } +} + +@[cold; inline] +fn eof_needing(pos int, need i64, remaining int) IError { + return UnexpectedEofError{ + pos: pos + need: need + remaining: remaining + } +} + +// eof_oversized reports an EOF caused by a length argument larger than +// the host can represent, clamped to i64::max so callers see a sensible +// number rather than a negative wrap-around. Used by string/bytes +// chunk readers where the wire length is u64. +@[cold; inline] +fn eof_oversized(pos int, want u64, remaining int) IError { + clamped := if want > u64(max_i64) { max_i64 } else { i64(want) } + return UnexpectedEofError{ + pos: pos + need: clamped + remaining: remaining + } +} + +@[cold; inline] +fn malformed(pos int, reason string) IError { + return MalformedError{ + pos: pos + reason: reason + } +} + +@[cold; inline] +fn type_mismatch(pos int, expected string, got u8) IError { + return TypeMismatchError{ + pos: pos + expected: expected + got: got + } +} + +@[cold; inline] +fn int_range(pos int, target string, value string) IError { + return IntRangeError{ + pos: pos + target: target + value: value + } +} diff --git a/vlib/encoding/cbor/generic.v b/vlib/encoding/cbor/generic.v new file mode 100644 index 000000000..7cb599186 --- /dev/null +++ b/vlib/encoding/cbor/generic.v @@ -0,0 +1,777 @@ +module cbor + +import math +import time + +// Generic comptime-driven encoder/decoder. The pack[T] / unpack[T] +// methods below dispatch on T at compile time, so each call site +// monomorphises into straight-line code with no runtime type tests. +// +// Supported targets: +// * bool, all signed/unsigned integer widths, f32, f64 +// * string (text), []u8 (byte string), enums (encoded as int) +// * `$array` (any V array) and `$map` (any K with a primitive scalar +// decoder — string, signed/unsigned ints, bool — plus any V). +// * `$struct` (encoded as a string-keyed map; honours +// `@[cbor: 'alt']`, `@[skip]`, `@[cbor: '-']`, optional fields) +// * `time.Time` — whole seconds use tag 1 (epoch seconds, integer); +// sub-second values use tag 0 (RFC 3339 string with nanosecond +// precision). Decode accepts tag 0 (RFC 3339 text) or tag 1 +// (integer or float). +// * `RawMessage`, `Value`, `Marshaler`/`Unmarshaler` implementers. + +// pack encodes `val` into the packer's buffer using compile-time dispatch. +@[inline] +pub fn (mut p Packer) pack[T](val T) ! { + $if T is RawMessage { + p.pack_raw(val)! + } $else $if T is Marshaler { + bytes := val.to_cbor() + if bytes.len == 0 { + return error('cbor: ${T.name}.to_cbor() returned empty bytes') + } + // Validate the user's output is exactly one well-formed CBOR + // item before splicing it into the parent stream. A malformed + // or truncated Marshaler would otherwise silently corrupt the + // surrounding fields (the next struct field would be parsed + // from inside the bad item's claimed payload). + mut probe := new_unpacker(bytes, DecodeOpts{}) + probe.skip_value() or { + return error('cbor: ${T.name}.to_cbor() returned malformed CBOR: ${err.msg()}') + } + if !probe.done() { + return error('cbor: ${T.name}.to_cbor() returned ${probe.remaining()} trailing byte(s) past one item') + } + p.reserve(bytes.len) + unsafe { p.buf.push_many(bytes.data, bytes.len) } + } $else $if T is Value { + p.pack_value(val)! + } $else $if T is time.Time { + // Whole-second values use tag 1 (epoch seconds) + integer — the + // most compact and canonical form (RFC 8949 §3.4.2). Sub-second + // values fall back to tag 0 (RFC 3339 string) with nanosecond + // precision: encoding the seconds.nanoseconds pair as a tag-1 + // float would lose ~µs of resolution past the year 2001 (f64 + // can't carry both a 10-digit unix epoch and 9 fractional digits). + if val.nanosecond == 0 { + p.pack_tag(tag_epoch) + p.pack_int(val.unix()) + } else { + p.pack_tag(tag_date_time) + p.pack_text(format_rfc3339_nano(val)) + } + } $else $if T is string { + if p.opts.validate_utf8 && !utf8_validate_slice(val.bytes(), 0, val.len) { + return error('cbor: validate_utf8 set, but string contains invalid UTF-8 (len=${val.len})') + } + p.pack_text(val) + } $else $if T is bool { + p.pack_bool(val) + } $else $if T is i8 { + p.pack_int(i64(val)) + } $else $if T is i16 { + p.pack_int(i64(val)) + } $else $if T is int { + p.pack_int(i64(val)) + } $else $if T is i32 { + p.pack_int(i64(val)) + } $else $if T is i64 { + p.pack_int(val) + } $else $if T is u8 { + p.pack_uint(u64(val)) + } $else $if T is u16 { + p.pack_uint(u64(val)) + } $else $if T is u32 { + p.pack_uint(u64(val)) + } $else $if T is u64 { + p.pack_uint(val) + } $else $if T is f32 { + p.pack_float(f64(val)) + } $else $if T is f64 { + p.pack_float(val) + } $else $if T is $enum { + p.pack_int(i64(val)) + } $else $if T is []u8 { + p.pack_bytes(val) + } $else $if T is $array { + p.pack_array_header(u64(val.len)) + for item in val { + p.pack(item)! + } + } $else $if T is $map { + p.pack_map_header(u64(val.len)) + if p.opts.canonical && val.len > 1 { + // Sub-encoders inherit `validate_utf8` so the strict-encode + // guarantee survives canonical mode. `self_describe` and + // `initial_cap` stay local — the wrapper belongs to the top-level + // stream only, and 16 B is enough for almost every key/value pair. + sub_opts := EncodeOpts{ + initial_cap: 16 + canonical: true + validate_utf8: p.opts.validate_utf8 + } + mut encoded_keys := [][]u8{cap: val.len} + mut encoded_vals := [][]u8{cap: val.len} + for k, item in val { + mut ksub := new_packer(sub_opts) + ksub.pack(k)! + encoded_keys << ksub.bytes().clone() + mut vsub := new_packer(sub_opts) + vsub.pack(item)! + encoded_vals << vsub.bytes().clone() + } + for i in sort_canonical_indices(encoded_keys) { + p.reserve(encoded_keys[i].len + encoded_vals[i].len) + unsafe { + p.buf.push_many(encoded_keys[i].data, encoded_keys[i].len) + p.buf.push_many(encoded_vals[i].data, encoded_vals[i].len) + } + } + } else { + for k, item in val { + p.pack(k)! + p.pack(item)! + } + } + } $else $if T is $struct { + mut strategy := '' + $for attr in T.attributes { + if attr.name == 'cbor_rename_all' { + strategy = attr.arg + } + } + mut field_count := 0 + $for field in T.fields { + if !cbor_field_skipped(field) { + field_count++ + } + } + p.pack_map_header(u64(field_count)) + if p.opts.canonical && field_count > 1 { + // RFC 8949 §4.2.1: deterministic encoding requires keys to + // be ordered by their encoded byte form, not by struct + // declaration. Encode each (key, value) pair to a sub-buffer, + // sort, then splice — same shape as the $map branch above. + // `validate_utf8` propagates so strict-encode callers don't + // silently lose the guarantee in canonical mode. + sub_opts := EncodeOpts{ + initial_cap: 16 + canonical: true + validate_utf8: p.opts.validate_utf8 + } + mut encoded_keys := [][]u8{cap: field_count} + mut encoded_vals := [][]u8{cap: field_count} + $for field in T.fields { + if !cbor_field_skipped(field) { + key := cbor_field_explicit_key(field) or { + if strategy != '' { cbor_rename(field.name, strategy) } else { field.name } + } + mut ksub := new_packer(sub_opts) + ksub.pack_text(key) + encoded_keys << ksub.bytes().clone() + mut vsub := new_packer(sub_opts) + $if field.typ is $option { + if val.$(field.name) == none { + vsub.pack_null() + } else { + vsub.pack(get_value_from_optional(val.$(field.name)))! + } + } $else { + vsub.pack(val.$(field.name))! + } + encoded_vals << vsub.bytes().clone() + } + } + for i in sort_canonical_indices(encoded_keys) { + p.reserve(encoded_keys[i].len + encoded_vals[i].len) + unsafe { + p.buf.push_many(encoded_keys[i].data, encoded_keys[i].len) + p.buf.push_many(encoded_vals[i].data, encoded_vals[i].len) + } + } + } else { + $for field in T.fields { + if !cbor_field_skipped(field) { + key := cbor_field_explicit_key(field) or { + if strategy != '' { cbor_rename(field.name, strategy) } else { field.name } + } + p.pack_text(key) + $if field.typ is $option { + if val.$(field.name) == none { + p.pack_null() + } else { + p.pack(get_value_from_optional(val.$(field.name)))! + } + } $else { + p.pack(val.$(field.name))! + } + } + } + } + } $else { + p.pack_null() + } +} + +// get_value_from_optional unwraps an Option known to be `Some`. +// Its signature exists solely so V's generic inferrer can pick up the +// inner T at the comptime call site. +fn get_value_from_optional[T](val ?T) T { + return val or { T{} } +} + +// unpack reads one CBOR value from the buffer and converts it to T. +@[inline] +pub fn (mut u Unpacker) unpack[T]() !T { + $if T is RawMessage { + return u.unpack_raw()! + } $else $if T is Unmarshaler { + start := u.pos + u.skip_value()! + mut v := T{} + v.from_cbor(u.data[start..u.pos])! + return v + } $else $if T is Value { + return u.unpack_value()! + } $else $if T is time.Time { + return u.unpack_time()! + } $else $if T is string { + return u.unpack_text()! + } $else $if T is bool { + // Accept null as false-equivalent? No — strict by default. + return u.unpack_bool()! + } $else $if T is i8 { + v := u.unpack_int()! + if v < -128 || v > 127 { + return int_range(u.pos, 'i8', v.str()) + } + return i8(v) + } $else $if T is i16 { + v := u.unpack_int()! + if v < -32_768 || v > 32_767 { + return int_range(u.pos, 'i16', v.str()) + } + return i16(v) + } $else $if T is int { + v := u.unpack_int()! + if v < -2_147_483_648 || v > 2_147_483_647 { + return int_range(u.pos, 'int', v.str()) + } + return int(v) + } $else $if T is i32 { + v := u.unpack_int()! + if v < -2_147_483_648 || v > 2_147_483_647 { + return int_range(u.pos, 'i32', v.str()) + } + return i32(v) + } $else $if T is i64 { + return u.unpack_int()! + } $else $if T is u8 { + v := u.unpack_int()! + if v < 0 || v > 255 { + return int_range(u.pos, 'u8', v.str()) + } + return u8(v) + } $else $if T is u16 { + v := u.unpack_int()! + if v < 0 || v > 65_535 { + return int_range(u.pos, 'u16', v.str()) + } + return u16(v) + } $else $if T is u32 { + v := u.unpack_int()! + if v < 0 || v > 4_294_967_295 { + return int_range(u.pos, 'u32', v.str()) + } + return u32(v) + } $else $if T is u64 { + neg, mag := u.unpack_int_full()! + if neg { + return int_range(u.pos, 'u64', '-1 - ${mag}') + } + return mag + } $else $if T is f32 { + return f32(u.unpack_float()!) + } $else $if T is f64 { + return u.unpack_float()! + } $else $if T is $enum { + v := int(u.unpack_int()!) + return unsafe { T(v) } + } $else $if T is []u8 { + return u.unpack_bytes()! + } $else $if T is $array { + mut out := T{} + u.unpack_array_into(mut out)! + return out + } $else $if T is $map { + mut out := T{} + read_pairs_into_helper(mut u, mut out)! + return out + } $else $if T is $struct { + mut result := T{} + u.unpack_struct_into(mut result)! + return result + } $else { + return error('cbor: unsupported target type') + } +} + +fn (mut u Unpacker) unpack_array_into[E](mut out []E) ! { + hdr := u.unpack_array_header()! + if hdr < 0 { + // Indefinite. + for { + if u.consume_break() { + break + } + out << u.unpack[E]()! + } + return + } + for _ in 0 .. hdr { + out << u.unpack[E]()! + } +} + +// read_pairs_into_helper is a standalone (non-method) generic function; +// V's generic-method dispatch can drop the second type parameter when +// invoked from a comptime $map branch, while the standalone form +// monomorphises correctly. +fn read_pairs_into_helper[K, V](mut u Unpacker, mut out map[K]V) ! { + hdr := u.unpack_map_header()! + if hdr < 0 { + for { + if u.consume_break() { + break + } + key := u.unpack[K]()! + val := u.unpack[V]()! + if u.opts.deny_duplicate_keys && key in out { + return malformed(u.pos, 'duplicate map key') + } + out[key] = val + } + return + } + for _ in 0 .. hdr { + key := u.unpack[K]()! + val := u.unpack[V]()! + if u.opts.deny_duplicate_keys && key in out { + return malformed(u.pos, 'duplicate map key') + } + out[key] = val + } +} + +fn (mut u Unpacker) unpack_struct_into[T](mut result T) ! { + mut strategy := '' + $for attr in T.attributes { + if attr.name == 'cbor_rename_all' { + strategy = attr.arg + } + } + hdr := u.unpack_map_header()! + indef := hdr < 0 + mut remaining := if indef { i64(-1) } else { hdr } + // Tracks keys already seen so deny_duplicate_keys can fire on struct + // decode too (the typed-map and Value paths track separately). Built + // only when the option is set, so the common case stays allocation-free. + // O(1) lookup via V map keeps decode linear even on adversarial inputs + // with thousands of distinct keys. + mut seen_keys := map[string]bool{} + for { + if indef { + if u.co