//! Canonical JSON: the one serialisation that gets hashed or signed. //! //! Byte-for-byte equal to Python's //! `json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")`. //! //! Object keys are sorted HERE, by code point (UTF-8 byte order is the same order). //! Nothing relies on `serde_json::Map` iteration order, which changes when any crate in //! the build enables serde_json's `preserve_order` feature. //! //! Numbers must be integers in the i64/u64 range. Floats (`1.0`, `1e3`, `-0`) and integers //! outside that range are refused: Python would serialise them, but no pack document holds //! one, and refusing them keeps float formatting out of the signed bytes. use serde_json::Value; use crate::error::{Error, Result}; /// Canonical bytes of `value`, or [`Error::Manifest`] if it holds a non-integer number. pub fn canonical_bytes(value: &Value) -> Result> { let mut out = Vec::with_capacity(1024); write_value(value, &mut out)?; Ok(out) } fn write_value(value: &Value, out: &mut Vec) -> Result<()> { match value { Value::Null => out.extend_from_slice(b"null"), Value::Bool(true) => out.extend_from_slice(b"true"), Value::Bool(false) => out.extend_from_slice(b"false"), Value::Number(n) => { if let Some(u) = n.as_u64() { out.extend_from_slice(u.to_string().as_bytes()); } else if let Some(i) = n.as_i64() { out.extend_from_slice(i.to_string().as_bytes()); } else { return Err(Error::manifest(format!( "number {n} is not an integer in the 64-bit range; pack documents hold integers only" ))); } } Value::String(s) => write_str(s, out), Value::Array(items) => { out.push(b'['); for (i, item) in items.iter().enumerate() { if i > 0 { out.push(b','); } write_value(item, out)?; } out.push(b']'); } Value::Object(map) => { let mut entries: Vec<(&String, &Value)> = map.iter().collect(); entries.sort_unstable_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes())); out.push(b'{'); for (i, (key, item)) in entries.into_iter().enumerate() { if i > 0 { out.push(b','); } write_str(key, out); out.push(b':'); write_value(item, out)?; } out.push(b'}'); } } Ok(()) } /// Python's `encode_basestring` (the `ensure_ascii=False` escaper). fn write_str(s: &str, out: &mut Vec) { const HEX: &[u8; 16] = b"0123456789abcdef"; out.push(b'"'); // Escapes only ever replace ASCII bytes, and bytes of multi-byte UTF-8 sequences are // all >= 0x80, so a byte loop copies every other character through untouched. for &b in s.as_bytes() { match b { b'"' => out.extend_from_slice(b"\\\""), b'\\' => out.extend_from_slice(b"\\\\"), b'\n' => out.extend_from_slice(b"\\n"), b'\r' => out.extend_from_slice(b"\\r"), b'\t' => out.extend_from_slice(b"\\t"), 0x08 => out.extend_from_slice(b"\\b"), 0x0c => out.extend_from_slice(b"\\f"), 0x00..=0x1f => { out.extend_from_slice(b"\\u00"); out.push(HEX[(b >> 4) as usize]); out.push(HEX[(b & 0x0f) as usize]); } _ => out.push(b), } } out.push(b'"'); } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn sorts_and_escapes_like_python() { let v = json!({"b": 1, "a": [1, {"d": 2, "c": 3}]}); assert_eq!(canonical_bytes(&v).unwrap(), br#"{"a":[1,{"c":3,"d":2}],"b":1}"#); let v = json!({"s": "q\"b\\n\n\u{1}\u{7f}é/"}); assert_eq!(canonical_bytes(&v).unwrap(), "{\"s\":\"q\\\"b\\\\n\\n\\u0001\u{7f}é/\"}".as_bytes()); } #[test] fn refuses_floats() { for text in ["1.0", "1e3", "-0", "[0.5]", "18446744073709551616"] { let v: Value = serde_json::from_str(text).unwrap(); assert!(canonical_bytes(&v).is_err(), "{text}"); } } }