sacp-cbor
sacp-cbor is a strict, deterministic CBOR implementation of the SACP-CBOR/1 profile from the
Synext Agent Control Protocol (SACP).
It is designed for hot-path validation (WebSocket frames, API request bodies) with:
- allocation-free validation on success (
validate_canonical) - allocation-free queries on validated bytes (borrowed views, no decoding)
- deterministic map ordering enforcement (canonical CBOR key ordering, by encoded key bytes)
- strict canonical integer/length encoding checks (shortest form)
- strict numeric rules (safe integers, bignums via tags 2/3, float64-only, canonical NaN, forbid
-0.0) - strict tag rules (only bignum tags 2 and 3)
no_stdsupport (with optionalallocfor owned types)
This crate intentionally keeps the core small and uncompromising. If bytes validate under SACP-CBOR/1, they are already canonical; therefore, for opaque payloads, semantic equality reduces to byte equality.
Status
- Version:
0.3.0 - License: MIT
- MSRV: Rust
1.75
Features
| Feature | Default | Meaning |
|---|---|---|
std |
yes | Implements std::error::Error for CborError. |
alloc |
yes | Enables owned AST types (CborValue, CborMap, CanonicalCbor) and canonical encoding. |
sha2 |
yes | Enables SHA-256 helpers for canonical CBOR bytes (sha256()). |
serde |
no | Enables serde-based conversions to/from canonical CBOR (to_vec, from_slice). |
Note: serde currently requires std + alloc.
no_std usage
- Validation-only (no allocation): disable default features:
= { = "0.3", = false }
no_std+alloc(owned values + encoding): enablealloc:
= { = "0.3", = false, = ["alloc"] }
no_std+alloc+sha2:
= { = "0.3", = false, = ["alloc", "sha2"] }
Note: alloc requires an allocator provided by your environment.
Quick start
Validate SACP-CBOR/1 bytes (hot path)
use ;
Decode into an owned AST (requires alloc)
use ;
let bytes = ; // {"a":1}
let v = decode_value?;
assert_eq!;
# Ok::
Build an owned AST with cbor! (requires alloc)
use cbor;
let user_key = "dynamic";
let v = cbor!?;
assert_eq!;
# Ok::
Query canonical bytes without decoding
use ;
// { "user": { "id": 42, "active": true } }
let bytes = ;
let canon = validate_canonical?;
let path = ;
let v = canon.at?.unwrap;
assert_eq!;
# Ok::
use ;
// { "a": 1, "b": 2, "c": 3 }
let bytes = ;
let canon = validate_canonical?;
let map = canon.root.map?;
let out = map.get_many_sorted?;
assert_eq!;
# Ok::
Serde encode/decode (requires serde + alloc)
use ;
use ;
let msg = Msg ;
let bytes = to_vec?;
let decoded: Msg = from_slice?;
assert_eq!;
# Ok::
Hash canonical bytes (requires sha2)
use ;
let bytes = ;
let canon = validate_canonical?;
let digest = canon.sha256;
API overview
validate_canonical(bytes, limits) -> CanonicalCborRefvalidate(bytes, limits) -> ()decode_value(bytes, limits) -> CborValue(featurealloc)CborValue::encode_canonical() -> Vec<u8>(featurealloc)cbor!(...) -> Result<CborValue, CborError>(featurealloc)cbor_equal(a, b) -> bool(featurealloc)CanonicalCborRef::root() -> CborValueRefCanonicalCborRef::at(path) -> Option<CborValueRef>CborValueRef::{kind, map, array, get_key, get_index, at}MapRef::{get, get_many_sorted, iter}MapRef::get_many(keys) -> Vec<Option<CborValueRef>>(featurealloc)to_vec<T: Serialize>(&T) -> Vec<u8>(featureserde+alloc)from_slice<T: DeserializeOwned>(bytes, limits) -> T(featureserde+alloc)
Fuzzing
This repository includes cargo-fuzz targets under ./fuzz.
Prerequisites:
Run:
The fuzz targets:
- validate arbitrary bytes under strict limits
- when validation succeeds, decode and re-encode and assert roundtrip identity
Benchmarks
Criterion benchmarks are under ./benches.
Run:
Coverage (llvm-cov + grcov)
Prerequisites:
Run the coverage script:
The HTML report is generated at ./coverage/index.html.
License
MIT. See LICENSE.