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