1use serde_json::Value;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum Mode {
41 Hash,
43 Pretty,
46}
47
48pub fn encode(value: &Value, mode: Mode) -> Vec<u8> {
51 let mut out = Vec::with_capacity(256);
52 write_value(&mut out, value, mode, 0);
53 out
54}
55
56fn write_value(out: &mut Vec<u8>, v: &Value, mode: Mode, depth: usize) {
57 match v {
58 Value::Null => out.extend_from_slice(b"null"),
59 Value::Bool(true) => out.extend_from_slice(b"true"),
60 Value::Bool(false) => out.extend_from_slice(b"false"),
61 Value::Number(n) => write_number(out, n),
62 Value::String(s) => write_string(out, s, mode),
63 Value::Array(a) => write_array(out, a, mode, depth),
64 Value::Object(o) => write_object(out, o, mode, depth),
65 }
66}
67
68fn write_string(out: &mut Vec<u8>, s: &str, mode: Mode) {
69 out.push(b'"');
70 for c in s.chars() {
71 write_char_escaped(out, c, mode);
72 }
73 out.push(b'"');
74}
75
76fn write_char_escaped(out: &mut Vec<u8>, c: char, mode: Mode) {
77 let cp = c as u32;
78 match c {
79 '"' => out.extend_from_slice(b"\\\""),
80 '\\' => out.extend_from_slice(b"\\\\"),
81 '/' => match mode {
82 Mode::Hash => out.extend_from_slice(b"\\/"),
83 Mode::Pretty => out.push(b'/'),
84 },
85 '\u{08}' => out.extend_from_slice(b"\\b"),
86 '\u{09}' => out.extend_from_slice(b"\\t"),
87 '\u{0a}' => out.extend_from_slice(b"\\n"),
88 '\u{0c}' => out.extend_from_slice(b"\\f"),
89 '\u{0d}' => out.extend_from_slice(b"\\r"),
90 _ if cp < 0x20 => write_unicode_escape(out, cp),
91 _ if cp < 0x80 => out.push(u8::try_from(cp).expect("ascii by construction")),
93 _ => match mode {
94 Mode::Hash => write_unicode_escape_full(out, cp),
95 Mode::Pretty => match cp {
96 0x2028 | 0x2029 => write_unicode_escape(out, cp),
101 _ => {
102 let mut buf = [0u8; 4];
103 out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
104 }
105 },
106 },
107 }
108}
109
110fn write_unicode_escape(out: &mut Vec<u8>, code: u32) {
112 const HEX: &[u8; 16] = b"0123456789abcdef";
113 out.extend_from_slice(b"\\u");
114 out.push(HEX[((code >> 12) & 0xf) as usize]);
115 out.push(HEX[((code >> 8) & 0xf) as usize]);
116 out.push(HEX[((code >> 4) & 0xf) as usize]);
117 out.push(HEX[(code & 0xf) as usize]);
118}
119
120fn write_unicode_escape_full(out: &mut Vec<u8>, code: u32) {
124 if code <= 0xFFFF {
125 write_unicode_escape(out, code);
126 } else {
127 let adjusted = code - 0x10000;
128 let high = 0xd800 + (adjusted >> 10);
129 let low = 0xdc00 + (adjusted & 0x3ff);
130 write_unicode_escape(out, high);
131 write_unicode_escape(out, low);
132 }
133}
134
135fn write_number(out: &mut Vec<u8>, n: &serde_json::Number) {
136 if let Some(i) = n.as_i64() {
137 out.extend_from_slice(i.to_string().as_bytes());
138 } else if let Some(u) = n.as_u64() {
139 out.extend_from_slice(u.to_string().as_bytes());
140 } else if let Some(f) = n.as_f64() {
141 out.extend_from_slice(f.to_string().as_bytes());
144 } else {
145 out.extend_from_slice(b"0");
149 }
150}
151
152fn write_array(out: &mut Vec<u8>, a: &[Value], mode: Mode, depth: usize) {
153 if a.is_empty() {
154 out.extend_from_slice(b"[]");
155 return;
156 }
157 match mode {
158 Mode::Hash => {
159 out.push(b'[');
160 for (i, v) in a.iter().enumerate() {
161 if i > 0 {
162 out.push(b',');
163 }
164 write_value(out, v, mode, depth + 1);
165 }
166 out.push(b']');
167 }
168 Mode::Pretty => {
169 out.push(b'[');
170 for (i, v) in a.iter().enumerate() {
171 if i > 0 {
172 out.push(b',');
173 }
174 out.push(b'\n');
175 indent(out, depth + 1);
176 write_value(out, v, mode, depth + 1);
177 }
178 out.push(b'\n');
179 indent(out, depth);
180 out.push(b']');
181 }
182 }
183}
184
185fn write_object(out: &mut Vec<u8>, o: &serde_json::Map<String, Value>, mode: Mode, depth: usize) {
186 if o.is_empty() {
187 out.extend_from_slice(b"{}");
190 return;
191 }
192 match mode {
193 Mode::Hash => {
194 out.push(b'{');
195 for (i, (k, v)) in o.iter().enumerate() {
196 if i > 0 {
197 out.push(b',');
198 }
199 write_string(out, k, mode);
200 out.push(b':');
201 write_value(out, v, mode, depth + 1);
202 }
203 out.push(b'}');
204 }
205 Mode::Pretty => {
206 out.push(b'{');
207 for (i, (k, v)) in o.iter().enumerate() {
208 if i > 0 {
209 out.push(b',');
210 }
211 out.push(b'\n');
212 indent(out, depth + 1);
213 write_string(out, k, mode);
214 out.extend_from_slice(b": ");
215 write_value(out, v, mode, depth + 1);
216 }
217 out.push(b'\n');
218 indent(out, depth);
219 out.push(b'}');
220 }
221 }
222}
223
224fn indent(out: &mut Vec<u8>, depth: usize) {
225 for _ in 0..depth {
226 out.extend_from_slice(b" ");
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use serde_json::json;
234
235 fn enc(v: &Value, mode: Mode) -> String {
236 String::from_utf8(encode(v, mode)).expect("UTF-8 output")
237 }
238
239 #[test]
242 fn hash_escapes_forward_slash() {
243 assert_eq!(enc(&json!("a/b"), Mode::Hash), "\"a\\/b\"");
245 }
246
247 #[test]
248 fn pretty_leaves_forward_slash_raw() {
249 assert_eq!(enc(&json!("a/b"), Mode::Pretty), "\"a/b\"");
251 }
252
253 #[test]
254 fn both_modes_escape_double_quote_and_backslash() {
255 assert_eq!(enc(&json!("\""), Mode::Hash), "\"\\\"\"");
256 assert_eq!(enc(&json!("\""), Mode::Pretty), "\"\\\"\"");
257 assert_eq!(enc(&json!("\\"), Mode::Hash), "\"\\\\\"");
258 assert_eq!(enc(&json!("\\"), Mode::Pretty), "\"\\\\\"");
259 }
260
261 #[test]
262 fn named_c0_escapes() {
263 assert_eq!(enc(&json!("\u{08}"), Mode::Hash), "\"\\b\"");
266 assert_eq!(enc(&json!("\u{09}"), Mode::Hash), "\"\\t\"");
267 assert_eq!(enc(&json!("\u{0a}"), Mode::Hash), "\"\\n\"");
268 assert_eq!(enc(&json!("\u{0c}"), Mode::Hash), "\"\\f\"");
269 assert_eq!(enc(&json!("\u{0d}"), Mode::Hash), "\"\\r\"");
270 assert_eq!(enc(&json!("\u{01}"), Mode::Hash), "\"\\u0001\"");
271 assert_eq!(enc(&json!("\u{0b}"), Mode::Hash), "\"\\u000b\"");
272 assert_eq!(enc(&json!("\u{1f}"), Mode::Hash), "\"\\u001f\"");
273 }
274
275 #[test]
276 fn del_0x7f_is_raw() {
277 let out = encode(&json!("\u{7f}"), Mode::Hash);
279 assert_eq!(out, vec![b'"', 0x7f, b'"']);
280 }
281
282 #[test]
283 fn hash_escapes_non_ascii_bmp_lowercase_hex() {
284 assert_eq!(enc(&json!("é"), Mode::Hash), "\"\\u00e9\"");
286 assert_eq!(enc(&json!("\u{201c}"), Mode::Hash), "\"\\u201c\"");
288 }
289
290 #[test]
291 fn hash_emits_surrogate_pair_for_supplementary_plane() {
292 assert_eq!(enc(&json!("\u{1f4a9}"), Mode::Hash), "\"\\ud83d\\udca9\"");
295 }
296
297 #[test]
298 fn pretty_emits_non_ascii_raw_utf8() {
299 let out = encode(&json!("é"), Mode::Pretty);
301 assert_eq!(out, vec![b'"', 0xc3, 0xa9, b'"']);
302 }
303
304 #[test]
305 fn pretty_still_escapes_line_terminators() {
306 assert_eq!(enc(&json!("\u{2028}"), Mode::Pretty), "\"\\u2028\"");
309 assert_eq!(enc(&json!("\u{2029}"), Mode::Pretty), "\"\\u2029\"");
310 }
311
312 #[test]
315 fn empty_collections() {
316 assert_eq!(enc(&json!([]), Mode::Hash), "[]");
317 assert_eq!(enc(&json!({}), Mode::Hash), "{}");
318 assert_eq!(enc(&json!([]), Mode::Pretty), "[]");
319 assert_eq!(enc(&json!({}), Mode::Pretty), "{}");
320 }
321
322 #[test]
323 fn hash_compact_no_whitespace() {
324 let v = json!({"name": "acme/widget", "require": {"php": "^8.3"}});
325 assert_eq!(
326 enc(&v, Mode::Hash),
327 "{\"name\":\"acme\\/widget\",\"require\":{\"php\":\"^8.3\"}}"
328 );
329 }
330
331 #[test]
332 fn hash_preserves_nested_key_order() {
333 let v = json!({"require": {"php": "^8.3", "ext-redis": "*"}});
337 let out = enc(&v, Mode::Hash);
338 assert!(out.contains("\"php\":\"^8.3\",\"ext-redis\":\"*\""));
339 }
340
341 #[test]
342 fn pretty_uses_four_space_indent() {
343 let v = json!({"a": 1, "b": [2, 3]});
344 let expected = "{\n \"a\": 1,\n \"b\": [\n 2,\n 3\n ]\n}";
346 assert_eq!(enc(&v, Mode::Pretty), expected);
347 }
348
349 #[test]
352 fn integers_plain_decimal() {
353 assert_eq!(enc(&json!(0), Mode::Hash), "0");
354 assert_eq!(enc(&json!(-7), Mode::Hash), "-7");
355 assert_eq!(enc(&json!(1_000_000_000_i64), Mode::Hash), "1000000000");
356 }
357
358 #[test]
359 fn floats_shortest_roundtrip_with_lowercase_e() {
360 assert_eq!(enc(&json!(0.1_f64), Mode::Hash), "0.1");
362 let one: Value = serde_json::from_str("1.0").unwrap();
367 assert_eq!(enc(&one, Mode::Hash), "1");
368 }
369
370 #[test]
373 fn null_true_false() {
374 assert_eq!(enc(&Value::Null, Mode::Hash), "null");
375 assert_eq!(enc(&json!(true), Mode::Hash), "true");
376 assert_eq!(enc(&json!(false), Mode::Hash), "false");
377 }
378}