use alloc::{string::String, vec::Vec};
use core::cmp::Ordering;
use blake3;
use ciborium::{de::from_reader, ser::into_writer, value::Value};
use serde::{Serialize, de::DeserializeOwned};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CanonicalError {
#[error("CBOR decode failed: {0}")]
Decode(String),
#[error("CBOR encode failed: {0}")]
Encode(String),
#[error("CBOR map keys must be text strings")]
NonStringMapKey,
#[error("CBOR floats are not allowed in canonical data")]
FloatNotAllowed,
#[error("CBOR tags are not allowed in canonical data")]
TagNotAllowed,
#[error("Base32 decode failed: {0}")]
Base32(#[from] Base32Error),
#[error("CBOR payload is not canonical")]
NotCanonical,
}
pub type Result<T> = core::result::Result<T, CanonicalError>;
pub fn to_canonical_cbor<T: Serialize>(value: &T) -> Result<Vec<u8>> {
let mut interim = Vec::new();
into_writer(value, &mut interim).map_err(|err| CanonicalError::Encode(err.to_string()))?;
canonicalize(&interim)
}
pub fn to_canonical_cbor_allow_floats<T: Serialize>(value: &T) -> Result<Vec<u8>> {
let mut interim = Vec::new();
into_writer(value, &mut interim).map_err(|err| CanonicalError::Encode(err.to_string()))?;
canonicalize_allow_floats(&interim)
}
pub fn from_cbor<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
from_reader(bytes).map_err(|err| CanonicalError::Decode(err.to_string()))
}
pub fn ensure_canonical(bytes: &[u8]) -> Result<()> {
let canonical = canonicalize(bytes)?;
if bytes != canonical.as_slice() {
return Err(CanonicalError::NotCanonical);
}
Ok(())
}
pub fn canonicalize(bytes: &[u8]) -> Result<Vec<u8>> {
let value: Value = from_reader(bytes).map_err(|err| CanonicalError::Decode(err.to_string()))?;
let canonical = canonicalize_value(value)?;
let mut buf = Vec::new();
into_writer(&canonical, &mut buf).map_err(|err| CanonicalError::Encode(err.to_string()))?;
Ok(buf)
}
pub fn canonicalize_allow_floats(bytes: &[u8]) -> Result<Vec<u8>> {
let value: Value = from_reader(bytes).map_err(|err| CanonicalError::Decode(err.to_string()))?;
let canonical = canonicalize_value_allow_floats(value)?;
let mut buf = Vec::new();
into_writer(&canonical, &mut buf).map_err(|err| CanonicalError::Encode(err.to_string()))?;
Ok(buf)
}
fn canonicalize_value(value: Value) -> Result<Value> {
match value {
Value::Integer(_) | Value::Bytes(_) | Value::Text(_) | Value::Bool(_) | Value::Null => {
Ok(value)
}
Value::Array(elements) => {
let canonical_elements = elements
.into_iter()
.map(canonicalize_value)
.collect::<Result<Vec<_>>>()?;
Ok(Value::Array(canonical_elements))
}
Value::Map(entries) => {
let mut canonical_entries = entries
.into_iter()
.map(|(key, val)| {
let canonical_key = match key {
Value::Text(text) => Value::Text(text),
_ => return Err(CanonicalError::NonStringMapKey),
};
let canonical_val = canonicalize_value(val)?;
Ok((canonical_key, canonical_val))
})
.collect::<Result<Vec<_>>>()?;
canonical_entries.sort_unstable_by(|(a, _), (b, _)| compare_map_keys(a, b));
Ok(Value::Map(canonical_entries))
}
Value::Float(_) => Err(CanonicalError::FloatNotAllowed),
Value::Tag(_, _) => Err(CanonicalError::TagNotAllowed),
_ => Err(CanonicalError::TagNotAllowed),
}
}
fn canonicalize_value_allow_floats(value: Value) -> Result<Value> {
match value {
Value::Integer(_)
| Value::Bytes(_)
| Value::Text(_)
| Value::Bool(_)
| Value::Null
| Value::Float(_) => Ok(value),
Value::Array(elements) => {
let canonical_elements = elements
.into_iter()
.map(canonicalize_value_allow_floats)
.collect::<Result<Vec<_>>>()?;
Ok(Value::Array(canonical_elements))
}
Value::Map(entries) => {
let mut canonical_entries = entries
.into_iter()
.map(|(key, val)| {
let canonical_key = match key {
Value::Text(text) => Value::Text(text),
_ => return Err(CanonicalError::NonStringMapKey),
};
let canonical_val = canonicalize_value_allow_floats(val)?;
Ok((canonical_key, canonical_val))
})
.collect::<Result<Vec<_>>>()?;
canonical_entries.sort_unstable_by(|(a, _), (b, _)| compare_map_keys(a, b));
Ok(Value::Map(canonical_entries))
}
Value::Tag(_, _) => Err(CanonicalError::TagNotAllowed),
_ => Err(CanonicalError::TagNotAllowed),
}
}
fn compare_map_keys(a: &Value, b: &Value) -> Ordering {
let (a_bytes, b_bytes) = match (a, b) {
(Value::Text(a), Value::Text(b)) => (a.as_bytes(), b.as_bytes()),
_ => panic!("map keys must be text strings"),
};
match a_bytes.len().cmp(&b_bytes.len()) {
Ordering::Equal => a_bytes.cmp(b_bytes),
other => other,
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn canonicalize_reorders_map_keys() {
let non_canonical = vec![
0xA2, 0x61, b'b', 0x01, 0x61, b'a', 0x02, ];
let canonical = match canonicalize(&non_canonical) {
Ok(bytes) => bytes,
Err(err) => panic!("canonicalize failed: {err:?}"),
};
assert_ne!(non_canonical, canonical);
assert!(ensure_canonical(&canonical).is_ok());
}
#[test]
fn ensure_canonical_rejects_indefinite_maps() {
let indefinite = vec![
0xBF, 0x61, b'a', 0x01, 0xFF, ];
assert!(matches!(
ensure_canonical(&indefinite),
Err(CanonicalError::NotCanonical)
));
}
#[test]
fn base32_decode_accepts_lowercase() {
let payload = [0x12, 0x34];
let encoded = encode_base32_crockford(&payload);
let lowercase = encoded.to_lowercase();
let decoded = match decode_base32_crockford(&lowercase) {
Ok(bytes) => bytes,
Err(err) => panic!("decode failed: {err:?}"),
};
assert_eq!(decoded, payload.to_vec());
}
}
pub fn blake3_128(bytes: &[u8]) -> [u8; 16] {
let mut out = [0u8; 16];
out.copy_from_slice(&blake3::hash(bytes).as_bytes()[..16]);
out
}
const BASE32_ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
pub fn encode_base32_crockford(bytes: &[u8]) -> String {
let mut bits = 0u32;
let mut available = 0;
let mut output = String::with_capacity((bytes.len() * 8).div_ceil(5));
for &byte in bytes {
bits = (bits << 8) | u32::from(byte);
available += 8;
while available >= 5 {
available -= 5;
let index = ((bits >> available) & 0x1f) as usize;
output.push(BASE32_ALPHABET[index] as char);
}
}
if available > 0 {
let index = ((bits << (5 - available)) & 0x1f) as usize;
output.push(BASE32_ALPHABET[index] as char);
}
output
}
pub fn decode_base32_crockford(value: &str) -> core::result::Result<Vec<u8>, Base32Error> {
let mut buffer = 0u32;
let mut bits = 0;
let mut output = Vec::with_capacity((value.len() * 5) / 8);
for ch in value.chars() {
let digit = match ch {
'0'..='9' => ch,
'a'..='z' => ch.to_ascii_uppercase(),
'A'..='Z' => ch,
other => return Err(Base32Error::InvalidCharacter(other)),
};
let val = match digit {
'I' | 'L' => 1,
'O' => 0,
symbol => {
let idx = BASE32_ALPHABET
.iter()
.position(|&b| b == symbol as u8)
.ok_or(Base32Error::InvalidCharacter(symbol))?;
idx as u8
}
};
buffer = (buffer << 5) | u32::from(val);
bits += 5;
if bits >= 8 {
bits -= 8;
output.push(((buffer >> bits) & 0xFF) as u8);
}
}
if bits != 0 && (buffer & ((1 << bits) - 1)) != 0 {
return Err(Base32Error::IncompleteByte);
}
Ok(output)
}
#[derive(Debug, Error)]
pub enum Base32Error {
#[error("invalid character {0}")]
InvalidCharacter(char),
#[error("incomplete trailing bits")]
IncompleteByte,
}