use std::fmt;
use serde_json::{Number, Value};
pub const CANONICAL_JSON_VERSION: &str = "sharpebench/canonical-json/v1";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CanonicalError {
NonFinite(String),
}
impl fmt::Display for CanonicalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CanonicalError::NonFinite(rendered) => {
write!(
f,
"canonical JSON has no form for the non-finite {rendered}"
)
}
}
}
}
impl std::error::Error for CanonicalError {}
pub fn canonical_number(value: f64) -> Result<String, CanonicalError> {
if !value.is_finite() {
return Err(CanonicalError::NonFinite(format!("{value}")));
}
if value == 0.0 {
return Ok("0".to_string());
}
let magnitude = format!("{}", value.abs());
let (integer_part, fraction_part) = match magnitude.split_once('.') {
Some((integer, fraction)) => (integer, fraction),
None => (magnitude.as_str(), ""),
};
let all = format!("{integer_part}{fraction_part}");
let leading_zeros = all.bytes().take_while(|byte| *byte == b'0').count();
let n = integer_part.len() as i64 - leading_zeros as i64;
let digits = all[leading_zeros..].trim_end_matches('0');
let k = digits.len() as i64;
let body = if k <= n && n <= 21 {
format!("{digits}{}", "0".repeat((n - k) as usize))
} else if 0 < n && n <= 21 {
let split = n as usize;
format!("{}.{}", &digits[..split], &digits[split..])
} else if -6 < n && n <= 0 {
format!("0.{}{digits}", "0".repeat((-n) as usize))
} else if k == 1 {
format!("{digits}e{}", signed_exponent(n - 1))
} else {
format!(
"{}.{}e{}",
&digits[..1],
&digits[1..],
signed_exponent(n - 1)
)
};
Ok(if value < 0.0 {
format!("-{body}")
} else {
body
})
}
fn signed_exponent(exponent: i64) -> String {
if exponent < 0 {
format!("-{}", -exponent)
} else {
format!("+{exponent}")
}
}
pub fn canonical_json_number(number: &Number) -> Result<String, CanonicalError> {
if let Some(value) = number.as_i64() {
return Ok(value.to_string());
}
if let Some(value) = number.as_u64() {
return Ok(value.to_string());
}
let value = number
.as_f64()
.ok_or_else(|| CanonicalError::NonFinite(number.to_string()))?;
canonical_number(value)
}
pub fn canonical_json(value: &Value) -> Result<String, CanonicalError> {
let mut output = String::new();
write_canonical(value, &mut output)?;
Ok(output)
}
fn write_canonical(value: &Value, output: &mut String) -> Result<(), CanonicalError> {
match value {
Value::Null => output.push_str("null"),
Value::Bool(true) => output.push_str("true"),
Value::Bool(false) => output.push_str("false"),
Value::Number(number) => output.push_str(&canonical_json_number(number)?),
Value::String(text) => write_canonical_string(text, output),
Value::Array(values) => {
output.push('[');
for (index, value) in values.iter().enumerate() {
if index > 0 {
output.push(',');
}
write_canonical(value, output)?;
}
output.push(']');
}
Value::Object(members) => {
output.push('{');
let mut fields: Vec<_> = members.iter().collect();
fields.sort_by(|left, right| left.0.cmp(right.0));
for (index, (key, value)) in fields.into_iter().enumerate() {
if index > 0 {
output.push(',');
}
write_canonical_string(key, output);
output.push(':');
write_canonical(value, output)?;
}
output.push('}');
}
}
Ok(())
}
fn write_canonical_string(text: &str, output: &mut String) {
output.push('"');
for character in text.chars() {
match character {
'"' => output.push_str("\\\""),
'\\' => output.push_str("\\\\"),
'\u{08}' => output.push_str("\\b"),
'\u{09}' => output.push_str("\\t"),
'\u{0a}' => output.push_str("\\n"),
'\u{0c}' => output.push_str("\\f"),
'\u{0d}' => output.push_str("\\r"),
control if control < '\u{20}' => {
output.push_str(&format!("\\u{:04x}", control as u32));
}
other => output.push(other),
}
}
output.push('"');
}
pub fn versioned_preimage(value: &Value) -> Result<Vec<u8>, CanonicalError> {
Ok(frame(CANONICAL_JSON_VERSION, &canonical_json(value)?))
}
fn frame(version: &str, body: &str) -> Vec<u8> {
let mut bytes = Vec::with_capacity(version.len() + 9 + body.len());
bytes.extend_from_slice(version.as_bytes());
bytes.push(0x00);
bytes.extend_from_slice(&(body.len() as u64).to_be_bytes());
bytes.extend_from_slice(body.as_bytes());
bytes
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn a_future_version_cannot_share_a_preimage() {
let value = json!({"neutral_threshold": 1e-5});
let body = canonical_json(&value).unwrap();
assert_eq!(
versioned_preimage(&value).unwrap(),
frame(CANONICAL_JSON_VERSION, &body)
);
assert_ne!(
versioned_preimage(&value).unwrap(),
frame("sharpebench/canonical-json/v2", &body)
);
}
#[test]
fn the_frame_is_injective_across_body_boundaries() {
let short = frame(CANONICAL_JSON_VERSION, "\"ab\"");
let long = frame(CANONICAL_JSON_VERSION, "\"abc\"");
assert_ne!(short, long);
assert!(!long.starts_with(&short));
}
}