use serde::Serialize;
use serde_json::Value;
use crate::error::CellosError;
pub const CANONICAL_PAYLOAD_VERSION: u32 = 1;
pub fn canonical_json_bytes(value: &Value) -> Result<Vec<u8>, CellosError> {
let mut out = Vec::new();
write_canonical(value, &mut out)?;
Ok(out)
}
pub fn canonical_payload<T: Serialize>(value: &T) -> Result<Vec<u8>, CellosError> {
let json = serde_json::to_value(value)
.map_err(|e| CellosError::InvalidSpec(format!("canonical_payload: serialize: {e}")))?;
canonical_json_bytes(&json)
}
fn write_canonical(value: &Value, out: &mut Vec<u8>) -> Result<(), CellosError> {
match value {
Value::Null => out.extend_from_slice(b"null"),
Value::Bool(b) => out.extend_from_slice(if *b { b"true" } else { b"false" }),
Value::Number(n) => {
if let Some(f) = n.as_f64() {
if n.as_i64().is_none() && n.as_u64().is_none() && !f.is_finite() {
return Err(CellosError::InvalidSpec(
"canonical json: non-finite number has no canonical form".into(),
));
}
}
out.extend_from_slice(n.to_string().as_bytes());
}
Value::String(s) => write_json_string(s, out),
Value::Array(items) => {
out.push(b'[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_canonical(item, out)?;
}
out.push(b']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable();
out.push(b'{');
for (i, key) in keys.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_json_string(key, out);
out.push(b':');
write_canonical(&map[key.as_str()], out)?;
}
out.push(b'}');
}
}
Ok(())
}
fn write_json_string(s: &str, out: &mut Vec<u8>) {
out.push(b'"');
for c in s.chars() {
match c {
'"' => out.extend_from_slice(b"\\\""),
'\\' => out.extend_from_slice(b"\\\\"),
'\n' => out.extend_from_slice(b"\\n"),
'\r' => out.extend_from_slice(b"\\r"),
'\t' => out.extend_from_slice(b"\\t"),
'\u{08}' => out.extend_from_slice(b"\\b"),
'\u{0c}' => out.extend_from_slice(b"\\f"),
c if (c as u32) < 0x20 => {
let code = c as u32;
out.extend_from_slice(format!("\\u{code:04x}").as_bytes());
}
c => {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
}
}
out.push(b'"');
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn sorts_object_keys_recursively() {
let v = json!({ "c": 3, "a": 1, "b": { "z": 26, "a": 1 } });
let bytes = canonical_json_bytes(&v).unwrap();
assert_eq!(bytes, br#"{"a":1,"b":{"a":1,"z":26},"c":3}"#.to_vec());
}
#[test]
fn independent_serializers_agree() {
let a = json!({ "x": [1, 2, 3], "y": "v", "z": null });
let b = json!({ "z": null, "y": "v", "x": [1, 2, 3] });
assert_eq!(
canonical_json_bytes(&a).unwrap(),
canonical_json_bytes(&b).unwrap()
);
}
#[test]
fn omitted_optional_field_does_not_change_bytes() {
let with_omitted = json!({ "a": 1, "b": 2 });
let baseline = json!({ "b": 2, "a": 1 });
assert_eq!(
canonical_json_bytes(&with_omitted).unwrap(),
canonical_json_bytes(&baseline).unwrap()
);
let with_present = json!({ "a": 1, "b": 2, "c": 3 });
assert_ne!(
canonical_json_bytes(&with_present).unwrap(),
canonical_json_bytes(&baseline).unwrap()
);
}
#[test]
fn array_order_is_preserved() {
let a = json!([3, 1, 2]);
assert_eq!(canonical_json_bytes(&a).unwrap(), b"[3,1,2]".to_vec());
}
#[test]
fn string_escaping_matches_serde_json_compact() {
let v = json!({ "k": "a\"b\\c\n\t\u{01}d" });
let reference = serde_json::to_vec(&v).unwrap();
assert_eq!(canonical_json_bytes(&v).unwrap(), reference);
}
#[test]
fn unicode_passes_through_unescaped() {
let v = json!({ "k": "héllo-世界" });
let reference = serde_json::to_vec(&v).unwrap();
assert_eq!(canonical_json_bytes(&v).unwrap(), reference);
}
#[test]
fn canonical_payload_lowers_typed_value() {
#[derive(serde::Serialize)]
struct T {
b: u8,
a: u8,
}
let bytes = canonical_payload(&T { b: 2, a: 1 }).unwrap();
assert_eq!(bytes, br#"{"a":1,"b":2}"#.to_vec());
}
#[test]
fn version_is_one() {
assert_eq!(CANONICAL_PAYLOAD_VERSION, 1);
}
}