Cbor-core: A deterministic CBOR::Core encoder/decoder

cbor-core is a Rust implementation of the CBOR::Core profile, as specified in draft-rundgren-cbor-core-25.

Its central type is an owned Value that can be built, inspected, modified, encoded to bytes, and decoded back. Encoding is always deterministic (shortest integer and float forms, sorted map keys), and the decoder rejects non-canonical input. NaN payloads, including signaling NaNs, are preserved bit-for-bit through round-trips.

The Value API is shaped around CBOR itself, so all CBOR data items, including tagged values, simple values and arbitrary map keys, stay directly reachable. For the familiar serialize/deserialize pattern the optional serde feature adds Serialize/Deserialize for Value together with to_value and from_value.

Notable features

  • Strict adherence to CBOR::Core draft. All test vectors from Appendix A pass, including rejection of non-deterministic encodings.
  • Full support for CBOR diagnostic notation (Section 2.3.6) in both directions:
    Debug prints it, and FromStr parses it.
  • No mandatory dependencies. The core crate is self-contained.
    Optional features add integration with serde, chrono, time, half (for f16), num-bigint, crypto-bigint, and rug.
  • Map and array access by &str and &[u8] keys without allocating a temporary on the heap for the comparison, via a ValueKey type used by get, get_mut, remove, contains, and Index/IndexMut.
  • Decoder hardening: bounded recursion depth, capped declared lengths, and a per-call allocation budget, so malformed input cannot exhaust memory or blow the stack.

Quick taste

Diagnostic notation is often the shortest way to write a literal CBOR
value:

use cbor_core::Value;

let cert: Value = r#"{
    "iss": "https://issuer.example",
    "sub": "user-42",
    "iat": 1700000000,
    "cnf": {
        "kty": "OKP",
        "crv": "Ed25519",
        "x":   h'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
    },
    "scope": ["read", "write"]
}"#.parse().unwrap();

assert_eq!(cert["cnf"]["crv"].as_str().unwrap(), "Ed25519");

let bytes = cert.encode();
assert_eq!(Value::decode(&bytes).unwrap(), cert);

Links

Status and origin

The API is not yet stable. Names and signatures may still change before a 1.0, but the encoding layer and test-vector coverage are settled.

The crate grew out of a side discussion in this users.rust-lang.org thread.

Feedback, bug reports, and PRs are welcome.

License: MIT.

0.7.0 release available:

Summary of changes

  • CBOR sequences are now supported on both sides, with iterator-style decoding from byte slices or readers and a streaming writer for byte, hex, and diagnostic output.
  • Decoder configuration moved into a dedicated options type that bundles the input format, recursion limit, collection length limit, and OOM-mitigation budget.
  • Diagnostic notation joined the binary and hex formats as a first-class decoder input, with the same hardening limits and proper error variants for nesting and trailing data.
  • The constructor surface grew a full set of explicit and const builders for scalar values.
  • Non-finite floats can now be constructed from and inspected as a 53-bit payload, so signaling NaNs and other non-finite bit patterns are addressable directly and round-trip unchanged.
  • Composite map keys (arrays and maps used as keys) now look up without a preparatory allocation.
  • An optional jiff feature joins the existing chrono and time integrations.
  • A small number of signature cleanups on the write and decode entry points; see the changelog for the exact breaking items.
  • First set of runnable examples ships with the crate, covering encoding and decoding of values and sequences, a pair of cbor2diag / diag2cbor conversion utilities, and short walkthroughs of the macros and const constructors.

0.9.0 is out

The headline feature is zero-copy binary decoding for text strings and byte strings.

Read the full release notes: NEWS