Skip to main content

rite_model/
canonical.rs

1//! Canonical JSON (RFC 8785, JCS) for the values the transcript commits to.
2//!
3//! A fact is committed as the SHA-256 of its canonical form, so any
4//! implementation that canonicalises the same value gets the same bytes. The
5//! transcript holds no floating-point numbers, and integers stay within the
6//! range JavaScript represents exactly, so this writer covers that subset and
7//! refuses anything else rather than guess at the number formatting RFC 8785
8//! borrows from ECMAScript.
9
10use std::fmt;
11use std::fmt::Write as _;
12
13use serde_json::{Number, Value};
14
15/// Largest integer magnitude written: `2^53 - 1`, the edge of the range an
16/// IEEE 754 double represents exactly.
17const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
18
19/// A value outside the subset the transcript's canonical form covers.
20#[derive(Debug, Clone, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum CanonicalJsonError {
23    /// A number that is not an integer.
24    NotAnInteger(String),
25    /// An integer beyond `±(2^53 - 1)`.
26    IntegerOutOfRange(String),
27}
28
29impl fmt::Display for CanonicalJsonError {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            CanonicalJsonError::NotAnInteger(n) => {
33                write!(
34                    f,
35                    "{n} is not an integer; the transcript records integers only"
36                )
37            }
38            CanonicalJsonError::IntegerOutOfRange(n) => {
39                write!(f, "{n} is beyond the integer range the transcript records")
40            }
41        }
42    }
43}
44
45impl std::error::Error for CanonicalJsonError {}
46
47/// The RFC 8785 canonical form of `value`.
48///
49/// Members are sorted by their UTF-16 code units and written without
50/// whitespace, so any implementation that reads the same value writes the
51/// same bytes.
52///
53/// ```
54/// use rite_model::canonical_json;
55/// use serde_json::json;
56///
57/// let value = json!({ "b": 2, "a": [true, null, "x"] });
58/// assert_eq!(canonical_json(&value)?, r#"{"a":[true,null,"x"],"b":2}"#);
59/// assert!(canonical_json(&json!({ "ratio": 1.5 })).is_err());
60/// # Ok::<(), rite_model::CanonicalJsonError>(())
61/// ```
62///
63/// # Errors
64///
65/// Returns [`CanonicalJsonError`] for a non-integer number or an integer
66/// beyond `±(2^53 - 1)`.
67pub fn canonical_json(value: &Value) -> Result<String, CanonicalJsonError> {
68    let mut out = String::new();
69    write_value(value, &mut out)?;
70    Ok(out)
71}
72
73/// Checks that every number in `value` is one the canonical form writes.
74///
75/// A ceremony checks its values with this before a run, so a number the
76/// transcript cannot record is refused by `rite check` rather than mid-run.
77///
78/// # Errors
79///
80/// Returns [`CanonicalJsonError`] for the first number that is not an integer
81/// or is beyond `±(2^53 - 1)`.
82pub fn check_numbers(value: &Value) -> Result<(), CanonicalJsonError> {
83    match value {
84        Value::Null | Value::Bool(_) | Value::String(_) => Ok(()),
85        Value::Number(n) => check_number(n),
86        Value::Array(items) => items.iter().try_for_each(check_numbers),
87        Value::Object(members) => members.values().try_for_each(check_numbers),
88    }
89}
90
91fn check_number(n: &Number) -> Result<(), CanonicalJsonError> {
92    let magnitude = if let Some(i) = n.as_i64() {
93        i.unsigned_abs()
94    } else if let Some(u) = n.as_u64() {
95        u
96    } else {
97        return Err(CanonicalJsonError::NotAnInteger(n.to_string()));
98    };
99    if magnitude > MAX_SAFE_INTEGER {
100        return Err(CanonicalJsonError::IntegerOutOfRange(n.to_string()));
101    }
102    Ok(())
103}
104
105fn write_value(value: &Value, out: &mut String) -> Result<(), CanonicalJsonError> {
106    match value {
107        Value::Null => out.push_str("null"),
108        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
109        Value::Number(n) => {
110            check_number(n)?;
111            out.push_str(&n.to_string());
112        }
113        Value::String(s) => write_string(s, out),
114        Value::Array(items) => {
115            out.push('[');
116            for (i, item) in items.iter().enumerate() {
117                if i > 0 {
118                    out.push(',');
119                }
120                write_value(item, out)?;
121            }
122            out.push(']');
123        }
124        Value::Object(map) => {
125            // RFC 8785 orders members by the UTF-16 code units of their
126            // names, which differs from byte order for characters above
127            // U+FFFF.
128            let mut members: Vec<(&String, &Value)> = map.iter().collect();
129            members.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
130            out.push('{');
131            for (i, (name, member)) in members.into_iter().enumerate() {
132                if i > 0 {
133                    out.push(',');
134                }
135                write_string(name, out);
136                out.push(':');
137                write_value(member, out)?;
138            }
139            out.push('}');
140        }
141    }
142    Ok(())
143}
144
145/// A string in RFC 8785 form: `"` and `\` escaped, the five short control
146/// escapes, other control characters as lowercase `\u00xx`, everything else
147/// as itself.
148fn write_string(s: &str, out: &mut String) {
149    out.push('"');
150    for c in s.chars() {
151        match c {
152            '"' => out.push_str("\\\""),
153            '\\' => out.push_str("\\\\"),
154            '\u{08}' => out.push_str("\\b"),
155            '\t' => out.push_str("\\t"),
156            '\n' => out.push_str("\\n"),
157            '\u{0C}' => out.push_str("\\f"),
158            '\r' => out.push_str("\\r"),
159            c if c < ' ' => {
160                let _ = write!(out, "\\u{:04x}", u32::from(c));
161            }
162            c => out.push(c),
163        }
164    }
165    out.push('"');
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use serde_json::json;
172
173    #[test]
174    fn members_are_ordered_by_utf16_code_units() {
175        // The ordering example from RFC 8785 section 3.2.3.
176        let value: Value = serde_json::from_str(
177            r#"{"\u20ac":"Euro Sign","\r":"Carriage Return","\ufb33":"Hebrew Letter Dalet With Dagesh","1":"One","\ud83d\ude00":"Emoji: Grinning Face","\u0080":"Control","\u00f6":"Latin Small Letter O With Diaeresis"}"#,
178        )
179        .expect("parse");
180        // The emoji (a surrogate pair from 0xD83D) sorts before U+FB33,
181        // though its UTF-8 bytes sort after.
182        let expected = concat!(
183            r#"{"\r":"Carriage Return","1":"One","#,
184            "\"\u{80}\":\"Control\",",
185            "\"\u{f6}\":\"Latin Small Letter O With Diaeresis\",",
186            "\"\u{20ac}\":\"Euro Sign\",",
187            "\"\u{1f600}\":\"Emoji: Grinning Face\",",
188            "\"\u{fb33}\":\"Hebrew Letter Dalet With Dagesh\"}",
189        );
190        assert_eq!(canonical_json(&value).expect("canonical"), expected);
191    }
192
193    #[test]
194    fn strings_use_the_rfc_escapes() {
195        // The string example from RFC 8785 section 3.2.2.2.
196        let value: Value =
197            serde_json::from_str(r#""\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/""#)
198                .expect("parse");
199        assert_eq!(
200            canonical_json(&value).expect("canonical"),
201            r#""€$\u000f\nA'B\"\\\\\"/""#
202        );
203    }
204
205    #[test]
206    fn nesting_and_literals() {
207        let value = json!({ "b": [true, null, -3, {"z": 1, "a": "x"}], "a": 0 });
208        assert_eq!(
209            canonical_json(&value).expect("canonical"),
210            r#"{"a":0,"b":[true,null,-3,{"a":"x","z":1}]}"#
211        );
212    }
213
214    #[test]
215    fn floats_and_large_integers_are_refused() {
216        assert!(matches!(
217            canonical_json(&json!(1.5)),
218            Err(CanonicalJsonError::NotAnInteger(_))
219        ));
220        assert!(matches!(
221            canonical_json(&json!(1_u64 << 53)),
222            Err(CanonicalJsonError::IntegerOutOfRange(_))
223        ));
224        assert!(canonical_json(&json!(MAX_SAFE_INTEGER)).is_ok());
225        assert!(canonical_json(&json!(-(1_i64 << 53) + 1)).is_ok());
226    }
227
228    #[test]
229    fn check_numbers_walks_nested_values() {
230        assert!(check_numbers(&json!({ "a": [1, "x", { "b": -2 }] })).is_ok());
231        assert!(matches!(
232            check_numbers(&json!({ "a": [1, { "b": 0.5 }] })),
233            Err(CanonicalJsonError::NotAnInteger(_))
234        ));
235        assert!(matches!(
236            check_numbers(&json!([u64::MAX])),
237            Err(CanonicalJsonError::IntegerOutOfRange(_))
238        ));
239    }
240}