1static JSON_ESCAPE_CHARS: [u8; 256] = [
5 0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 9u8, 10u8, 11u8, 12u8, 13u8, 14u8, 15u8, 16u8,
6 17u8, 18u8, 19u8, 20u8, 21u8, 22u8, 23u8, 24u8, 25u8, 26u8, 27u8, 28u8, 29u8, 30u8, 31u8, 34u8,
7 34u8, 32u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
8 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
9 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
10 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 33u8, 34u8, 34u8, 34u8, 34u8,
11 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
12 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
13 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
14 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
15 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
16 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
17 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
18 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
19 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
20 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8,
21];
22static JSON_ESCAPE_QUOTES: [&str; 34usize] = [
23 "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005", "\\u0006", "\\u0007", "\\b",
24 "\\t", "\\n", "\\u000b", "\\f", "\\r", "\\u000e", "\\u000f", "\\u0010", "\\u0011", "\\u0012",
25 "\\u0013", "\\u0014", "\\u0015", "\\u0016", "\\u0017", "\\u0018", "\\u0019", "\\u001a",
26 "\\u001b", "\\u001c", "\\u001d", "\\u001e", "\\u001f", "\\\"", "\\\\",
27];
28
29const ESCAPE_LEN: usize = 34;
30
31#[cold]
32#[inline(always)]
33fn write_surrogate_escape(result: &mut String, bytes: &[u8], i: usize) -> usize {
34 let code_point = ((bytes[i] as u16 & 0x0F) << 12)
35 | ((bytes[i + 1] as u16 & 0x3F) << 6)
36 | (bytes[i + 2] as u16 & 0x3F);
37
38 result.push_str("\\u");
39 let hex = [
40 (code_point >> 12) as u8,
41 ((code_point >> 8) & 0xF) as u8,
42 ((code_point >> 4) & 0xF) as u8,
43 (code_point & 0xF) as u8,
44 ];
45 for h in hex {
46 result.push(if h < 10 {
47 (b'0' + h) as char
48 } else {
49 (b'a' + h - 10) as char
50 });
51 }
52 3
53}
54
55#[allow(dead_code)]
56pub fn escape_json(bytes: &[u8]) -> String {
57 let mut result = String::new();
58 escape_json_string(&mut result, bytes);
59 result
60}
61
62#[inline(always)]
63fn process_byte(
64 result: &mut String,
65 bytes: &[u8],
66 byte: u8,
67 i: &mut usize,
68 start: &mut usize,
69 len: usize,
70) {
71 let c = JSON_ESCAPE_CHARS[byte as usize] as usize;
74 if c < ESCAPE_LEN {
75 let esc = unsafe { JSON_ESCAPE_QUOTES.get_unchecked(c) }.as_bytes();
77 let pending = unsafe { bytes.get_unchecked(*start..*i) };
78 unsafe {
80 let vec = result.as_mut_vec();
81 let total = pending.len() + esc.len();
82 vec.reserve(total);
83 let cur = vec.len();
84 let dst = vec.as_mut_ptr().add(cur);
85 std::ptr::copy_nonoverlapping(pending.as_ptr(), dst, pending.len());
86 std::ptr::copy_nonoverlapping(esc.as_ptr(), dst.add(pending.len()), esc.len());
87 vec.set_len(cur + total);
88 }
89 *i += 1;
90 *start = *i;
91 return;
92 }
93
94 if byte == 0xED && *i + 2 < len && (bytes[*i + 1] & 0xF0) >= 0xA0 {
96 if *start < *i {
97 result.push_str(unsafe {
99 std::str::from_utf8_unchecked(bytes.get_unchecked(*start..*i))
100 });
101 }
102 *i += write_surrogate_escape(result, bytes, *i);
103 *start = *i;
104 return;
105 }
106 *i += 1;
107}
108
109#[inline(always)]
115fn chunk_escape_mask(chunk: &[u8; 8]) -> u64 {
116 const ONES: u64 = 0x0101_0101_0101_0101;
117 const HIGH: u64 = 0x8080_8080_8080_8080;
118 let x = u64::from_le_bytes(*chunk);
119 let lt32 = x.wrapping_sub(0x20 * ONES) & !x;
120 let eq34 = {
121 let y = x ^ (0x22 * ONES);
122 y.wrapping_sub(ONES) & !y
123 };
124 let eq92 = {
125 let y = x ^ (0x5C * ONES);
126 y.wrapping_sub(ONES) & !y
127 };
128 let eqed = {
129 let y = x ^ (0xED * ONES);
130 y.wrapping_sub(ONES) & !y
131 };
132 (lt32 | eq34 | eq92 | eqed) & HIGH
133}
134
135#[inline(always)]
143pub fn escape_json_string_simple(result: &mut String, bytes: &[u8]) {
144 let len = bytes.len();
145 let mut start = 0;
146 let mut i = 0;
147 let headroom = if len < 128 {
151 len * 5 + 16
152 } else {
153 len / 4 + 16
154 };
155 result.reserve(len + headroom);
156
157 let (chunks64, tail) = bytes.as_chunks::<64>();
158
159 let mut base = 0usize;
160 for chunk64 in chunks64 {
161 macro_rules! mask_at {
164 ($off:expr) => {
165 chunk_escape_mask((&chunk64[$off..$off + 8]).try_into().unwrap())
166 };
167 }
168 let m_0 = mask_at!(0);
169 let m_1 = mask_at!(8);
170 let m_2 = mask_at!(16);
171 let m_3 = mask_at!(24);
172 let m_4 = mask_at!(32);
173 let m_5 = mask_at!(40);
174 let m_6 = mask_at!(48);
175 let m_7 = mask_at!(56);
176 if (m_0 | m_1 | m_2 | m_3 | m_4 | m_5 | m_6 | m_7) == 0 {
177 i = base + 64;
178 } else {
179 macro_rules! dispatch {
180 ($off:expr, $mask:expr) => {
181 process_dirty_half(result, bytes, base + $off, $mask, &mut i, &mut start, len)
182 };
183 }
184 dispatch!(0, m_0);
185 dispatch!(8, m_1);
186 dispatch!(16, m_2);
187 dispatch!(24, m_3);
188 dispatch!(32, m_4);
189 dispatch!(40, m_5);
190 dispatch!(48, m_6);
191 dispatch!(56, m_7);
192 }
193 base += 64;
194 }
195
196 let (sub_chunks, _sub_tail) = tail.as_chunks::<8>();
198 for (k, sub) in sub_chunks.iter().enumerate() {
199 let mask = chunk_escape_mask(sub);
200 process_dirty_half(result, bytes, base + k * 8, mask, &mut i, &mut start, len);
201 }
202
203 while i < len {
204 process_byte(result, bytes, bytes[i], &mut i, &mut start, len);
205 }
206
207 if start < len {
208 result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..len]) });
209 }
210}
211
212#[inline(always)]
213fn process_dirty_half(
214 result: &mut String,
215 bytes: &[u8],
216 half_start: usize,
217 mask: u64,
218 i: &mut usize,
219 start: &mut usize,
220 len: usize,
221) {
222 let half_end = half_start + 8;
223 if mask == 0 {
224 *i = (*i).max(half_end);
225 return;
226 }
227 let mut m = mask & (!0u64 << ((*i - half_start) * 8));
230 if m.count_ones() == 1 {
232 *i = half_start + (m.trailing_zeros() as usize) / 8;
233 process_byte(result, bytes, bytes[*i], i, start, len);
234 *i = (*i).max(half_end);
235 return;
236 }
237 while m != 0 {
238 *i = half_start + (m.trailing_zeros() as usize) / 8;
239 process_byte(result, bytes, bytes[*i], i, start, len);
240 let consumed = *i - half_start;
242 m &= (!0u64).checked_shl((consumed as u32) * 8).unwrap_or(0);
243 }
244 *i = (*i).max(half_end);
245}
246
247pub fn escape_json_string(result: &mut String, bytes: &[u8]) {
248 escape_json_string_simple(result, bytes);
249}
250
251#[cfg(test)]
252mod tests {
253 use crate::json::escape::escape_json;
254
255 #[test]
256 fn escape_json_simple() {
257 assert_eq!(escape_json(b"Hello, World!"), "Hello, World!");
258 }
259
260 #[test]
261 fn escape_json_quotes() {
262 assert_eq!(escape_json(b"\"quoted\""), "\\\"quoted\\\"");
263 }
264
265 #[test]
266 fn escape_json_backslash() {
267 assert_eq!(escape_json(b"back\\slash"), "back\\\\slash");
268 }
269
270 #[test]
271 fn escape_json_newline() {
272 assert_eq!(escape_json(b"line\nbreak"), "line\\nbreak");
273 }
274
275 #[test]
276 fn escape_json_tab() {
277 assert_eq!(escape_json(b"tab\tcharacter"), "tab\\tcharacter");
278 }
279
280 #[test]
281 fn escape_json_unicode() {
282 assert_eq!(
283 escape_json("unicode: \u{1F609}".as_bytes()),
284 "unicode: \u{1F609}"
285 );
286 }
287
288 #[test]
289 fn escape_json_special_characters() {
290 assert_eq!(
291 escape_json(b"!@#$%^&*()_+-=[]{}|;':,.<>?/"),
292 "!@#$%^&*()_+-=[]{}|;':,.<>?/"
293 );
294 }
295
296 #[test]
297 fn escape_json_mixed_characters() {
298 assert_eq!(
299 escape_json(b"123\"\"45678901\"234567"),
300 "123\\\"\\\"45678901\\\"234567"
301 );
302 }
303
304 #[test]
308 fn escape_json_lone_surrogate() {
309 assert_eq!(escape_json(&[0xED, 0xA0, 0x80]), "\\ud800");
311 }
312
313 #[test]
314 fn escape_json_lone_surrogate_with_context() {
315 let mut input = b"abcdefg".to_vec(); input.extend_from_slice(&[0xED, 0xBF, 0xBF]); input.extend_from_slice(b"xyz");
320 assert_eq!(escape_json(&input), "abcdefg\\udfffxyz");
321 }
322
323 #[test]
324 fn escape_json_surrogate_at_chunk_boundary() {
325 let mut input = b"abcdef".to_vec(); input.extend_from_slice(&[0xED, 0xA0, 0x80]); input.extend_from_slice(b"xyz123456789");
329 let expected = "abcdef\\ud800xyz123456789";
330 assert_eq!(escape_json(&input), expected);
331 }
332
333 #[test]
334 fn escape_json_korean_passthrough() {
335 let s = "훈훈훈";
338 assert_eq!(escape_json(s.as_bytes()), s);
339 }
340}