pub fn escape_into(out: &mut String, text: &str) {
out.reserve(text.len());
for character in text.chars() {
match character {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
other if (other as u32) < 0x20 || other == '\u{7f}' => {
out.push_str("\\u");
let code = other as u32;
for shift in [12u32, 8, 4, 0] {
let digit = (code >> shift) & 0xf;
out.push(char::from_digit(digit, 16).unwrap_or('0'));
}
}
other => out.push(other),
}
}
}
pub fn escape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
escape_into(&mut out, text);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_escape_json_quote_produces() {
for (raw, escaped) in [
("", ""),
("plain", "plain"),
("a\"b", "a\\\"b"),
("a\\b", "a\\\\b"),
("a\nb", "a\\nb"),
("a\rb", "a\\rb"),
("a\tb", "a\\tb"),
("a\u{8}b", "a\\bb"),
("a\u{c}b", "a\\fb"),
("a\u{0}b", "a\\u0000b"),
("a\u{1}b", "a\\u0001b"),
("a\u{1f}b", "a\\u001fb"),
("a\u{7f}b", "a\\u007fb"),
] {
assert_eq!(escape(raw), escaped, "escaping {raw:?}");
}
}
#[test]
fn text_above_ascii_is_left_alone() {
for text in ["café", "日本語", "🛟", "\u{a0}"] {
assert_eq!(escape(text), text);
}
}
#[test]
fn appending_and_allocating_agree() {
let mut built = String::from("before:");
escape_into(&mut built, "a\"b\nc");
assert_eq!(built, format!("before:{}", escape("a\"b\nc")));
}
#[test]
fn nothing_is_escaped_that_does_not_have_to_be() {
let plain = "the quick brown fox, 0123456789 !@#$%^&*()_+-=[]{};':,./<>?";
assert_eq!(escape(plain), plain);
}
}