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("\\\""),
157 '\'' if !in_double_quotes => {
158 out.push_str("\\'");
160 }
161 c if is_nongraphic(c) => {
162 let u = c as u32;
163 if u <= 0xff {
164 out.push_str(&format!("\\{u:03o}"));
165 } else {
166 out.push_str(&format!("\\u{u:04x}"));
167 }
168 }
169 c => out.push(c),
170 }
171}
172
173pub fn display_name(name: &str, style: QuotingStyle, hide_control: bool) -> String {
175 quote_name(name, style, hide_control)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn literal_passthrough() {
184 assert_eq!(quote_name("hello", QuotingStyle::Literal, false), "hello");
185 }
186
187 #[test]
188 fn hide_control_replaces() {
189 assert_eq!(quote_name("a\nb", QuotingStyle::Literal, true), "a?b");
190 }
191
192 #[test]
193 fn escape_style_c_escapes() {
194 let q = quote_name("a\nb", QuotingStyle::Escape, false);
195 assert_eq!(q, "a\\nb");
196 let q2 = quote_name("x y", QuotingStyle::Escape, false);
197 assert_eq!(q2, "x y");
198 }
199
200 #[test]
201 fn c_style_double_quotes() {
202 let q = quote_name("hello", QuotingStyle::C, false);
203 assert_eq!(q, "\"hello\"");
204 let q2 = quote_name("a\"b", QuotingStyle::C, false);
205 assert_eq!(q2, "\"a\\\"b\"");
206 let q3 = quote_name("a\tb", QuotingStyle::C, false);
207 assert_eq!(q3, "\"a\\tb\"");
208 }
209
210 #[test]
211 fn shell_quotes_when_needed() {
212 assert_eq!(quote_name("plain", QuotingStyle::Shell, false), "plain");
213 let q = quote_name("a b", QuotingStyle::Shell, false);
214 assert_eq!(q, "'a b'");
215 let q2 = quote_name("it's", QuotingStyle::Shell, false);
216 assert!(q2.starts_with('\''));
217 assert!(q2.contains("\\'"));
218 }
219
220 #[test]
221 fn shell_always_quotes() {
222 assert_eq!(
223 quote_name("plain", QuotingStyle::ShellAlways, false),
224 "'plain'"
225 );
226 }
227
228 #[test]
229 fn shell_escape_nongraphic() {
230 let q = quote_name("a\nb", QuotingStyle::ShellEscape, false);
231 assert!(q.starts_with("$'"), "{q}");
232 assert!(q.contains("\\n"), "{q}");
233 }
234}