Skip to content

Latest commit

 

History

History
193 lines (151 loc) · 5.18 KB

File metadata and controls

193 lines (151 loc) · 5.18 KB

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

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

struct Login {
	user_name string @[cbor: 'u']            // emit/read key "u"
	password  string @[skip]                  // never serialise
	remember  bool   @[cbor_rename: 'kebab-case'] // becomes "remember"
}

The @[cbor_rename: '...'] attribute on a struct (not a field) applies a global rename strategy — supported strategies: snake_case, camelCase, PascalCase, kebab-case, SCREAMING_SNAKE_CASE.

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 {
	r u8
	g u8
	b u8
}

pub fn (c Color) marshal_cbor(mut p cbor.Packer) ! {
	p.pack_array_header(3)!
	p.pack_uint(c.r)!
	p.pack_uint(c.g)!
	p.pack_uint(c.b)!
}

pub fn (mut c Color) unmarshal_cbor(mut u cbor.Unpacker) ! {
	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()!)
}

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 are serialised with tag 0 (RFC 3339 string) on encode and accept either tag 0 or tag 1 (epoch seconds) on decode. 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/