diff --git a/ciborium/src/value/canonical.rs b/ciborium/src/value/canonical.rs index 072e1cf..cd90fa6 100644 --- a/ciborium/src/value/canonical.rs +++ b/ciborium/src/value/canonical.rs @@ -1,10 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::value::Value; -use alloc::vec::Vec; +use alloc::{boxed::Box, string::ToString, vec::Vec}; +use ciborium_io::Write; use core::cmp::Ordering; use serde::{de, ser}; +use crate::value::Value; + /// Manually serialize values to compare them. fn serialized_canonical_cmp(v1: &Value, v2: &Value) -> Ordering { // There is an optimization to be done here, but it would take a lot more code @@ -122,3 +124,63 @@ impl PartialOrd for CanonicalValue { Some(self.cmp(other)) } } + +/// Recursively convert a Value to its canonical form as defined in RFC 8949 "core deterministic encoding requirements". +pub fn canonical_value(value: Value) -> Value { + match value { + Value::Map(entries) => { + let mut canonical_entries: Vec<(Value, Value)> = entries + .into_iter() + .map(|(k, v)| (canonical_value(k), canonical_value(v))) + .collect(); + + // Sort entries based on the canonical comparison of their keys. + // cmp_value (defined in this file) implements RFC 8949 key sorting. + canonical_entries.sort_by(|(k1, _), (k2, _)| cmp_value(k1, k2)); + + Value::Map(canonical_entries) + } + Value::Array(elements) => { + let canonical_elements: Vec = + elements.into_iter().map(canonical_value).collect(); + Value::Array(canonical_elements) + } + Value::Tag(tag, inner_value) => { + // The tag itself is a u64; its representation is handled by the serializer. + // The inner value must be in canonical form. + Value::Tag(tag, Box::new(canonical_value(*inner_value))) + } + // Other Value variants (Integer, Bytes, Text, Bool, Null, Float) + // are considered "canonical" in their structure. + _ => value, + } +} + +/// Serializes an object as CBOR into a writer using RFC 8949 Deterministic Encoding. +#[inline] +pub fn canonical_into_writer( + value: &T, + writer: W, +) -> Result<(), crate::ser::Error> +where + W::Error: core::fmt::Debug, +{ + let value = + Value::serialized(value).map_err(|err| crate::ser::Error::Value(err.to_string()))?; + + let cvalue = canonical_value(value); + crate::into_writer(&cvalue, writer) +} + +/// Serializes an object as CBOR into a new Vec using RFC 8949 Deterministic Encoding. +#[cfg(feature = "std")] +#[inline] +pub fn canonical_into_vec( + value: &T, +) -> Result, crate::ser::Error< as ciborium_io::Write>::Error>> { + let value = + Value::serialized(value).map_err(|err| crate::ser::Error::Value(err.to_string()))?; + + let cvalue = canonical_value(value); + crate::into_vec(&cvalue) +} diff --git a/ciborium/src/value/mod.rs b/ciborium/src/value/mod.rs index 7233026..aaf4373 100644 --- a/ciborium/src/value/mod.rs +++ b/ciborium/src/value/mod.rs @@ -9,10 +9,13 @@ mod de; mod error; mod ser; -pub use canonical::CanonicalValue; +pub use canonical::{canonical_into_writer, canonical_value, CanonicalValue}; pub use error::Error; pub use integer::Integer; +#[cfg(feature = "std")] +pub use canonical::canonical_into_vec; + use alloc::{boxed::Box, string::String, vec::Vec}; /// A representation of a dynamic CBOR value that can handled dynamically diff --git a/ciborium/tests/canonical.rs b/ciborium/tests/canonical.rs index d4aa33c..9689a37 100644 --- a/ciborium/tests/canonical.rs +++ b/ciborium/tests/canonical.rs @@ -4,8 +4,9 @@ extern crate std; use ciborium::cbor; use ciborium::tag::Required; -use ciborium::value::CanonicalValue; +use ciborium::value::{canonical_into_writer, canonical_value, CanonicalValue, Value}; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; macro_rules! cval { @@ -109,3 +110,95 @@ fn tagged_option() { let output = ciborium::de::from_reader(&bytes[..]).unwrap(); assert_eq!(opt, output); } + +#[test] +fn canonical_value_example() { + let map = Value::Map(vec![ + (val!(false), val!(2)), + (val!([-1]), val!(5)), + (val!(-1), val!(1)), + (val!(10), val!(0)), + (val!(100), val!(3)), + (val!([100]), val!(7)), + (val!("z"), val!(4)), + (val!("aa"), val!(6)), + ]); + + let mut bytes = Vec::new(); + canonical_into_writer(&map, &mut bytes).unwrap(); + assert_eq!( + hex::encode(&bytes), + "a80a002001f402186403617a048120056261610681186407" + ); + + bytes.clear(); + let canonical = canonical_value(map); + ciborium::ser::into_writer(&canonical, &mut bytes).unwrap(); + + assert_eq!( + hex::encode(&bytes), + "a80a002001f402186403617a048120056261610681186407" + ); +} + +#[test] +fn canonical_value_nested_structures() { + // Create nested structure with unsorted maps + let nested = Value::Array(vec![ + Value::Map(vec![(val!("b"), val!(2)), (val!("a"), val!(1))]), + Value::Tag( + 1, + Box::new(Value::Map(vec![ + (val!(100), val!("high")), + (val!(10), val!("low")), + ])), + ), + ]); + + let canonical = canonical_value(nested); + + if let Value::Array(elements) = canonical { + // Check first map is sorted + if let Value::Map(entries) = &elements[0] { + assert_eq!(entries[0].0, val!("a")); + assert_eq!(entries[1].0, val!("b")); + } + + // Check tagged map is sorted + if let Value::Tag(_, inner) = &elements[1] { + if let Value::Map(entries) = inner.as_ref() { + assert_eq!(entries[0].0, val!(10)); + assert_eq!(entries[1].0, val!(100)); + } + } + } else { + panic!("Expected Array value"); + } +} + +#[test] +fn canonical_value_struct() { + #[derive(Clone, Debug, Deserialize, Serialize)] + struct T1 { + a: u32, + b: u32, + c: u32, + } + + #[derive(Clone, Debug, Deserialize, Serialize)] + struct T2 { + c: u32, + b: u32, + a: u32, + } + + let t1 = T1 { a: 1, b: 2, c: 3 }; + let t2 = T2 { c: 3, b: 2, a: 1 }; + + let mut bytes1 = Vec::new(); + canonical_into_writer(&t1, &mut bytes1).unwrap(); + + let mut bytes2 = Vec::new(); + canonical_into_writer(&t2, &mut bytes2).unwrap(); + assert_eq!(bytes1, bytes2); +}