atomvm-cbor

Public Release GateLine and branch coverage gate 95%MIT LicenseErlang OTP 25+AtomVM 0.6.6Buy Me a Coffee

A compact RFC 8949 CBOR encoder and decoder for AtomVM and constrained Erlang runtimes. It is pure Erlang, has no runtime dependencies, NIFs, or ports, and preserves the public {text, Binary} and {map, Pairs} representations.

Version 0.3.0 adds bounded descriptor traversal, selective extraction, sequence encoding/folding, exact-size encoding, and complete-input validation. Version 0.2.0 was the first Hex.pm release; public Git tag v0.1.1 was never published to Hex.

Current release

Representative attached-device benchmark changes from 0.2.0 to 0.3.0. Negative is faster; positive is slower. Chart labels are percentages rounded to two decimal places; exact timings follow in the benchmark table.

---
config:
xyChart:
height: 360
showDataLabel: true
showDataLabelOutsideBar: true
themeVariables:
xyChart:
plotColorPalette: "#5C2D91"
---
xychart-beta
title "ESP32-S3 N16R8 at 240 MHz"
x-axis ["encode/1", "decode/1", "partial_decode/1"]
y-axis "Timing change (%)" -0.24 --> 0.24
bar [-0.06, -0.19, 0.00]
---
config:
xyChart:
height: 360
showDataLabel: true
showDataLabelOutsideBar: true
themeVariables:
xyChart:
plotColorPalette: "#65AE00"
---
xychart-beta
title "WaveShare N32R16V at 240 MHz"
x-axis ["encode/1", "decode/1", "partial_decode/1"]
y-axis "Timing change (%)" -0.13 --> 0.13
bar [0.02, 0.11, 0.03]
---
config:
xyChart:
height: 360
showDataLabel: true
showDataLabelOutsideBar: true
themeVariables:
xyChart:
plotColorPalette: "#2F80ED"
---
xychart-beta
title "RP2040 E462… at 133 MHz"
x-axis ["encode/1", "decode/1", "partial_decode/1"]
y-axis "Timing change (%)" -1.12 --> 1.12
bar [-0.69, -0.25, 0.93]

Installation

Rebar3 projects use the OTP application name avm_cbor and Hex package name atomvm_cbor:

{deps, [
{avm_cbor, "~> 0.3.0", {pkg, atomvm_cbor}}
]}.

Mix projects use:

{:avm_cbor, "~> 0.3.0", hex: :atomvm_cbor}

Capabilities

Attached-device benchmarks

The exact public 0.2.0 and 0.3.0 tags were measured on the same three physical devices with AtomVM 0.6.6, identical firmware and harness conditions, and five alternating paired captures. Both ESP32-S3 CPUs ran at their supported 240 MHz upper limit; RP2040 ran at its supported 133 MHz upper limit. Lower timings are better.

BoardExact identityCPU
ESP32-S3 N16R8QFN56 rev 0.2, MAC 1c:db:d4:5b:f5:d0, native USB identifier 1C:DB:D4:5B:F5:D0240 MHz
WaveShare N32R16VESP32-S3-DEV-KIT-N32R16V, MAC 90:e5:b1:d8:48:b0, CH340 UART serial 5B61092782240 MHz
RP2040 E462…RP2040 B2, BOOTSEL serial E0C9125B0D9B, flash ID E46254C5C32D122C133 MHz
FunctionESP32-S3 N16R8 0.2.0 µs0.3.0 µsChangeWaveShare N32R16V 0.2.0 µs0.3.0 µsChangeRP2040 E462… 0.2.0 µs0.3.0 µsChange
encode/18849.468844.020.06% faster3507.303508.000.02% slower4824.604791.260.69% faster
decode/110063.5210044.020.19% faster3931.223935.360.11% slower6082.166067.220.25% faster
partial_decode/115575.3815575.920.00% slower6325.486327.620.03% slower9261.069346.800.93% slower

The full report publishes all 14 comparable workloads and all nine 0.3.0-only workloads for every board, exact host results, measured accessor regressions, device and firmware identities, three-device 40-round soak evidence, evidence checksums, and reproduction details. See the 0.3.0 validation report and the historical 0.2.0 report.

Public term representation

CBOR valueErlang value
unsigned or negative integerinteger
byte stringbinary
text string{text, Binary}
arraylist
map{map, [{Key, Value}, ...]}
semantic tag{tag, Number, Value}
boolean/null/undefinedtrue, false, null, undefined
simple value{simple, Number}
floatfloat

Core API

avm_cbor:decode(Binary).
avm_cbor:decode(Binary, Options).
avm_cbor:decode_all(Binary, Options).
avm_cbor:decode_sequence(Binary, Options).
avm_cbor:sequence_fold(Binary, Fun, Acc).
avm_cbor:encode(Value, Options).
avm_cbor:encode_with_size(Value, Options).
avm_cbor:encode_sequence(Values, Options).
avm_cbor:validate_all(Binary, Options).

decode/1,2 consumes one value and returns {ok, Value, Rest}. decode_all/1,2 requires a complete input. decode_sequence/1,2 returns all complete sequence items and retains a truncated final item as Rest.

encode_with_size/1,2 returns {ok, Binary, Size} from the real encode operation. encode_sequence/1,2 emits concatenated RFC 8742 items while enforcing the configured item and total-byte limits. sequence_fold/3 consumes complete sequence items with {cont, NewAcc} or {halt, Result} callbacks. validate_all/1,2 validates exactly one complete item and rejects trailing bytes without constructing its nested Erlang value.

Pull-based continuation decode

{ok, State0} = avm_cbor:decode_start(Binary, Options),
case avm_cbor:decode_continue(State0, Budget) of
{done, Value, Rest} -> use(Value, Rest);
{more, State1} -> schedule_next(State1);
{error, Reason} -> reject(Reason)
end.

Budget is a positive bound on explicit parser transitions. The returned state is immutable and opaque. The decoder never sleeps or yields; event-loop pacing belongs to the caller. This is not a streaming-input API: Binary must already exist when decode_start/2 is called.

Partial and deferred decode

partial_decode/1,2 validates and measures one value without eagerly constructing nested Erlang terms. It returns an opaque descriptor; callers must use the public accessors rather than matching its internal shape.

{ok, Descriptor, Rest} = avm_cbor:partial_decode(Binary),
Type = avm_cbor:partial_type(Descriptor),
Length = avm_cbor:partial_length(Descriptor),
{ok, Value} = avm_cbor:partial_deep_decode(Descriptor).

Available accessors are partial_value_bytes/1, partial_deep_decode/1, partial_skip/1, partial_type/1, partial_count/1, partial_tag/1, partial_size/1, partial_offset/1, partial_length/1, and partial_contents/1.

Maps and arrays can be traversed without deep-decoding unselected values:

{ok, MapDescriptor, <<>>} = avm_cbor:partial_decode(Binary),
{ok, ValueDescriptor} = avm_cbor:partial_map_find(MapDescriptor, Key),
{ok, Value} = avm_cbor:partial_deep_decode(ValueDescriptor).

partial_map_fold/3 and partial_array_fold/3 support {cont, NewAcc} and {halt, Result} callbacks. partial_select/2 finds a small requested key set in one map pass, partial_map_find/2 returns the first matching value descriptor, and partial_array_nth/2 uses a zero-based index.

Options and limits

OptionDefaultPartial defaultPurpose
max_depth128128nesting depth
max_items409664global node/work budget
max_bytes10485761048576input/output bytes
max_string_bytes6553665536one string or chunk
max_total_string_bytes10485761048576cumulative decoded string bytes
max_string_sizenot used8192partial byte/text content
allow_floatstruetruefloat support
allow_simpletruetruesimple values
allow_tagstruetruesemantic tags
allow_indefinitetruetrueindefinite-length values
preferredfalsefalsepreferred serialization checks/output
deterministicfalsefalsedeterministic encoding and strict decode validation

Options are normalized once into fixed internal state. Recursive paths do not repeatedly scan or rebuild option proplists.

deterministic=true rejects indefinite-length input, non-preferred width choices, duplicate map keys, and map keys not ordered by their original encoded bytes. ble_options/0 provides a stricter device-oriented profile.

Treat options as trusted application policy. Do not allow a network client to raise them. Enforce the same or a smaller byte ceiling at transport ingress before buffering the complete request, use a lower fixed profile on targets that cannot allocate the defaults, and require empty trailing Rest for protocols that permit exactly one item.

Large definite containers made entirely of preferred one-byte unsigned values use a bounded fixed-cost path. It is independently capped at 4095 direct nodes, so caller-raised limits cannot turn it into an unbounded speculative allocation. Mixed, malformed, deterministic, nested, multi-byte, or larger containers use the general bounded decoder and retain the same errors.

Documentation and validation

Run the public validation suite:

scripts/release-check.sh 0.3.0

Run the reproducible host benchmark:

scripts/bench.sh

Benchmark values depend on the host, OTP version, and architecture. Published comparisons are measured context, not guarantees for other devices.

License

MIT. See LICENSE.