1use std::borrow::Cow;
12use std::fmt::Write as _;
13
14pub fn must_escape(c: char) -> bool {
19 c.is_control()
20 || matches!(
21 c,
22 '\u{00ad}'
23 | '\u{0600}'..='\u{0605}'
24 | '\u{061c}'
25 | '\u{06dd}'
26 | '\u{070f}'
27 | '\u{0890}'..='\u{0891}'
28 | '\u{08e2}'
29 | '\u{180e}'
30 | '\u{200b}'..='\u{200f}'
31 | '\u{2028}'..='\u{202e}'
32 | '\u{2060}'..='\u{2064}'
33 | '\u{2066}'..='\u{206f}'
34 | '\u{feff}'
35 | '\u{fff9}'..='\u{fffb}'
36 | '\u{110bd}'
37 | '\u{110cd}'
38 | '\u{13430}'..='\u{1343f}'
39 | '\u{1bca0}'..='\u{1bca3}'
40 | '\u{1d173}'..='\u{1d17a}'
41 | '\u{e0001}'
42 | '\u{e0020}'..='\u{e007f}'
43 )
44}
45
46pub fn push_unicode_escape(out: &mut String, c: char) {
49 for unit in c.encode_utf16(&mut [0; 2]) {
50 let _ = write!(out, "\\u{unit:04x}");
52 }
53}
54
55pub fn quote_into(out: &mut String, s: &str) {
59 out.reserve(s.len() + 2);
60 out.push('"');
61 for c in s.chars() {
62 match c {
63 '"' => out.push_str("\\\""),
64 '\\' => out.push_str("\\\\"),
65 '\n' => out.push_str("\\n"),
66 '\t' => out.push_str("\\t"),
67 '\r' => out.push_str("\\r"),
68 c if must_escape(c) => push_unicode_escape(out, c),
69 c => out.push(c),
70 }
71 }
72 out.push('"');
73}
74
75pub fn quoted(s: &str) -> String {
77 let mut out = String::new();
78 quote_into(&mut out, s);
79 out
80}
81
82pub fn value(s: &str) -> Cow<'_, str> {
87 let plain = !s.is_empty() && !s.chars().any(|c| c.is_whitespace() || must_escape(c) || matches!(c, '"' | '\\' | '='));
88 if plain { Cow::Borrowed(s) } else { Cow::Owned(quoted(s)) }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn quoted_strings_stay_on_one_line_and_parse_as_json() {
97 let raw = "a\"b\\c\nd\te\r\u{1b}\u{7f}\u{85}é😀";
98 let q = quoted(raw);
99 assert_eq!(q, r#""a\"b\\c\nd\te\r\u001b\u007f\u0085é😀""#);
100 assert_eq!(serde_json::from_str::<String>(&q).unwrap(), raw);
101 }
102
103 #[test]
104 fn characters_that_disguise_a_row_are_escaped() {
105 let raw = "pay\u{202e}lmth.exe\u{2066}x\u{2069}\u{200b}\u{200d}\u{feff}\u{2028}\u{e0041}";
108 let q = quoted(raw);
109 assert_eq!(q, r#""pay\u202elmth.exe\u2066x\u2069\u200b\u200d\ufeff\u2028\udb40\udc41""#);
110 assert_eq!(serde_json::from_str::<String>(&q).unwrap(), raw);
111 assert!(q.is_ascii());
112 assert_eq!(quoted("登录 مرحبا ñ"), "\"登录 مرحبا ñ\"");
114 for c in ['\u{2066}', '\u{2069}', '\u{202a}', '\u{200e}', '\u{061c}', '\u{00ad}', '\u{2060}'] {
115 assert!(must_escape(c), "{:04x}", u32::from(c));
116 }
117 for c in ['a', ' ', '\u{a0}', '\u{2010}', '\u{2030}', '中'] {
118 assert!(!must_escape(c), "{:04x}", u32::from(c));
119 }
120 }
121
122 #[test]
123 fn values_are_quoted_only_when_ambiguous() {
124 assert_eq!(value("com.demo.Main"), "com.demo.Main");
125 assert_eq!(value("signature|privileged"), "signature|privileged");
126 assert_eq!(value("1.0 beta"), r#""1.0 beta""#);
127 assert_eq!(value("a=b"), r#""a=b""#);
128 assert_eq!(value(""), r#""""#);
129 assert_eq!(value("a\u{200b}b"), r#""a\u200bb""#);
130 }
131}