Skip to content

Commit 1b60854

Browse files
zenshrjzak
authored andcommitted
feat: add canonical_into_vec, canonical_into_writer, canonical_value
Signed-off-by: 0xZensh <[email protected]>
1 parent 66554fc commit 1b60854

File tree

3 files changed

+162
-4
lines changed

3 files changed

+162
-4
lines changed

ciborium/src/value/canonical.rs

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// SPDX-License-Identifier: Apache-2.0
22

3-
use crate::value::Value;
4-
use alloc::vec::Vec;
3+
use alloc::{boxed::Box, string::ToString, vec::Vec};
4+
use ciborium_io::Write;
55
use core::cmp::Ordering;
66
use serde::{de, ser};
77

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

ciborium/src/value/mod.rs

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

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

16+
#[cfg(feature = "std")]
17+
pub use canonical::canonical_into_vec;
18+
1619
use alloc::{boxed::Box, string::String, vec::Vec};
1720

1821
/// A representation of a dynamic CBOR value that can handled dynamically

ciborium/tests/canonical.rs

Lines changed: 94 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_writer, 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,95 @@ 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 mut bytes = Vec::new();
128+
canonical_into_writer(&map, &mut bytes).unwrap();
129+
assert_eq!(
130+
hex::encode(&bytes),
131+
"a80a002001f402186403617a048120056261610681186407"
132+
);
133+
134+
bytes.clear();
135+
let canonical = canonical_value(map);
136+
ciborium::ser::into_writer(&canonical, &mut bytes).unwrap();
137+
138+
assert_eq!(
139+
hex::encode(&bytes),
140+
"a80a002001f402186403617a048120056261610681186407"
141+
);
142+
}
143+
144+
#[test]
145+
fn canonical_value_nested_structures() {
146+
// Create nested structure with unsorted maps
147+
let nested = Value::Array(vec![
148+
Value::Map(vec![(val!("b"), val!(2)), (val!("a"), val!(1))]),
149+
Value::Tag(
150+
1,
151+
Box::new(Value::Map(vec![
152+
(val!(100), val!("high")),
153+
(val!(10), val!("low")),
154+
])),
155+
),
156+
]);
157+
158+
let canonical = canonical_value(nested);
159+
160+
if let Value::Array(elements) = canonical {
161+
// Check first map is sorted
162+
if let Value::Map(entries) = &elements[0] {
163+
assert_eq!(entries[0].0, val!("a"));
164+
assert_eq!(entries[1].0, val!("b"));
165+
}
166+
167+
// Check tagged map is sorted
168+
if let Value::Tag(_, inner) = &elements[1] {
169+
if let Value::Map(entries) = inner.as_ref() {
170+
assert_eq!(entries[0].0, val!(10));
171+
assert_eq!(entries[1].0, val!(100));
172+
}
173+
}
174+
} else {
175+
panic!("Expected Array value");
176+
}
177+
}
178+
179+
#[test]
180+
fn canonical_value_struct() {
181+
#[derive(Clone, Debug, Deserialize, Serialize)]
182+
struct T1 {
183+
a: u32,
184+
b: u32,
185+
c: u32,
186+
}
187+
188+
#[derive(Clone, Debug, Deserialize, Serialize)]
189+
struct T2 {
190+
c: u32,
191+
b: u32,
192+
a: u32,
193+
}
194+
195+
let t1 = T1 { a: 1, b: 2, c: 3 };
196+
let t2 = T2 { c: 3, b: 2, a: 1 };
197+
198+
let mut bytes1 = Vec::new();
199+
canonical_into_writer(&t1, &mut bytes1).unwrap();
200+
201+
let mut bytes2 = Vec::new();
202+
canonical_into_writer(&t2, &mut bytes2).unwrap();
203+
assert_eq!(bytes1, bytes2);
204+
}

0 commit comments

Comments
 (0)