Skip to content

Commit b8d8e47

Browse files
committed
feat: add canonical_into_vec, canonical_into_writer, canonical_value
Signed-off-by: 0xZensh <[email protected]>
1 parent 7217bb0 commit b8d8e47

File tree

3 files changed

+143
-40
lines changed

3 files changed

+143
-40
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: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
extern crate std;
44

55
use ciborium::cbor;
6-
use ciborium::tag::Required;
7-
use ciborium::value::CanonicalValue;
6+
use ciborium::value::{canonical_into_vec, canonical_value, CanonicalValue, Value};
87
use rand::prelude::*;
8+
use serde::{Deserialize, Serialize};
99
use std::collections::BTreeMap;
10+
use std::vec;
1011

1112
macro_rules! cval {
1213
($x:expr) => {
@@ -64,48 +65,89 @@ fn map() {
6465
}
6566

6667
#[test]
67-
fn negative_numbers() {
68-
let mut array: Vec<CanonicalValue> = vec![
69-
cval!(10),
70-
cval!(-1),
71-
cval!(-2),
72-
cval!(-3),
73-
cval!(-4),
74-
cval!(false),
75-
cval!(100),
76-
cval!(-100),
77-
cval!(-200),
78-
cval!("z"),
79-
cval!([-1]),
80-
cval!(-300),
81-
cval!("aa"),
82-
cval!([100]),
83-
];
84-
let golden = array.clone();
85-
86-
// Shuffle the array.
87-
array.shuffle(&mut rand::thread_rng());
68+
fn canonical_value_example() {
69+
let map = Value::Map(vec![
70+
(val!(false), val!(2)),
71+
(val!([-1]), val!(5)),
72+
(val!(-1), val!(1)),
73+
(val!(10), val!(0)),
74+
(val!(100), val!(3)),
75+
(val!([100]), val!(7)),
76+
(val!("z"), val!(4)),
77+
(val!("aa"), val!(6)),
78+
]);
79+
80+
let bytes = canonical_into_vec(&map).unwrap();
81+
assert_eq!(
82+
hex::encode(&bytes),
83+
"a80a002001f402186403617a048120056261610681186407"
84+
);
8885

89-
array.sort();
86+
let canonical = canonical_value(map);
87+
let bytes = ciborium::ser::into_vec(&canonical).unwrap();
9088

91-
assert_eq!(array, golden);
89+
assert_eq!(
90+
hex::encode(&bytes),
91+
"a80a002001f402186403617a048120056261610681186407"
92+
);
9293
}
9394

9495
#[test]
95-
fn tagged_option() {
96-
let mut opt = Some(Required::<u64, 0xff>(2u32.into()));
97-
98-
let mut bytes = Vec::new();
99-
ciborium::ser::into_writer(&opt, &mut bytes).unwrap();
100-
101-
let output = ciborium::de::from_reader(&bytes[..]).unwrap();
102-
assert_eq!(opt, output);
103-
104-
opt = None;
96+
fn canonical_value_nested_structures() {
97+
// Create nested structure with unsorted maps
98+
let nested = Value::Array(vec![
99+
Value::Map(vec![(val!("b"), val!(2)), (val!("a"), val!(1))]),
100+
Value::Tag(
101+
1,
102+
Box::new(Value::Map(vec![
103+
(val!(100), val!("high")),
104+
(val!(10), val!("low")),
105+
])),
106+
),
107+
]);
108+
109+
let canonical = canonical_value(nested);
110+
111+
if let Value::Array(elements) = canonical {
112+
// Check first map is sorted
113+
if let Value::Map(entries) = &elements[0] {
114+
assert_eq!(entries[0].0, val!("a"));
115+
assert_eq!(entries[1].0, val!("b"));
116+
}
117+
118+
// Check tagged map is sorted
119+
if let Value::Tag(_, inner) = &elements[1] {
120+
if let Value::Map(entries) = inner.as_ref() {
121+
assert_eq!(entries[0].0, val!(10));
122+
assert_eq!(entries[1].0, val!(100));
123+
}
124+
}
125+
} else {
126+
panic!("Expected Array value");
127+
}
128+
}
105129

106-
let mut bytes = Vec::new();
107-
ciborium::ser::into_writer(&opt, &mut bytes).unwrap();
130+
#[test]
131+
fn canonical_value_struct() {
132+
#[derive(Clone, Debug, Deserialize, Serialize)]
133+
struct T1 {
134+
a: u32,
135+
b: u32,
136+
c: u32,
137+
}
138+
139+
#[derive(Clone, Debug, Deserialize, Serialize)]
140+
struct T2 {
141+
c: u32,
142+
b: u32,
143+
a: u32,
144+
}
145+
146+
let t1 = T1 { a: 1, b: 2, c: 3 };
147+
let t2 = T2 { c: 3, b: 2, a: 1 };
108148

109-
let output = ciborium::de::from_reader(&bytes[..]).unwrap();
110-
assert_eq!(opt, output);
149+
assert_eq!(
150+
canonical_into_vec(&t1).unwrap(),
151+
canonical_into_vec(&t2).unwrap()
152+
);
111153
}

0 commit comments

Comments
 (0)