Skip to content

Commit caac9f2

Browse files
committed
feat: add canonical_into_vec, canonical_into_writer, canonical_value
Signed-off-by: 0xZensh <txr1883@gmail.com>
1 parent 66554fc commit caac9f2

File tree

3 files changed

+152
-2
lines changed

3 files changed

+152
-2
lines changed

ciborium/src/value/canonical.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use crate::value::Value;
44
use alloc::vec::Vec;
5+
use ciborium_io::Write;
56
use core::cmp::Ordering;
67
use serde::{de, ser};
78

@@ -122,3 +123,63 @@ impl PartialOrd for CanonicalValue {
122123
Some(self.cmp(other))
123124
}
124125
}
126+
127+
/// Recursively convert a Value to its canonical form as defined in RFC 8949 "core deterministic encoding requirements".
128+
pub fn canonical_value(value: Value) -> Value {
129+
match value {
130+
Value::Map(entries) => {
131+
let mut canonical_entries: Vec<(Value, Value)> = entries
132+
.into_iter()
133+
.map(|(k, v)| (canonical_value(k), canonical_value(v)))
134+
.collect();
135+
136+
// Sort entries based on the canonical comparison of their keys.
137+
// cmp_value (defined in this file) implements RFC 8949 key sorting.
138+
canonical_entries.sort_by(|(k1, _), (k2, _)| cmp_value(k1, k2));
139+
140+
Value::Map(canonical_entries)
141+
}
142+
Value::Array(elements) => {
143+
let canonical_elements: Vec<Value> =
144+
elements.into_iter().map(canonical_value).collect();
145+
Value::Array(canonical_elements)
146+
}
147+
Value::Tag(tag, inner_value) => {
148+
// The tag itself is a u64; its representation is handled by the serializer.
149+
// The inner value must be in canonical form.
150+
Value::Tag(tag, Box::new(canonical_value(*inner_value)))
151+
}
152+
// Other Value variants (Integer, Bytes, Text, Bool, Null, Float)
153+
// are considered "canonical" in their structure.
154+
_ => value,
155+
}
156+
}
157+
158+
/// Serializes an object as CBOR into a writer using RFC 8949 Deterministic Encoding.
159+
#[inline]
160+
pub fn canonical_into_writer<T: ?Sized + ser::Serialize, W: Write>(
161+
value: &T,
162+
writer: W,
163+
) -> Result<(), crate::ser::Error<W::Error>>
164+
where
165+
W::Error: core::fmt::Debug,
166+
{
167+
let value =
168+
Value::serialized(value).map_err(|err| crate::ser::Error::Value(err.to_string()))?;
169+
170+
let cvalue = canonical_value(value);
171+
crate::into_writer(&cvalue, writer)
172+
}
173+
174+
/// Serializes an object as CBOR into a new Vec<u8> using RFC 8949 Deterministic Encoding.
175+
#[cfg(feature = "std")]
176+
#[inline]
177+
pub fn canonical_into_vec<T: ?Sized + ser::Serialize>(
178+
value: &T,
179+
) -> Result<Vec<u8>, crate::ser::Error<<Vec<u8> as ciborium_io::Write>::Error>> {
180+
let value =
181+
Value::serialized(value).map_err(|err| crate::ser::Error::Value(err.to_string()))?;
182+
183+
let cvalue = canonical_value(value);
184+
crate::into_vec(&cvalue)
185+
}

ciborium/src/value/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod de;
99
mod error;
1010
mod ser;
1111

12-
pub use canonical::CanonicalValue;
12+
pub use canonical::{canonical_into_vec, canonical_into_writer, canonical_value, CanonicalValue};
1313
pub use error::Error;
1414
pub use integer::Integer;
1515

ciborium/tests/canonical.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ extern crate std;
44

55
use ciborium::cbor;
66
use ciborium::tag::Required;
7-
use ciborium::value::CanonicalValue;
7+
use ciborium::value::{canonical_into_vec, canonical_value, CanonicalValue, Value};
88
use rand::prelude::*;
9+
use serde::{Deserialize, Serialize};
910
use std::collections::BTreeMap;
1011

1112
macro_rules! cval {
@@ -109,3 +110,91 @@ fn tagged_option() {
109110
let output = ciborium::de::from_reader(&bytes[..]).unwrap();
110111
assert_eq!(opt, output);
111112
}
113+
114+
#[test]
115+
fn canonical_value_example() {
116+
let map = Value::Map(vec![
117+
(val!(false), val!(2)),
118+
(val!([-1]), val!(5)),
119+
(val!(-1), val!(1)),
120+
(val!(10), val!(0)),
121+
(val!(100), val!(3)),
122+
(val!([100]), val!(7)),
123+
(val!("z"), val!(4)),
124+
(val!("aa"), val!(6)),
125+
]);
126+
127+
let bytes = canonical_into_vec(&map).unwrap();
128+
assert_eq!(
129+
hex::encode(&bytes),
130+
"a80a002001f402186403617a048120056261610681186407"
131+
);
132+
133+
let canonical = canonical_value(map);
134+
let bytes = ciborium::ser::into_vec(&canonical).unwrap();
135+
136+
assert_eq!(
137+
hex::encode(&bytes),
138+
"a80a002001f402186403617a048120056261610681186407"
139+
);
140+
}
141+
142+
#[test]
143+
fn canonical_value_nested_structures() {
144+
// Create nested structure with unsorted maps
145+
let nested = Value::Array(vec![
146+
Value::Map(vec![(val!("b"), val!(2)), (val!("a"), val!(1))]),
147+
Value::Tag(
148+
1,
149+
Box::new(Value::Map(vec![
150+
(val!(100), val!("high")),
151+
(val!(10), val!("low")),
152+
])),
153+
),
154+
]);
155+
156+
let canonical = canonical_value(nested);
157+
158+
if let Value::Array(elements) = canonical {
159+
// Check first map is sorted
160+
if let Value::Map(entries) = &elements[0] {
161+
assert_eq!(entries[0].0, val!("a"));
162+
assert_eq!(entries[1].0, val!("b"));
163+
}
164+
165+
// Check tagged map is sorted
166+
if let Value::Tag(_, inner) = &elements[1] {
167+
if let Value::Map(entries) = inner.as_ref() {
168+
assert_eq!(entries[0].0, val!(10));
169+
assert_eq!(entries[1].0, val!(100));
170+
}
171+
}
172+
} else {
173+
panic!("Expected Array value");
174+
}
175+
}
176+
177+
#[test]
178+
fn canonical_value_struct() {
179+
#[derive(Clone, Debug, Deserialize, Serialize)]
180+
struct T1 {
181+
a: u32,
182+
b: u32,
183+
c: u32,
184+
}
185+
186+
#[derive(Clone, Debug, Deserialize, Serialize)]
187+
struct T2 {
188+
c: u32,
189+
b: u32,
190+
a: u32,
191+
}
192+
193+
let t1 = T1 { a: 1, b: 2, c: 3 };
194+
let t2 = T2 { c: 3, b: 2, a: 1 };
195+
196+
assert_eq!(
197+
canonical_into_vec(&t1).unwrap(),
198+
canonical_into_vec(&t2).unwrap()
199+
);
200+
}

0 commit comments

Comments
 (0)