v / vlib / encoding / cbor / README.md
218 lines · 175 sloc · 6.1 KB · 468855eef1db0ff73c62be2d1bf176ffa0e1478e
Raw

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:

Usage

encode[T] / decode[T]

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

@[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):

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:

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:

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):

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.

v test vlib/encoding/cbor/tests/