1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/// The method is not meant to be used by other crates! It may change
/// or be removed in the future, with no regard for backwards compatibility.
#[allow(clippy::missing_panics_doc)]
pub fn escape_string(input: &str, out: &mut String) {
    for ch in input.chars() {
        match ch {
            '\\' => out.push_str("\\\\"),
            '"' => out.push_str("\\\""),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            '\t' => out.push_str("\\t"),
            '\x20'..='\x7e' => out.push(ch),
            _ => {
                let mut buf = [0; 2];
                for &mut code_unit in ch.encode_utf16(&mut buf) {
                    out.push_str("\\u");
                    out.push(char::from_digit(u32::from(code_unit) / 0x1000, 16).unwrap());
                    out.push(char::from_digit(u32::from(code_unit) % 0x1000 / 0x100, 16).unwrap());
                    out.push(char::from_digit(u32::from(code_unit) % 0x100 / 0x10, 16).unwrap());
                    out.push(char::from_digit(u32::from(code_unit) % 0x10, 16).unwrap());
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[track_caller]
    fn test_json_str(input: &str, expected: &str) {
        let mut out = String::new();
        escape_string(input, &mut out);
        assert_eq!(out, expected);
    }

    #[test]
    fn json_string_encoding() {
        test_json_str("abc", "abc");
        test_json_str("fancy string\n", "fancy string\\n");
        test_json_str("\n\t\r\0\\\'\"", "\\n\\t\\r\\u0000\\\\'\\\"");
        test_json_str("\u{1d54a}", "\\ud835\\udd4a");
    }
}