Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions examples/cbor.v
Original file line number Diff line number Diff line change
@@ -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}')
}
193 changes: 193 additions & 0 deletions vlib/encoding/cbor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
## 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]

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

```v

Check failure on line 84 in vlib/encoding/cbor/README.md

View workflow job for this annotation

GitHub Actions / check-markdown

example is not formatted

Check failure on line 84 in vlib/encoding/cbor/README.md

View workflow job for this annotation

GitHub Actions / check-markdown

example failed to compile
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 ignore
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):

```v ignore
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.

```bash
v test vlib/encoding/cbor/tests/
```
37 changes: 37 additions & 0 deletions vlib/encoding/cbor/cbor.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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, 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.
pub fn decode[T](data []u8, opts DecodeOpts) !T {
mut u := new_unpacker(data, opts)
return u.unpack[T]()!
}
Loading
Loading