use std::fmt;
use crate::engine::encode_loop;
pub fn for_json(input: &str) -> String {
let mut out = String::with_capacity(input.len());
write_json(&mut out, input).expect("writing to string cannot fail");
out
}
pub fn write_json<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
encode_loop(out, input, needs_json_encoding, write_json_encoded)
}
fn needs_json_encoding(c: char) -> bool {
matches!(
c,
'\x00'..='\x1F' | '"' | '\\' | '/' | '\u{2028}' | '\u{2029}'
)
}
fn write_json_encoded<W: fmt::Write>(out: &mut W, c: char, _next: Option<char>) -> fmt::Result {
match c {
'\x08' => out.write_str("\\b"),
'\t' => out.write_str("\\t"),
'\n' => out.write_str("\\n"),
'\x0C' => out.write_str("\\f"),
'\r' => out.write_str("\\r"),
'"' => out.write_str("\\\""),
'\\' => out.write_str("\\\\"),
'/' => out.write_str("\\/"),
'\u{2028}' => out.write_str("\\u2028"),
'\u{2029}' => out.write_str("\\u2029"),
c => write!(out, "\\u{:04x}", c as u32),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn passthrough() {
assert_eq!(for_json("hello world"), "hello world");
assert_eq!(for_json(""), "");
assert_eq!(for_json("café"), "café");
assert_eq!(for_json("日本語"), "日本語");
assert_eq!(for_json("😀"), "😀");
}
#[test]
fn single_quotes_not_escaped() {
assert_eq!(for_json("it's"), "it's");
assert_eq!(for_json("'quoted'"), "'quoted'");
}
#[test]
fn double_quotes_escaped() {
assert_eq!(for_json(r#"a"b"#), r#"a\"b"#);
assert_eq!(for_json(r#""hello""#), r#"\"hello\""#);
}
#[test]
fn backslash() {
assert_eq!(for_json(r"a\b"), r"a\\b");
assert_eq!(for_json(r"\\"), r"\\\\");
}
#[test]
fn named_escapes() {
assert_eq!(for_json("\x08"), "\\b");
assert_eq!(for_json("\t"), "\\t");
assert_eq!(for_json("\n"), "\\n");
assert_eq!(for_json("\x0C"), "\\f");
assert_eq!(for_json("\r"), "\\r");
}
#[test]
fn control_chars_use_unicode_escapes() {
assert_eq!(for_json("\x00"), "\\u0000");
assert_eq!(for_json("\x01"), "\\u0001");
assert_eq!(for_json("\x07"), "\\u0007");
assert_eq!(for_json("\x0B"), "\\u000b");
assert_eq!(for_json("\x0E"), "\\u000e");
assert_eq!(for_json("\x1F"), "\\u001f");
}
#[test]
fn line_separators() {
assert_eq!(for_json("\u{2028}"), "\\u2028");
assert_eq!(for_json("\u{2029}"), "\\u2029");
assert_eq!(for_json("a\u{2028}b\u{2029}c"), "a\\u2028b\\u2029c");
}
#[test]
fn forward_slash_escaped() {
assert_eq!(for_json("/"), "\\/");
assert_eq!(for_json("a/b"), "a\\/b");
assert_eq!(for_json("https://example.com"), "https:\\/\\/example.com");
}
#[test]
fn ampersand_not_escaped() {
assert_eq!(for_json("a&b"), "a&b");
}
#[test]
fn script_tag_breakout_prevented() {
assert_eq!(for_json("</script>"), "<\\/script>");
assert_eq!(
for_json("</script><script>alert(1)//"),
"<\\/script><script>alert(1)\\/\\/"
);
}
#[test]
fn mixed_input() {
assert_eq!(
for_json("he said \"hello\"\nnew line"),
"he said \\\"hello\\\"\\nnew line"
);
}
#[test]
fn writer_matches_string() {
let input = "test\x00\"\\\n\u{2028}café";
let string_result = for_json(input);
let mut writer_result = String::new();
write_json(&mut writer_result, input).unwrap();
assert_eq!(string_result, writer_result);
}
#[test]
fn differs_from_js_source_on_single_quotes() {
assert_eq!(for_json("a'b"), "a'b");
assert_ne!(for_json("a'b"), crate::for_javascript_source("a'b"));
}
#[test]
fn differs_from_js_source_on_control_format() {
assert_eq!(for_json("\x01"), "\\u0001");
assert_eq!(crate::for_javascript_source("\x01"), "\\x01");
}
}