Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions ciborium/src/value/canonical.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Value> =
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<T: ?Sized + ser::Serialize, W: Write>(
value: &T,
writer: W,
) -> Result<(), crate::ser::Error<W::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<u8> using RFC 8949 Deterministic Encoding.
#[cfg(feature = "std")]
#[inline]
pub fn canonical_into_vec<T: ?Sized + ser::Serialize>(
value: &T,
) -> Result<Vec<u8>, crate::ser::Error<<Vec<u8> 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)
}
5 changes: 4 additions & 1 deletion ciborium/src/value/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
95 changes: 94 additions & 1 deletion ciborium/tests/canonical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
Loading