Packages

macula

4.2.3
7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula native macula_cbor_nif src lib.rs
Raw

native/macula_cbor_nif/src/lib.rs

//! CBOR pack/unpack NIF for Macula mesh wire protocol.
//!
//! Replaces the pure-Erlang `msgpack` hex package as the per-frame
//! serialization layer. CBOR (RFC 8949) is the long-term wire format
//! for Macula because:
//!
//! - It composes with UCAN, DID, COSE, and IPLD which the platform
//! already uses for identity and auth.
//! - It has deterministic encoding rules (RFC 8949 §4.2.1) which
//! matter for signed payloads.
//! - It is an IETF standard with stable governance.
//!
//! Wire-format conversion (Erlang term <-> CBOR Value):
//!
//! Atom -> Text string (atoms are stringly-typed in BEAM
//! wire protocols; we lose atom-vs-binary
//! distinction across the wire, same as
//! msgpack did)
//! Binary -> Byte string (preserved as bytes; readers expecting
//! a binary still get a binary)
//! Integer -> Integer (CBOR uint or negative-int as appropriate)
//! Float -> Float
//! Boolean (atom) -> Bool (true/false)
//! Atom 'undefined' -> Null
//! Atom 'nil' -> Null
//! List -> Array
//! Tuple -> Array (BEAM tuples lose their tuple-ness;
//! use lists when arity matters)
//! Map -> Map (keys can be any encodable term)
//!
//! Decoding is symmetric except text strings always come back as binaries
//! (the encoder cannot reliably round-trip atoms — same constraint as msgpack).
use ciborium::value::Value;
use rustler::types::atom::Atom as ErlAtom;
use rustler::types::map::MapIterator;
use rustler::{Binary, Encoder, Env, NifResult, OwnedBinary, Term, TermType};
mod atoms {
rustler::atoms! {
ok,
error,
// Common BEAM atoms we round-trip with semantic meaning.
nil,
undefined,
true_ = "true",
false_ = "false",
}
}
#[rustler::nif]
fn nif_pack<'a>(env: Env<'a>, term: Term<'a>) -> NifResult<Binary<'a>> {
let value = term_to_value(term)?;
let mut buf: Vec<u8> = Vec::with_capacity(64);
ciborium::ser::into_writer(&value, &mut buf)
.map_err(|e| rustler::Error::Term(Box::new(format!("cbor encode: {}", e))))?;
let mut out = OwnedBinary::new(buf.len()).ok_or_else(|| {
rustler::Error::Term(Box::new("cbor: failed to allocate output binary"))
})?;
out.as_mut_slice().copy_from_slice(&buf);
Ok(out.release(env))
}
#[rustler::nif]
fn nif_unpack<'a>(env: Env<'a>, bytes: Binary<'a>) -> NifResult<(ErlAtom, Term<'a>)> {
let value: Value = match ciborium::de::from_reader(bytes.as_slice()) {
Ok(v) => v,
Err(e) => {
let err = format!("{}", e);
let err_term = err.as_str().encode(env);
return Ok((atoms::error(), err_term));
}
};
let term = value_to_term(env, &value)?;
Ok((atoms::ok(), term))
}
// ---------- Encode: Erlang Term -> ciborium Value ----------
fn term_to_value(term: Term) -> Result<Value, rustler::Error> {
match term.get_type() {
TermType::Atom => atom_to_value(term),
TermType::Binary => {
let bin: Binary = term.decode()?;
Ok(Value::Bytes(bin.as_slice().to_vec()))
}
TermType::Integer => {
// Try i128 first (handles both small and very large ints up to 64-bit safely).
if let Ok(i) = term.decode::<i64>() {
Ok(Value::Integer(i.into()))
} else if let Ok(u) = term.decode::<u64>() {
Ok(Value::Integer(u.into()))
} else {
Err(rustler::Error::Term(Box::new(
"cbor: integer outside i64/u64 range not supported",
)))
}
}
TermType::Float => {
let f: f64 = term.decode()?;
Ok(Value::Float(f))
}
TermType::List => {
let list: Vec<Term> = term.decode()?;
let mut arr = Vec::with_capacity(list.len());
for item in list {
arr.push(term_to_value(item)?);
}
Ok(Value::Array(arr))
}
TermType::Tuple => {
// Tuples lose tuple-ness — encode as array. Document at module level.
let tup = rustler::types::tuple::get_tuple(term)?;
let mut arr = Vec::with_capacity(tup.len());
for item in tup {
arr.push(term_to_value(item)?);
}
Ok(Value::Array(arr))
}
TermType::Map => {
let mut entries: Vec<(Value, Value)> = Vec::new();
let iter = MapIterator::new(term).ok_or_else(|| {
rustler::Error::Term(Box::new("cbor: failed to iterate map"))
})?;
for (k, v) in iter {
entries.push((term_to_value(k)?, term_to_value(v)?));
}
Ok(Value::Map(entries))
}
_ => Err(rustler::Error::Term(Box::new(
"cbor: unsupported term type (pid/ref/port/fun)",
))),
}
}
fn atom_to_value(term: Term) -> Result<Value, rustler::Error> {
if term == atoms::true_().to_term(term.get_env()) {
return Ok(Value::Bool(true));
}
if term == atoms::false_().to_term(term.get_env()) {
return Ok(Value::Bool(false));
}
if term == atoms::nil().to_term(term.get_env())
|| term == atoms::undefined().to_term(term.get_env())
{
return Ok(Value::Null);
}
// Other atoms: encode as text string. Lossy on round-trip (decoder
// returns binary), matching msgpack-era semantics.
let s: String = term.atom_to_string()?;
Ok(Value::Text(s))
}
// ---------- Decode: ciborium Value -> Erlang Term ----------
fn value_to_term<'a>(env: Env<'a>, value: &Value) -> NifResult<Term<'a>> {
match value {
Value::Null => Ok(atoms::nil().to_term(env)),
Value::Bool(b) => Ok(b.encode(env)),
Value::Integer(i) => {
// ciborium::value::Integer: try to fit into i64, then u64.
if let Ok(n) = i64::try_from(*i) {
Ok(n.encode(env))
} else if let Ok(n) = u64::try_from(*i) {
Ok(n.encode(env))
} else {
Err(rustler::Error::Term(Box::new(
"cbor: integer too large for i64/u64 decode",
)))
}
}
Value::Float(f) => Ok(f.encode(env)),
Value::Text(s) => {
// Text strings always come back as binaries (lossy round-trip
// on atoms — same as msgpack-era behavior).
let bytes = s.as_bytes();
let mut out = OwnedBinary::new(bytes.len()).ok_or_else(|| {
rustler::Error::Term(Box::new("cbor: failed to allocate text->binary"))
})?;
out.as_mut_slice().copy_from_slice(bytes);
Ok(out.release(env).to_term(env))
}
Value::Bytes(b) => {
let mut out = OwnedBinary::new(b.len()).ok_or_else(|| {
rustler::Error::Term(Box::new("cbor: failed to allocate bytes->binary"))
})?;
out.as_mut_slice().copy_from_slice(b);
Ok(out.release(env).to_term(env))
}
Value::Array(arr) => {
let mut items = Vec::with_capacity(arr.len());
for v in arr {
items.push(value_to_term(env, v)?);
}
Ok(items.encode(env))
}
Value::Map(entries) => {
let mut keys: Vec<Term> = Vec::with_capacity(entries.len());
let mut vals: Vec<Term> = Vec::with_capacity(entries.len());
for (k, v) in entries {
keys.push(value_to_term(env, k)?);
vals.push(value_to_term(env, v)?);
}
Term::map_from_arrays(env, &keys, &vals)
.map_err(|_| rustler::Error::Term(Box::new("cbor: map_from_arrays failed")))
}
Value::Tag(_, inner) => {
// Tags are CBOR's typed-data extension. We don't surface them
// as tuples; pass through the inner value. If/when callers need
// tag inspection, expand this branch.
value_to_term(env, inner)
}
_ => Err(rustler::Error::Term(Box::new(
"cbor: unsupported value variant",
))),
}
}
rustler::init!("macula_cbor_nif");