Skip to main content

rustev_contract/
canonical.rs

1//! Canonical bytes and digests (spec 002, 3.3).
2//!
3//! Canonical bytes are written by this module's own writer rather than by
4//! `serde_json`'s map order, so the form does not depend on whether any crate
5//! in the build enables `serde_json/preserve_order`: object keys are sorted by
6//! byte order, there is no insignificant whitespace, strings use `serde_json`
7//! escaping, and a fractional or exponent number is refused.
8//!
9//! The record canonical form (spec 004, 5.6) differs only for numbers held
10//! as binary64, which [`write_binary64`] writes itself rather than through
11//! `serde_json`'s formatter, whose digit choice at exact ties differs. It is
12//! for backend outputs, run records, judgments, reports and replay
13//! documents. A document without a binary64 number has the same bytes in
14//! both forms.
15
16use std::fmt;
17
18use serde::Serialize;
19use serde::de::DeserializeOwned;
20use serde_json::Value;
21use sha2::{Digest as _, Sha256};
22
23/// Why a value has no canonical form.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum CanonicalError {
26    /// A fractional or exponent number, at a JSON pointer path.
27    FractionalNumber { path: String },
28    /// The value could not be serialized.
29    Serialize { message: String },
30    /// The written bytes do not parse back to an equal value: a non-finite
31    /// number, which JSON writes as `null` (spec 004, 5.6).
32    NotRoundTrip,
33}
34
35impl fmt::Display for CanonicalError {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        match self {
38            CanonicalError::FractionalNumber { path } => {
39                write!(f, "fractional number at {path} has no canonical form")
40            }
41            CanonicalError::Serialize { message } => write!(f, "cannot serialize: {message}"),
42            CanonicalError::NotRoundTrip => {
43                write!(f, "the value does not round-trip (a non-finite number?)")
44            }
45        }
46    }
47}
48
49impl std::error::Error for CanonicalError {}
50
51/// The canonical bytes of any serializable value.
52pub fn canonical_bytes<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, CanonicalError> {
53    let v = serde_json::to_value(value).map_err(|e| CanonicalError::Serialize {
54        message: e.to_string(),
55    })?;
56    let mut out = Vec::new();
57    write_value(&v, &mut out, &mut String::new(), false)?;
58    Ok(out)
59}
60
61/// The canonical bytes of an already-built JSON value.
62pub fn canonical_value_bytes(value: &Value) -> Result<Vec<u8>, CanonicalError> {
63    let mut out = Vec::new();
64    write_value(value, &mut out, &mut String::new(), false)?;
65    Ok(out)
66}
67
68/// The record canonical bytes of a typed document (spec 004, 5.6); the
69/// form is defined for typed documents only, never for an untyped JSON
70/// value, whose numbers lose the binary64 distinction. Refused
71/// unless they parse back to an equal document whose record canonical bytes
72/// are the same bytes: a non-finite number, which serializes as `null`,
73/// never gets a form.
74pub fn record_canonical_bytes<T>(value: &T) -> Result<Vec<u8>, CanonicalError>
75where
76    T: Serialize + DeserializeOwned + PartialEq,
77{
78    let out = record_write(value)?;
79    match serde_json::from_slice::<T>(&out) {
80        Ok(back) if back == *value && record_write(&back)? == out => Ok(out),
81        _ => Err(CanonicalError::NotRoundTrip),
82    }
83}
84
85fn record_write<T: Serialize>(value: &T) -> Result<Vec<u8>, CanonicalError> {
86    let v = serde_json::to_value(value).map_err(|e| CanonicalError::Serialize {
87        message: e.to_string(),
88    })?;
89    let mut out = Vec::new();
90    write_value(&v, &mut out, &mut String::new(), true)?;
91    Ok(out)
92}
93
94/// Write a finite binary64 (spec 004, 5.6): the shortest round-trip digits
95/// nearest the exact value, exact ties to the larger magnitude, as Rust's
96/// `core::fmt` shortest mode gives them; plain notation for a decimal
97/// exponent `e` with `-5 <= e < 16` and at least one fractional digit,
98/// otherwise `d[.ddd]e+N` or `d[.ddd]e-N`; zero as `0.0` or `-0.0`.
99pub fn write_binary64(x: f64, out: &mut Vec<u8>) {
100    if x == 0.0 {
101        out.extend_from_slice(if x.is_sign_negative() {
102            b"-0.0"
103        } else {
104            b"0.0"
105        });
106        return;
107    }
108    // `{:e}` gives the shortest round-trip digits as `d[.ddd]eN`.
109    let sci = format!("{:e}", x.abs());
110    let (mantissa, exp) = sci.split_once('e').unwrap_or((sci.as_str(), "0"));
111    let e: i32 = exp.parse().unwrap_or(0);
112    let digits: Vec<u8> = mantissa.bytes().filter(u8::is_ascii_digit).collect();
113    let n = digits.len() as i32;
114    if x.is_sign_negative() {
115        out.push(b'-');
116    }
117    if (-5..16).contains(&e) {
118        if e >= 0 {
119            let whole = (e + 1) as usize;
120            if n <= e + 1 {
121                out.extend_from_slice(&digits);
122                out.extend(std::iter::repeat_n(b'0', whole - digits.len()));
123                out.extend_from_slice(b".0");
124            } else {
125                out.extend_from_slice(&digits[..whole]);
126                out.push(b'.');
127                out.extend_from_slice(&digits[whole..]);
128            }
129        } else {
130            out.extend_from_slice(b"0.");
131            out.extend(std::iter::repeat_n(b'0', (-e - 1) as usize));
132            out.extend_from_slice(&digits);
133        }
134    } else {
135        out.push(digits[0]);
136        if n > 1 {
137            out.push(b'.');
138            out.extend_from_slice(&digits[1..]);
139        }
140        out.push(b'e');
141        out.push(if e > 0 { b'+' } else { b'-' });
142        out.extend_from_slice(e.unsigned_abs().to_string().as_bytes());
143    }
144}
145
146fn write_str(s: &str, out: &mut Vec<u8>) {
147    // Serializing a &str cannot fail.
148    out.extend_from_slice(serde_json::to_string(s).unwrap_or_default().as_bytes());
149}
150
151fn write_value(
152    v: &Value,
153    out: &mut Vec<u8>,
154    path: &mut String,
155    fractional: bool,
156) -> Result<(), CanonicalError> {
157    match v {
158        Value::Null => out.extend_from_slice(b"null"),
159        Value::Bool(b) => out.extend_from_slice(if *b { b"true" } else { b"false" }),
160        Value::Number(n) => {
161            if let Some(i) = n.as_i64() {
162                out.extend_from_slice(i.to_string().as_bytes());
163            } else if let Some(u) = n.as_u64() {
164                out.extend_from_slice(u.to_string().as_bytes());
165            } else if let (true, Some(f)) = (fractional, n.as_f64()) {
166                write_binary64(f, out);
167            } else {
168                return Err(CanonicalError::FractionalNumber {
169                    path: if path.is_empty() {
170                        "/".into()
171                    } else {
172                        path.clone()
173                    },
174                });
175            }
176        }
177        Value::String(s) => write_str(s, out),
178        Value::Array(items) => {
179            out.push(b'[');
180            for (i, item) in items.iter().enumerate() {
181                if i > 0 {
182                    out.push(b',');
183                }
184                let len = path.len();
185                path.push('/');
186                path.push_str(&i.to_string());
187                write_value(item, out, path, fractional)?;
188                path.truncate(len);
189            }
190            out.push(b']');
191        }
192        Value::Object(map) => {
193            let keys = sorted_keys(map.keys());
194            out.push(b'{');
195            for (i, k) in keys.into_iter().enumerate() {
196                if i > 0 {
197                    out.push(b',');
198                }
199                write_str(k, out);
200                out.push(b':');
201                let len = path.len();
202                path.push('/');
203                path.push_str(k);
204                write_value(&map[k], out, path, fractional)?;
205                path.truncate(len);
206            }
207            out.push(b'}');
208        }
209    }
210    Ok(())
211}
212
213/// Object keys in byte order, whatever order the map iterates in (a
214/// `serde_json` built with `preserve_order` iterates in insertion order).
215fn sorted_keys<'k>(keys: impl Iterator<Item = &'k String>) -> Vec<&'k String> {
216    let mut keys: Vec<&String> = keys.collect();
217    keys.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
218    keys
219}
220
221/// `sha256:` and the lowercase hex SHA-256 of `tag || 0x00 || bytes`.
222pub fn tagged_digest(tag: &str, bytes: &[u8]) -> String {
223    let mut h = Sha256::new();
224    h.update(tag.as_bytes());
225    h.update([0u8]);
226    h.update(bytes);
227    let d = h.finalize();
228    let mut s = String::with_capacity(71);
229    s.push_str("sha256:");
230    for b in d {
231        s.push(char::from_digit(u32::from(b >> 4), 16).unwrap_or('0'));
232        s.push(char::from_digit(u32::from(b & 0xf), 16).unwrap_or('0'));
233    }
234    s
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use serde_json::json;
241
242    #[test]
243    fn keys_are_sorted_by_bytes_and_whitespace_removed() {
244        let v =
245            json!({"b": 1, "a": [true, null, "x"], "B": {"z": -2, "y": 18446744073709551615u64}});
246        assert_eq!(
247            canonical_value_bytes(&v).unwrap(),
248            br#"{"B":{"y":18446744073709551615,"z":-2},"a":[true,null,"x"],"b":1}"#
249        );
250    }
251
252    #[test]
253    fn keys_are_sorted_whatever_the_iteration_order() {
254        let (b, a, upper) = ("b".to_string(), "a".to_string(), "B".to_string());
255        let keys = sorted_keys([&b, &a, &upper].into_iter());
256        assert_eq!(keys, vec![&upper, &a, &b]);
257    }
258
259    #[test]
260    fn a_fractional_number_is_refused_with_its_path() {
261        let v = json!({"a": [1, 2.5]});
262        assert_eq!(
263            canonical_value_bytes(&v),
264            Err(CanonicalError::FractionalNumber {
265                path: "/a/1".into()
266            })
267        );
268        // Negative control: the same document with an integer passes.
269        assert!(canonical_value_bytes(&json!({"a": [1, 2]})).is_ok());
270    }
271
272    fn binary64(x: f64) -> String {
273        let mut out = Vec::new();
274        write_binary64(x, &mut out);
275        String::from_utf8(out).unwrap()
276    }
277
278    // The tie cases are exact binary64 values (ulp 0.125 and 0.25); their
279    // literals need every digit to state the tie.
280    #[allow(clippy::excessive_precision)]
281    #[test]
282    fn binary64_known_answers_at_the_layout_boundaries() {
283        for (x, want) in [
284            (0.0, "0.0"),
285            (-0.0, "-0.0"),
286            (3.0, "3.0"),
287            (-2.5, "-2.5"),
288            (0.1, "0.1"),
289            (1e-5, "0.00001"),
290            (1.234e-5, "0.00001234"),
291            (9.99e-6, "9.99e-6"),
292            (1e-6, "1e-6"),
293            (1.5e-7, "1.5e-7"),
294            (1e15, "1000000000000000.0"),
295            (9.999999999999998e15, "9999999999999998.0"),
296            (1e16, "1e+16"),
297            (1.5e16, "1.5e+16"),
298            (1e21, "1e+21"),
299            (123456789.123, "123456789.123"),
300            (f64::MAX, "1.7976931348623157e+308"),
301            (-f64::MAX, "-1.7976931348623157e+308"),
302            (f64::MIN_POSITIVE, "2.2250738585072014e-308"),
303            (5e-324, "5e-324"),
304            // An exact tie between two shortest digit strings takes the
305            // larger magnitude; `serde_json` 1.0.151 (zmij) writes `...94.2`.
306            (934356417220194.25, "934356417220194.3"),
307            (1658206780088562.25, "1658206780088562.3"),
308            (-934356417220194.25, "-934356417220194.3"),
309        ] {
310            assert_eq!(binary64(x), want, "{x:e}");
311        }
312    }
313
314    #[test]
315    fn binary64_round_trips_through_the_correctly_rounded_parser() {
316        // Requires `serde_json/float_roundtrip` (spec 004, 5.6).
317        let mut seed: u64 = 0x9e37_79b9_7f4a_7c15;
318        for _ in 0..200_000 {
319            seed ^= seed << 13;
320            seed ^= seed >> 7;
321            seed ^= seed << 17;
322            let x = f64::from_bits(seed);
323            if !x.is_finite() {
324                continue;
325            }
326            let text = binary64(x);
327            let back: f64 = serde_json::from_str(&text).unwrap();
328            assert_eq!(back.to_bits(), x.to_bits(), "{text}");
329        }
330    }
331
332    #[test]
333    fn record_form_writes_shortest_round_trip_floats() {
334        let v: Vec<f64> = vec![0.1, 3.0, -0.0, 1e21, 1.5e-7, 5e-324, 2.0f64.sqrt()];
335        assert_eq!(
336            record_canonical_bytes(&v).unwrap(),
337            b"[0.1,3.0,-0.0,1e+21,1.5e-7,5e-324,1.4142135623730951]"
338        );
339        // The existing form still refuses them.
340        assert!(canonical_bytes(&v).is_err());
341        // Without fractional numbers the forms agree byte for byte.
342        let ints = json!({"b": [1, -2], "a": "x"});
343        assert_eq!(
344            record_canonical_bytes(&ints).unwrap(),
345            canonical_bytes(&ints).unwrap()
346        );
347    }
348
349    #[test]
350    fn record_form_refuses_non_finite_numbers() {
351        for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
352            assert_eq!(
353                record_canonical_bytes(&vec![1.0, bad]),
354                Err(CanonicalError::NotRoundTrip),
355                "{bad}"
356            );
357        }
358        // Negative control: the finite neighbour is accepted.
359        assert!(record_canonical_bytes(&vec![1.0, f64::MAX]).is_ok());
360    }
361
362    #[test]
363    fn strings_use_json_escaping() {
364        let v = json!("a\"b\\c\n\u{1}é");
365        assert_eq!(
366            canonical_value_bytes(&v).unwrap(),
367            "\"a\\\"b\\\\c\\n\\u0001é\"".as_bytes()
368        );
369    }
370
371    #[test]
372    fn digests_are_domain_separated_and_stable() {
373        let a = tagged_digest("rustev.plan/1", b"{}");
374        let b = tagged_digest("rustev.definition/1", b"{}");
375        assert_ne!(a, b);
376        assert_eq!(a.len(), 71);
377        assert!(a.starts_with("sha256:"));
378        // Known answer, computed independently: printf 'x\0' | shasum -a 256
379        assert_eq!(
380            tagged_digest("x", b""),
381            "sha256:14f825b2bbc32dd8d196367fa8776873069c12a8954d8da7513aa7704ddd09eb"
382        );
383    }
384}