use std::fmt;
use std::fmt::Write as _;
use serde_json::{Number, Value};
const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CanonicalJsonError {
NotAnInteger(String),
IntegerOutOfRange(String),
}
impl fmt::Display for CanonicalJsonError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CanonicalJsonError::NotAnInteger(n) => {
write!(
f,
"{n} is not an integer; the transcript records integers only"
)
}
CanonicalJsonError::IntegerOutOfRange(n) => {
write!(f, "{n} is beyond the integer range the transcript records")
}
}
}
}
impl std::error::Error for CanonicalJsonError {}
pub fn canonical_json(value: &Value) -> Result<String, CanonicalJsonError> {
let mut out = String::new();
write_value(value, &mut out)?;
Ok(out)
}
pub fn check_numbers(value: &Value) -> Result<(), CanonicalJsonError> {
match value {
Value::Null | Value::Bool(_) | Value::String(_) => Ok(()),
Value::Number(n) => check_number(n),
Value::Array(items) => items.iter().try_for_each(check_numbers),
Value::Object(members) => members.values().try_for_each(check_numbers),
}
}
fn check_number(n: &Number) -> Result<(), CanonicalJsonError> {
let magnitude = if let Some(i) = n.as_i64() {
i.unsigned_abs()
} else if let Some(u) = n.as_u64() {
u
} else {
return Err(CanonicalJsonError::NotAnInteger(n.to_string()));
};
if magnitude > MAX_SAFE_INTEGER {
return Err(CanonicalJsonError::IntegerOutOfRange(n.to_string()));
}
Ok(())
}
fn write_value(value: &Value, out: &mut String) -> Result<(), CanonicalJsonError> {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => {
check_number(n)?;
out.push_str(&n.to_string());
}
Value::String(s) => write_string(s, out),
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_value(item, out)?;
}
out.push(']');
}
Value::Object(map) => {
let mut members: Vec<(&String, &Value)> = map.iter().collect();
members.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
out.push('{');
for (i, (name, member)) in members.into_iter().enumerate() {
if i > 0 {
out.push(',');
}
write_string(name, out);
out.push(':');
write_value(member, out)?;
}
out.push('}');
}
}
Ok(())
}
fn write_string(s: &str, out: &mut String) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{08}' => out.push_str("\\b"),
'\t' => out.push_str("\\t"),
'\n' => out.push_str("\\n"),
'\u{0C}' => out.push_str("\\f"),
'\r' => out.push_str("\\r"),
c if c < ' ' => {
let _ = write!(out, "\\u{:04x}", u32::from(c));
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn members_are_ordered_by_utf16_code_units() {
let value: Value = serde_json::from_str(
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"}"#,
)
.expect("parse");
let expected = concat!(
r#"{"\r":"Carriage Return","1":"One","#,
"\"\u{80}\":\"Control\",",
"\"\u{f6}\":\"Latin Small Letter O With Diaeresis\",",
"\"\u{20ac}\":\"Euro Sign\",",
"\"\u{1f600}\":\"Emoji: Grinning Face\",",
"\"\u{fb33}\":\"Hebrew Letter Dalet With Dagesh\"}",
);
assert_eq!(canonical_json(&value).expect("canonical"), expected);
}
#[test]
fn strings_use_the_rfc_escapes() {
let value: Value =
serde_json::from_str(r#""\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/""#)
.expect("parse");
assert_eq!(
canonical_json(&value).expect("canonical"),
r#""€$\u000f\nA'B\"\\\\\"/""#
);
}
#[test]
fn nesting_and_literals() {
let value = json!({ "b": [true, null, -3, {"z": 1, "a": "x"}], "a": 0 });
assert_eq!(
canonical_json(&value).expect("canonical"),
r#"{"a":0,"b":[true,null,-3,{"a":"x","z":1}]}"#
);
}
#[test]
fn floats_and_large_integers_are_refused() {
assert!(matches!(
canonical_json(&json!(1.5)),
Err(CanonicalJsonError::NotAnInteger(_))
));
assert!(matches!(
canonical_json(&json!(1_u64 << 53)),
Err(CanonicalJsonError::IntegerOutOfRange(_))
));
assert!(canonical_json(&json!(MAX_SAFE_INTEGER)).is_ok());
assert!(canonical_json(&json!(-(1_i64 << 53) + 1)).is_ok());
}
#[test]
fn check_numbers_walks_nested_values() {
assert!(check_numbers(&json!({ "a": [1, "x", { "b": -2 }] })).is_ok());
assert!(matches!(
check_numbers(&json!({ "a": [1, { "b": 0.5 }] })),
Err(CanonicalJsonError::NotAnInteger(_))
));
assert!(matches!(
check_numbers(&json!([u64::MAX])),
Err(CanonicalJsonError::IntegerOutOfRange(_))
));
}
}