#[must_use]
pub fn escape(s: &str) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn escape_passes_ascii_unchanged() {
assert_eq!(escape("hello world"), "hello world");
assert_eq!(escape(""), "");
}
#[test]
fn escape_doubles_backslash() {
assert_eq!(escape(r"path\to\file"), r"path\\to\\file");
}
#[test]
fn escape_escapes_double_quote() {
assert_eq!(escape("say \"hi\""), "say \\\"hi\\\"");
}
#[test]
fn escape_handles_common_whitespace_controls() {
assert_eq!(escape("a\nb"), "a\\nb");
assert_eq!(escape("a\rb"), "a\\rb");
assert_eq!(escape("a\tb"), "a\\tb");
}
#[test]
fn escape_emits_uxxxx_for_other_control_chars() {
assert_eq!(escape("\x01"), "\\u0001");
assert_eq!(escape("\x07"), "\\u0007");
assert_eq!(escape("\x1f"), "\\u001f");
}
#[test]
fn escape_passes_non_ascii_utf8_unchanged() {
assert_eq!(escape("ναί"), "ναί");
assert_eq!(escape("好"), "好");
assert_eq!(escape("🦀"), "🦀");
}
#[test]
fn escape_output_round_trips_through_serde_json() {
let input = "quote\" backslash\\ newline\n tab\t ctrl\x01 utf8\u{1F980} end";
let json_body = format!("\"{}\"", escape(input));
let parsed: String = serde_json::from_str(&json_body).expect("must parse");
assert_eq!(parsed, input);
}
}