Skip to main content

librotenado/
canonical.rs

1// SPDX-License-Identifier: MIT
2//! The canonical encoding (spec part 0003): JSON as defined by the JSON Canonicalization Scheme (RFC 8785), restricted
3//! to what the protocol uses – no floating-point numbers, integers up to 2^53 − 1.
4
5use std::fmt;
6
7use serde::{
8    Deserialize, Deserializer, Serialize,
9    de::{self, MapAccess, SeqAccess, Visitor},
10};
11use serde_json::{Map, Value};
12
13/// Why a value cannot be encoded canonically.
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15pub enum CanonicalError {
16    /// A number that is not an integer between −(2^53 − 1) and 2^53 − 1.
17    #[error("number {0} is not a safe integer")]
18    Number(String),
19    /// The value cannot be represented as JSON, or the input is not JSON – including an object with two members of
20    /// the same name.
21    #[error("not representable as JSON: {0}")]
22    Json(String),
23}
24
25/// The largest integer every JSON implementation represents exactly (2^53 − 1).
26pub const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
27
28/// The canonical bytes of a value: UTF-8, no whitespace, object members sorted by their names' UTF-16 code units,
29/// strings escaped only where JSON requires it.
30pub fn to_canonical_vec<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, CanonicalError> {
31    let value = serde_json::to_value(value).map_err(|e| CanonicalError::Json(e.to_string()))?;
32    let mut out = String::new();
33    write_value(&value, &mut out)?;
34    Ok(out.into_bytes())
35}
36
37/// The canonical bytes of JSON text as received: every member is kept, also those the reader does not understand.
38/// Rejects text that is not JSON, objects with two members of the same name, and numbers that are not safe integers.
39pub fn canonicalize(json: &[u8]) -> Result<Vec<u8>, CanonicalError> {
40    let Strict(value) = serde_json::from_slice(json).map_err(|e| CanonicalError::Json(e.to_string()))?;
41    let mut out = String::new();
42    write_value(&value, &mut out)?;
43    Ok(out.into_bytes())
44}
45
46/// A JSON value read without letting a later member silently replace an earlier one of the same name.
47struct Strict(Value);
48
49impl<'de> Deserialize<'de> for Strict {
50    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
51        deserializer.deserialize_any(StrictVisitor).map(Strict)
52    }
53}
54
55struct StrictVisitor;
56
57impl<'de> Visitor<'de> for StrictVisitor {
58    type Value = Value;
59
60    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str("a JSON value")
62    }
63
64    fn visit_unit<E>(self) -> Result<Value, E> {
65        Ok(Value::Null)
66    }
67
68    fn visit_bool<E>(self, v: bool) -> Result<Value, E> {
69        Ok(Value::Bool(v))
70    }
71
72    fn visit_i64<E>(self, v: i64) -> Result<Value, E> {
73        Ok(Value::from(v))
74    }
75
76    fn visit_u64<E>(self, v: u64) -> Result<Value, E> {
77        Ok(Value::from(v))
78    }
79
80    fn visit_f64<E>(self, v: f64) -> Result<Value, E> {
81        // Kept as a float, so the encoder rejects it.
82        Ok(Value::from(v))
83    }
84
85    fn visit_str<E>(self, v: &str) -> Result<Value, E> {
86        Ok(Value::String(v.to_owned()))
87    }
88
89    fn visit_string<E>(self, v: String) -> Result<Value, E> {
90        Ok(Value::String(v))
91    }
92
93    fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Value, A::Error> {
94        let mut items = Vec::new();
95        while let Some(Strict(item)) = seq.next_element()? {
96            items.push(item);
97        }
98        Ok(Value::Array(items))
99    }
100
101    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
102        let mut members = Map::new();
103        while let Some(name) = map.next_key::<String>()? {
104            let Strict(value) = map.next_value()?;
105            if members.contains_key(&name) {
106                return Err(de::Error::custom(format!("duplicate member {name:?}")));
107            }
108            members.insert(name, value);
109        }
110        Ok(Value::Object(members))
111    }
112}
113
114fn write_value(value: &Value, out: &mut String) -> Result<(), CanonicalError> {
115    match value {
116        Value::Null => out.push_str("null"),
117        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
118        Value::Number(n) => {
119            let safe = n.as_u64().is_some_and(|v| v <= MAX_SAFE_INTEGER)
120                || n.as_i64().is_some_and(|v| v.unsigned_abs() <= MAX_SAFE_INTEGER);
121            if !safe {
122                return Err(CanonicalError::Number(n.to_string()));
123            }
124            out.push_str(&n.to_string());
125        }
126        Value::String(s) => write_string(s, out),
127        Value::Array(items) => {
128            out.push('[');
129            for (i, item) in items.iter().enumerate() {
130                if i > 0 {
131                    out.push(',');
132                }
133                write_value(item, out)?;
134            }
135            out.push(']');
136        }
137        Value::Object(members) => {
138            let mut sorted: Vec<_> = members.iter().collect();
139            sorted.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
140            out.push('{');
141            for (i, (name, item)) in sorted.into_iter().enumerate() {
142                if i > 0 {
143                    out.push(',');
144                }
145                write_string(name, out);
146                out.push(':');
147                write_value(item, out)?;
148            }
149            out.push('}');
150        }
151    }
152    Ok(())
153}
154
155/// A string as RFC 8785 writes it: `"` and `\` escaped, control characters as the short escapes or `\u00xx` in
156/// lower case, everything else – including non-ASCII and `/` – as is.
157fn write_string(s: &str, out: &mut String) {
158    out.push('"');
159    for c in s.chars() {
160        match c {
161            '"' => out.push_str("\\\""),
162            '\\' => out.push_str("\\\\"),
163            '\u{8}' => out.push_str("\\b"),
164            '\u{c}' => out.push_str("\\f"),
165            '\n' => out.push_str("\\n"),
166            '\r' => out.push_str("\\r"),
167            '\t' => out.push_str("\\t"),
168            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
169            c => out.push(c),
170        }
171    }
172    out.push('"');
173}
174
175#[cfg(test)]
176mod tests {
177    use serde_json::json;
178
179    use super::*;
180
181    fn canon(value: Value) -> String {
182        String::from_utf8(to_canonical_vec(&value).unwrap()).unwrap()
183    }
184
185    #[test]
186    fn sorts_members_and_drops_whitespace() {
187        assert_eq!(
188            canon(json!({"b": 1, "a": [true, null, {"d": "x", "c": 2}]})),
189            r#"{"a":[true,null,{"c":2,"d":"x"}],"b":1}"#
190        );
191    }
192
193    #[test]
194    fn sorts_by_utf16_code_units() {
195        // RFC 8785, 3.2.3: "\u{10000}" (surrogate pair D800 DC00) sorts before "\u{FB33}".
196        assert_eq!(canon(json!({"\u{FB33}": 1, "\u{10000}": 2})), "{\"\u{10000}\":2,\"\u{FB33}\":1}");
197    }
198
199    #[test]
200    fn escapes_only_what_json_requires() {
201        assert_eq!(canon(json!("a\"b\\c/€\u{1}\n")), "\"a\\\"b\\\\c/€\\u0001\\n\"");
202        assert_eq!(canon(json!("\u{1f}")), "\"\\u001f\"");
203    }
204
205    #[test]
206    fn canonicalizes_received_text() {
207        let text = br#"{ "b" : [1, "\u00e9"], "a" : {"unknown": null} }"#;
208        assert_eq!(canonicalize(text).unwrap(), "{\"a\":{\"unknown\":null},\"b\":[1,\"\u{e9}\"]}".as_bytes());
209        let duplicate = canonicalize(br#"{"a":1,"a":2}"#).unwrap_err();
210        assert!(duplicate.to_string().contains("duplicate member \"a\""), "{duplicate}");
211        for bad in [&b"{"[..], b"1.5", b"9007199254740992", b"{\"a\":1e3}"] {
212            assert!(canonicalize(bad).is_err(), "{}", String::from_utf8_lossy(bad));
213        }
214    }
215
216    #[test]
217    fn allows_only_safe_integers() {
218        assert_eq!(canon(json!(9_007_199_254_740_991u64)), "9007199254740991");
219        assert_eq!(canon(json!(-42)), "-42");
220        assert!(to_canonical_vec(&json!(9_007_199_254_740_992u64)).is_err());
221        assert!(to_canonical_vec(&json!(1.5)).is_err());
222    }
223}