use std::borrow::Cow;
use std::fmt::Write as _;
pub fn must_escape(c: char) -> bool {
c.is_control()
|| matches!(
c,
'\u{00ad}'
| '\u{0600}'..='\u{0605}'
| '\u{061c}'
| '\u{06dd}'
| '\u{070f}'
| '\u{0890}'..='\u{0891}'
| '\u{08e2}'
| '\u{180e}'
| '\u{200b}'..='\u{200f}'
| '\u{2028}'..='\u{202e}'
| '\u{2060}'..='\u{2064}'
| '\u{2066}'..='\u{206f}'
| '\u{feff}'
| '\u{fff9}'..='\u{fffb}'
| '\u{110bd}'
| '\u{110cd}'
| '\u{13430}'..='\u{1343f}'
| '\u{1bca0}'..='\u{1bca3}'
| '\u{1d173}'..='\u{1d17a}'
| '\u{e0001}'
| '\u{e0020}'..='\u{e007f}'
)
}
pub fn push_unicode_escape(out: &mut String, c: char) {
for unit in c.encode_utf16(&mut [0; 2]) {
let _ = write!(out, "\\u{unit:04x}");
}
}
pub fn quote_into(out: &mut String, s: &str) {
out.reserve(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
c if must_escape(c) => push_unicode_escape(out, c),
c => out.push(c),
}
}
out.push('"');
}
pub fn quoted(s: &str) -> String {
let mut out = String::new();
quote_into(&mut out, s);
out
}
pub fn value(s: &str) -> Cow<'_, str> {
let plain = !s.is_empty() && !s.chars().any(|c| c.is_whitespace() || must_escape(c) || matches!(c, '"' | '\\' | '='));
if plain { Cow::Borrowed(s) } else { Cow::Owned(quoted(s)) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quoted_strings_stay_on_one_line_and_parse_as_json() {
let raw = "a\"b\\c\nd\te\r\u{1b}\u{7f}\u{85}é😀";
let q = quoted(raw);
assert_eq!(q, r#""a\"b\\c\nd\te\r\u001b\u007f\u0085é😀""#);
assert_eq!(serde_json::from_str::<String>(&q).unwrap(), raw);
}
#[test]
fn characters_that_disguise_a_row_are_escaped() {
let raw = "pay\u{202e}lmth.exe\u{2066}x\u{2069}\u{200b}\u{200d}\u{feff}\u{2028}\u{e0041}";
let q = quoted(raw);
assert_eq!(q, r#""pay\u202elmth.exe\u2066x\u2069\u200b\u200d\ufeff\u2028\udb40\udc41""#);
assert_eq!(serde_json::from_str::<String>(&q).unwrap(), raw);
assert!(q.is_ascii());
assert_eq!(quoted("登录 مرحبا ñ"), "\"登录 مرحبا ñ\"");
for c in ['\u{2066}', '\u{2069}', '\u{202a}', '\u{200e}', '\u{061c}', '\u{00ad}', '\u{2060}'] {
assert!(must_escape(c), "{:04x}", u32::from(c));
}
for c in ['a', ' ', '\u{a0}', '\u{2010}', '\u{2030}', '中'] {
assert!(!must_escape(c), "{:04x}", u32::from(c));
}
}
#[test]
fn values_are_quoted_only_when_ambiguous() {
assert_eq!(value("com.demo.Main"), "com.demo.Main");
assert_eq!(value("signature|privileged"), "signature|privileged");
assert_eq!(value("1.0 beta"), r#""1.0 beta""#);
assert_eq!(value("a=b"), r#""a=b""#);
assert_eq!(value(""), r#""""#);
assert_eq!(value("a\u{200b}b"), r#""a\u200bb""#);
}
}