vauban-claim 0.1.0

Vauban Claim Algebra — reference implementation of draft-vauban-claim-algebra-00 (post-quantum claim sextuplet + 5 composition operators, canonical CBOR/JSON codec).
Documentation
//! Canonical CBOR (RFC 8949 §4.2.1) and JSON encoding helpers.
//!
//! Canonicalisation strategy:
//! 1. Serialise the value via `ciborium` to obtain a `Value` tree.
//! 2. Recursively sort every map by the deterministic encoding of its keys.
//! 3. Reject any float (CBOR major types 7/25/26/27) — forbidden by the
//!    Vauban grammar.
//! 4. Re-encode with `ciborium`.

use alloc::format;
use alloc::string::ToString;
use alloc::vec::Vec;
use ciborium::value::Value;
use serde::{de::DeserializeOwned, Serialize};

use crate::error::EncodingError;

/// Encode any serde-serialisable value as canonical CBOR (RFC 8949 §4.2.1).
pub fn to_cbor_canonical<T: Serialize>(value: &T) -> Result<Vec<u8>, EncodingError> {
    let mut intermediate = Vec::new();
    ciborium::ser::into_writer(value, &mut intermediate)
        .map_err(|e| EncodingError::CborSer(e.to_string()))?;
    let parsed: Value = ciborium::de::from_reader(intermediate.as_slice())
        .map_err(|e| EncodingError::CborDe(e.to_string()))?;
    let canonical = canonicalise(parsed)?;
    let mut out = Vec::new();
    ciborium::ser::into_writer(&canonical, &mut out)
        .map_err(|e| EncodingError::CborSer(e.to_string()))?;
    Ok(out)
}

/// Decode canonical CBOR bytes into a typed value, rejecting any float item.
pub fn from_cbor<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, EncodingError> {
    let parsed: Value = ciborium::de::from_reader(bytes)
        .map_err(|e| EncodingError::CborDe(e.to_string()))?;
    reject_floats(&parsed)?;
    let mut buf = Vec::new();
    ciborium::ser::into_writer(&parsed, &mut buf)
        .map_err(|e| EncodingError::CborSer(e.to_string()))?;
    ciborium::de::from_reader::<T, _>(buf.as_slice())
        .map_err(|e| EncodingError::CborDe(e.to_string()))
}

/// Encode as JSON (UTF-8). Convenience wrapper around `serde_json::to_vec`.
pub fn to_json<T: Serialize>(value: &T) -> Result<Vec<u8>, EncodingError> {
    serde_json::to_vec(value).map_err(EncodingError::JsonSer)
}

/// Decode UTF-8 JSON into a typed value.
pub fn from_json<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, EncodingError> {
    serde_json::from_slice(bytes).map_err(EncodingError::JsonSer)
}

/// Recursively canonicalise a CBOR `Value` tree. Returns an error if any
/// float is encountered.
fn canonicalise(value: Value) -> Result<Value, EncodingError> {
    match value {
        Value::Float(_) => Err(EncodingError::FloatForbidden),
        Value::Array(items) => {
            let mut out = Vec::with_capacity(items.len());
            for item in items {
                out.push(canonicalise(item)?);
            }
            Ok(Value::Array(out))
        }
        Value::Map(pairs) => {
            let mut canonicalised = Vec::with_capacity(pairs.len());
            for (k, v) in pairs {
                let k = canonicalise(k)?;
                let v = canonicalise(v)?;
                let mut k_bytes = Vec::new();
                ciborium::ser::into_writer(&k, &mut k_bytes)
                    .map_err(|e| EncodingError::CborSer(e.to_string()))?;
                canonicalised.push((k_bytes, k, v));
            }
            // Sort by encoded key bytes, length-then-lexicographic per
            // RFC 8949 §4.2.1 deterministic encoding.
            canonicalised.sort_by(|a, b| match a.0.len().cmp(&b.0.len()) {
                core::cmp::Ordering::Equal => a.0.cmp(&b.0),
                other => other,
            });
            // Detect duplicate keys (illegal in canonical CBOR).
            for w in canonicalised.windows(2) {
                if w[0].0 == w[1].0 {
                    return Err(EncodingError::CborSer(format!(
                        "duplicate map key in canonical encoding: {} bytes",
                        w[0].0.len()
                    )));
                }
            }
            Ok(Value::Map(
                canonicalised.into_iter().map(|(_, k, v)| (k, v)).collect(),
            ))
        }
        Value::Tag(tag, inner) => Ok(Value::Tag(tag, alloc::boxed::Box::new(canonicalise(*inner)?))),
        other => Ok(other),
    }
}

fn reject_floats(value: &Value) -> Result<(), EncodingError> {
    match value {
        Value::Float(_) => Err(EncodingError::FloatForbidden),
        Value::Array(items) => {
            for i in items {
                reject_floats(i)?;
            }
            Ok(())
        }
        Value::Map(pairs) => {
            for (k, v) in pairs {
                reject_floats(k)?;
                reject_floats(v)?;
            }
            Ok(())
        }
        Value::Tag(_, inner) => reject_floats(inner),
        _ => Ok(()),
    }
}