use serde::Serialize;
use serde_json::Value;
pub const VERSION: u16 = 3;
pub fn to_bytes<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
let value = serde_json::to_value(value)?;
Ok(value_bytes(&value))
}
#[must_use]
pub fn value_bytes(value: &Value) -> Vec<u8> {
let mut out = Vec::new();
write_canonical(value, &mut out);
out
}
fn utf16_order(a: &str, b: &str) -> std::cmp::Ordering {
a.encode_utf16().cmp(b.encode_utf16())
}
fn write_canonical(value: &Value, out: &mut Vec<u8>) {
match value {
Value::Object(map) => {
out.push(b'{');
let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
keys.sort_unstable_by(|a, b| utf16_order(a, b));
for (i, key) in keys.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_scalar(&Value::String((*key).to_owned()), out);
out.push(b':');
write_canonical(&map[*key], out);
}
out.push(b'}');
}
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']');
}
scalar => write_scalar(scalar, out),
}
}
fn write_scalar(value: &Value, out: &mut Vec<u8>) {
if let Value::Number(n) = value
&& n.is_f64()
{
es_number(n.as_f64().expect("is_f64 implies as_f64"), out);
return;
}
serde_json::to_writer(&mut *out, value).expect("serde_json cannot fail on a scalar Value");
}
fn es_number(value: f64, out: &mut Vec<u8>) {
if value == 0.0 {
out.push(b'0');
return;
}
if value.is_sign_negative() {
out.push(b'-');
es_number(-value, out);
return;
}
let sci = format!("{value:e}");
let (mantissa, exponent) = sci
.split_once('e')
.expect("LowerExp always writes an exponent");
let digits: Vec<u8> = mantissa.bytes().filter(|b| *b != b'.').collect();
let exponent: i32 = exponent.parse().expect("a LowerExp exponent is an integer");
let k = i32::try_from(digits.len()).expect("shortest f64 digits fit in i32");
let n = exponent + 1;
if n >= k && n <= 21 {
out.extend_from_slice(&digits);
out.extend(std::iter::repeat_n(
b'0',
usize::try_from(n - k).expect("n >= k"),
));
} else if n > 0 && n <= 21 {
let split = usize::try_from(n).expect("n > 0");
out.extend_from_slice(&digits[..split]);
out.push(b'.');
out.extend_from_slice(&digits[split..]);
} else if n > -6 && n <= 0 {
out.extend_from_slice(b"0.");
out.extend(std::iter::repeat_n(
b'0',
usize::try_from(-n).expect("n <= 0"),
));
out.extend_from_slice(&digits);
} else {
out.push(digits[0]);
if digits.len() > 1 {
out.push(b'.');
out.extend_from_slice(&digits[1..]);
}
out.push(b'e');
let e = n - 1;
out.push(if e < 0 { b'-' } else { b'+' });
out.extend_from_slice(e.unsigned_abs().to_string().as_bytes());
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn sorted_keys_guard() {
let a = json!({"z": 1, "a": 2, "m": 3});
let b = json!({"a": 2, "m": 3, "z": 1});
assert_eq!(
value_bytes(&a),
value_bytes(&b),
"serde_json must sort object keys — is `preserve_order` enabled?"
);
assert_eq!(
String::from_utf8(value_bytes(&a)).unwrap(),
r#"{"a":2,"m":3,"z":1}"#
);
}
#[test]
fn keys_sort_by_utf16_code_unit_not_utf8_byte() {
let bmp = "\u{ffff}";
let astral = "\u{10000}";
assert!(
bmp < astral,
"the fixture no longer distinguishes the orderings"
);
let bytes = value_bytes(&json!({ astral: 1, bmp: 2 }));
let text = String::from_utf8(bytes).unwrap();
assert!(
text.find(astral) < text.find(bmp),
"keys came out in UTF-8 byte order, not UTF-16 — so a signed Agent \
Card canonicalized here is rejected by any conforming verifier, and \
every ASCII test still passes: {text}"
);
}
#[test]
fn nested_objects_are_also_sorted() {
let a = json!({"outer": {"z": 1, "a": 2}});
let b = json!({"outer": {"a": 2, "z": 1}});
assert_eq!(value_bytes(&a), value_bytes(&b));
}
#[test]
fn array_order_is_significant() {
assert_ne!(value_bytes(&json!([1, 2])), value_bytes(&json!([2, 1])));
}
#[expect(
clippy::excessive_precision,
reason = "the literal is RFC 8785's own vector, quoted verbatim"
)]
#[test]
fn doubles_format_per_rfc_8785() {
let vectors: &[(f64, &str)] = &[
(0.0, "0"),
(-0.0, "0"), (4.5, "4.5"),
(0.002, "0.002"),
(1e-6, "0.000001"), (1e-7, "1e-7"), (1e20, "100000000000000000000"), (1e21, "1e+21"), (1e30, "1e+30"),
(1e-27, "1e-27"),
(9_007_199_254_740_992.0, "9007199254740992"), (333_333_333.333_333_29, "333333333.3333333"), (9.999_999_999_999_997e22, "9.999999999999997e+22"),
(5e-324, "5e-324"), (1.797_693_134_862_315_7e308, "1.7976931348623157e+308"), (-4.5, "-4.5"),
(-1e30, "-1e+30"),
];
for (input, expected) in vectors {
let mut out = Vec::new();
es_number(*input, &mut out);
assert_eq!(
std::str::from_utf8(&out).unwrap(),
*expected,
"RFC 8785 formats {input:?} as {expected}"
);
}
}
#[test]
fn canonical_bytes_carry_rfc_8785_numbers() {
let value = json!({
"big": 1e30,
"frac": 4.5,
"int": 100,
"neg": -0.0,
"small": 1e-7,
"whole": 100.0,
});
assert_eq!(
String::from_utf8(value_bytes(&value)).unwrap(),
r#"{"big":1e+30,"frac":4.5,"int":100,"neg":0,"small":1e-7,"whole":100}"#
);
}
#[test]
fn integers_beyond_double_precision_stay_distinct() {
let a = json!(9_007_199_254_740_993_u64); let b = json!(9_007_199_254_740_992_u64); assert_ne!(value_bytes(&a), value_bytes(&b));
assert_eq!(
String::from_utf8(value_bytes(&a)).unwrap(),
"9007199254740993"
);
}
}