Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -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/
|
||||
```
|
||||
Reference in new issue
Block a user