1use f00_core::QuotingStyle;
4
5pub fn quote_name(name: &str, style: QuotingStyle, hide_control: bool) -> String {
10 match style {
11 QuotingStyle::Literal => {
12 if hide_control {
13 hide_control_chars(name)
14 } else {
15 name.to_string()
16 }
17 }
18 QuotingStyle::Locale => {
19 if needs_shell_escape(name) {
22 shell_escape_quote(name, false)
23 } else if hide_control {
24 hide_control_chars(name)
25 } else {
26 name.to_string()
27 }
28 }
29 QuotingStyle::Shell => shell_quote(name, false),
30 QuotingStyle::ShellAlways => shell_quote(name, true),
31 QuotingStyle::ShellEscape => shell_escape_quote(name, false),
32 QuotingStyle::ShellEscapeAlways => shell_escape_quote(name, true),
33 QuotingStyle::C => c_quote(name, true),
34 QuotingStyle::Escape => c_quote(name, false),
35 }
36}
37
38fn hide_control_chars(name: &str) -> String {
39 name.chars()
40 .map(|c| if is_nongraphic(c) { '?' } else { c })
41 .collect()
42}
43
44fn is_nongraphic(c: char) -> bool {
45 c.is_control() || c == '\u{7f}'
47}
48
49fn is_shell_special(c: char) -> bool {
50 matches!(
51 c,
52 ' ' | '\t'
53 | '\n'
54 | '\''
55 | '"'
56 | '\\'
57 | '|'
58 | '&'
59 | ';'
60 | '('
61 | ')'
62 | '<'
63 | '>'
64 | '$'
65 | '`'
66 | '!'
67 | '*'
68 | '?'
69 | '['
70 | ']'
71 | '#'
72 | '~'
73 | '='
74 | '%'
75 | '{'
76 | '}'
77 ) || c == '\0'
78}
79
80fn needs_shell_quote(name: &str) -> bool {
81 name.is_empty() || name.chars().any(is_shell_special) || name.starts_with('-')
82}
83
84fn needs_shell_escape(name: &str) -> bool {
85 name.is_empty()
86 || name
87 .chars()
88 .any(|c| is_shell_special(c) || is_nongraphic(c))
89}
90
91fn shell_quote(name: &str, always: bool) -> String {
93 if !always && !needs_shell_quote(name) {
94 return name.to_string();
95 }
96 let mut out = String::with_capacity(name.len() + 2);
98 out.push('\'');
99 for c in name.chars() {
100 if c == '\'' {
101 out.push_str("'\\''");
102 } else {
103 out.push(c);
104 }
105 }
106 out.push('\'');
107 out
108}
109
110fn shell_escape_quote(name: &str, always: bool) -> String {
112 let has_nongraphic = name.chars().any(is_nongraphic);
113 if has_nongraphic || (always && needs_shell_escape(name)) {
114 return dollar_quote(name);
115 }
116 if always || needs_shell_quote(name) {
117 return shell_quote(name, true);
118 }
119 name.to_string()
120}
121
122fn dollar_quote(name: &str) -> String {
123 let mut out = String::from("$'");
124 for c in name.chars() {
125 push_c_escape(&mut out, c, true);
126 }
127 out.push('\'');
128 out
129}
130
131fn c_quote(name: &str, quoted: bool) -> String {
133 let mut out = String::with_capacity(name.len() + 2);
134 if quoted {
135 out.push('"');
136 }
137 for c in name.chars() {
138 push_c_escape(&mut out, c, quoted);
139 }
140 if quoted {
141 out.push('"');
142 }
143 out
144}
145
146fn push_c_escape(out: &mut String, c: char, in_double_quotes: bool) {
147 match c {
148 '\\' => out.push_str("\\\\"),
149 '\n' => out.push_str("\\n"),
150 '\t' => out.push_str("\\t"),
151 '\r' => out.push_str("\\r"),
152 '\x08' => out.push_str("\\b"),
153 '\x0c' => out.push_str("\\f"),
154 '\x0b' => out.push_str("\\v"),
155 '\0' => out.push_str("\\0"),
156 ' ' if !in_double_quotes => out.push_str("\\ "),
158 '"' if in_double_quotes => out.push_str("\\\""),
159 '\'' if !in_double_quotes => {
160 out.push_str("\\'");
162 }
163 c if is_nongraphic(c) => {
164 let u = c as u32;
165 if u <= 0xff {
166 out.push_str(&format!("\\{u:03o}"));
167 } else {
168 out.push_str(&format!("\\u{u:04x}"));
169 }
170 }
171 c => out.push(c),
172 }
173}
174
175pub fn display_name(name: &str, style: QuotingStyle, hide_control: bool) -> String {
177 quote_name(name, style, hide_control)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn literal_passthrough() {
186 assert_eq!(quote_name("hello", QuotingStyle::Literal, false), "hello");
187 }
188
189 #[test]
190 fn hide_control_replaces() {
191 assert_eq!(quote_name("a\nb", QuotingStyle::Literal, true), "a?b");
192 }
193
194 #[test]
195 fn escape_style_c_escapes() {
196 let q = quote_name("a\nb", QuotingStyle::Escape, false);
197 assert_eq!(q, "a\\nb");
198 let q2 = quote_name("x y", QuotingStyle::Escape, false);
199 assert_eq!(q2, "x\\ y");
200 }
201
202 #[test]
203 fn c_style_double_quotes() {
204 let q = quote_name("hello", QuotingStyle::C, false);
205 assert_eq!(q, "\"hello\"");
206 let q2 = quote_name("a\"b", QuotingStyle::C, false);
207 assert_eq!(q2, "\"a\\\"b\"");
208 let q3 = quote_name("a\tb", QuotingStyle::C, false);
209 assert_eq!(q3, "\"a\\tb\"");
210 }
211
212 #[test]
213 fn shell_quotes_when_needed() {
214 assert_eq!(quote_name("plain", QuotingStyle::Shell, false), "plain");
215 let q = quote_name("a b", QuotingStyle::Shell, false);
216 assert_eq!(q, "'a b'");
217 let q2 = quote_name("it's", QuotingStyle::Shell, false);
218 assert!(q2.starts_with('\''));
219 assert!(q2.contains("\\'"));
220 }
221
222 #[test]
223 fn shell_always_quotes() {
224 assert_eq!(
225 quote_name("plain", QuotingStyle::ShellAlways, false),
226 "'plain'"
227 );
228 }
229
230 #[test]
231 fn shell_escape_nongraphic() {
232 let q = quote_name("a\nb", QuotingStyle::ShellEscape, false);
233 assert!(q.starts_with("$'"), "{q}");
234 assert!(q.contains("\\n"), "{q}");
235 }
236}