use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Hash,
Pretty,
}
pub fn encode(value: &Value, mode: Mode) -> Vec<u8> {
let mut out = Vec::with_capacity(256);
write_value(&mut out, value, mode, 0);
out
}
fn write_value(out: &mut Vec<u8>, v: &Value, mode: Mode, depth: usize) {
match v {
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) => write_number(out, n),
Value::String(s) => write_string(out, s, mode),
Value::Array(a) => write_array(out, a, mode, depth),
Value::Object(o) => write_object(out, o, mode, depth),
}
}
fn write_string(out: &mut Vec<u8>, s: &str, mode: Mode) {
out.push(b'"');
for c in s.chars() {
write_char_escaped(out, c, mode);
}
out.push(b'"');
}
fn write_char_escaped(out: &mut Vec<u8>, c: char, mode: Mode) {
let cp = c as u32;
match c {
'"' => out.extend_from_slice(b"\\\""),
'\\' => out.extend_from_slice(b"\\\\"),
'/' => match mode {
Mode::Hash => out.extend_from_slice(b"\\/"),
Mode::Pretty => out.push(b'/'),
},
'\u{08}' => out.extend_from_slice(b"\\b"),
'\u{09}' => out.extend_from_slice(b"\\t"),
'\u{0a}' => out.extend_from_slice(b"\\n"),
'\u{0c}' => out.extend_from_slice(b"\\f"),
'\u{0d}' => out.extend_from_slice(b"\\r"),
_ if cp < 0x20 => write_unicode_escape(out, cp),
_ if cp < 0x80 => out.push(u8::try_from(cp).expect("ascii by construction")),
_ => match mode {
Mode::Hash => write_unicode_escape_full(out, cp),
Mode::Pretty => match cp {
0x2028 | 0x2029 => write_unicode_escape(out, cp),
_ => {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
},
},
}
}
fn write_unicode_escape(out: &mut Vec<u8>, code: u32) {
const HEX: &[u8; 16] = b"0123456789abcdef";
out.extend_from_slice(b"\\u");
out.push(HEX[((code >> 12) & 0xf) as usize]);
out.push(HEX[((code >> 8) & 0xf) as usize]);
out.push(HEX[((code >> 4) & 0xf) as usize]);
out.push(HEX[(code & 0xf) as usize]);
}
fn write_unicode_escape_full(out: &mut Vec<u8>, code: u32) {
if code <= 0xFFFF {
write_unicode_escape(out, code);
} else {
let adjusted = code - 0x10000;
let high = 0xd800 + (adjusted >> 10);
let low = 0xdc00 + (adjusted & 0x3ff);
write_unicode_escape(out, high);
write_unicode_escape(out, low);
}
}
fn write_number(out: &mut Vec<u8>, n: &serde_json::Number) {
if let Some(i) = n.as_i64() {
out.extend_from_slice(i.to_string().as_bytes());
} else if let Some(u) = n.as_u64() {
out.extend_from_slice(u.to_string().as_bytes());
} else if let Some(f) = n.as_f64() {
out.extend_from_slice(f.to_string().as_bytes());
} else {
out.extend_from_slice(b"0");
}
}
fn write_array(out: &mut Vec<u8>, a: &[Value], mode: Mode, depth: usize) {
if a.is_empty() {
out.extend_from_slice(b"[]");
return;
}
match mode {
Mode::Hash => {
out.push(b'[');
for (i, v) in a.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_value(out, v, mode, depth + 1);
}
out.push(b']');
}
Mode::Pretty => {
out.push(b'[');
for (i, v) in a.iter().enumerate() {
if i > 0 {
out.push(b',');
}
out.push(b'\n');
indent(out, depth + 1);
write_value(out, v, mode, depth + 1);
}
out.push(b'\n');
indent(out, depth);
out.push(b']');
}
}
}
fn write_object(out: &mut Vec<u8>, o: &serde_json::Map<String, Value>, mode: Mode, depth: usize) {
if o.is_empty() {
out.extend_from_slice(b"{}");
return;
}
match mode {
Mode::Hash => {
out.push(b'{');
for (i, (k, v)) in o.iter().enumerate() {
if i > 0 {
out.push(b',');
}
write_string(out, k, mode);
out.push(b':');
write_value(out, v, mode, depth + 1);
}
out.push(b'}');
}
Mode::Pretty => {
out.push(b'{');
for (i, (k, v)) in o.iter().enumerate() {
if i > 0 {
out.push(b',');
}
out.push(b'\n');
indent(out, depth + 1);
write_string(out, k, mode);
out.extend_from_slice(b": ");
write_value(out, v, mode, depth + 1);
}
out.push(b'\n');
indent(out, depth);
out.push(b'}');
}
}
}
fn indent(out: &mut Vec<u8>, depth: usize) {
for _ in 0..depth {
out.extend_from_slice(b" ");
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn enc(v: &Value, mode: Mode) -> String {
String::from_utf8(encode(v, mode)).expect("UTF-8 output")
}
#[test]
fn hash_escapes_forward_slash() {
assert_eq!(enc(&json!("a/b"), Mode::Hash), "\"a\\/b\"");
}
#[test]
fn pretty_leaves_forward_slash_raw() {
assert_eq!(enc(&json!("a/b"), Mode::Pretty), "\"a/b\"");
}
#[test]
fn both_modes_escape_double_quote_and_backslash() {
assert_eq!(enc(&json!("\""), Mode::Hash), "\"\\\"\"");
assert_eq!(enc(&json!("\""), Mode::Pretty), "\"\\\"\"");
assert_eq!(enc(&json!("\\"), Mode::Hash), "\"\\\\\"");
assert_eq!(enc(&json!("\\"), Mode::Pretty), "\"\\\\\"");
}
#[test]
fn named_c0_escapes() {
assert_eq!(enc(&json!("\u{08}"), Mode::Hash), "\"\\b\"");
assert_eq!(enc(&json!("\u{09}"), Mode::Hash), "\"\\t\"");
assert_eq!(enc(&json!("\u{0a}"), Mode::Hash), "\"\\n\"");
assert_eq!(enc(&json!("\u{0c}"), Mode::Hash), "\"\\f\"");
assert_eq!(enc(&json!("\u{0d}"), Mode::Hash), "\"\\r\"");
assert_eq!(enc(&json!("\u{01}"), Mode::Hash), "\"\\u0001\"");
assert_eq!(enc(&json!("\u{0b}"), Mode::Hash), "\"\\u000b\"");
assert_eq!(enc(&json!("\u{1f}"), Mode::Hash), "\"\\u001f\"");
}
#[test]
fn del_0x7f_is_raw() {
let out = encode(&json!("\u{7f}"), Mode::Hash);
assert_eq!(out, vec![b'"', 0x7f, b'"']);
}
#[test]
fn hash_escapes_non_ascii_bmp_lowercase_hex() {
assert_eq!(enc(&json!("é"), Mode::Hash), "\"\\u00e9\"");
assert_eq!(enc(&json!("\u{201c}"), Mode::Hash), "\"\\u201c\"");
}
#[test]
fn hash_emits_surrogate_pair_for_supplementary_plane() {
assert_eq!(enc(&json!("\u{1f4a9}"), Mode::Hash), "\"\\ud83d\\udca9\"");
}
#[test]
fn pretty_emits_non_ascii_raw_utf8() {
let out = encode(&json!("é"), Mode::Pretty);
assert_eq!(out, vec![b'"', 0xc3, 0xa9, b'"']);
}
#[test]
fn pretty_still_escapes_line_terminators() {
assert_eq!(enc(&json!("\u{2028}"), Mode::Pretty), "\"\\u2028\"");
assert_eq!(enc(&json!("\u{2029}"), Mode::Pretty), "\"\\u2029\"");
}
#[test]
fn empty_collections() {
assert_eq!(enc(&json!([]), Mode::Hash), "[]");
assert_eq!(enc(&json!({}), Mode::Hash), "{}");
assert_eq!(enc(&json!([]), Mode::Pretty), "[]");
assert_eq!(enc(&json!({}), Mode::Pretty), "{}");
}
#[test]
fn hash_compact_no_whitespace() {
let v = json!({"name": "acme/widget", "require": {"php": "^8.3"}});
assert_eq!(
enc(&v, Mode::Hash),
"{\"name\":\"acme\\/widget\",\"require\":{\"php\":\"^8.3\"}}"
);
}
#[test]
fn hash_preserves_nested_key_order() {
let v = json!({"require": {"php": "^8.3", "ext-redis": "*"}});
let out = enc(&v, Mode::Hash);
assert!(out.contains("\"php\":\"^8.3\",\"ext-redis\":\"*\""));
}
#[test]
fn pretty_uses_four_space_indent() {
let v = json!({"a": 1, "b": [2, 3]});
let expected = "{\n \"a\": 1,\n \"b\": [\n 2,\n 3\n ]\n}";
assert_eq!(enc(&v, Mode::Pretty), expected);
}
#[test]
fn integers_plain_decimal() {
assert_eq!(enc(&json!(0), Mode::Hash), "0");
assert_eq!(enc(&json!(-7), Mode::Hash), "-7");
assert_eq!(enc(&json!(1_000_000_000_i64), Mode::Hash), "1000000000");
}
#[test]
fn floats_shortest_roundtrip_with_lowercase_e() {
assert_eq!(enc(&json!(0.1_f64), Mode::Hash), "0.1");
let one: Value = serde_json::from_str("1.0").unwrap();
assert_eq!(enc(&one, Mode::Hash), "1");
}
#[test]
fn null_true_false() {
assert_eq!(enc(&Value::Null, Mode::Hash), "null");
assert_eq!(enc(&json!(true), Mode::Hash), "true");
assert_eq!(enc(&json!(false), Mode::Hash), "false");
}
}