1use crate::config::DroppedFileQuoteStyle;
6use std::path::Path;
7
8const SHELL_SPECIAL_CHARS: &[char] = &[
11 ' ', '\t', '\n', '\r', '\'', '"', '`', '$', '!', '&', '|', ';', '(', ')', '{', '}', '[', ']', '<', '>', '*', '?', '\\', '#', '~', '^', ];
19
20fn needs_quoting(path: &str) -> bool {
22 path.chars().any(|c| SHELL_SPECIAL_CHARS.contains(&c))
23}
24
25fn quote_single(path: &str) -> String {
34 let escaped = path.replace('\'', "'\\''");
37 format!("'{}'", escaped)
38}
39
40fn quote_double(path: &str) -> String {
45 let mut result = String::with_capacity(path.len() + 10);
47 result.push('"');
48
49 for c in path.chars() {
50 match c {
51 '$' | '`' | '\\' | '"' | '!' => {
52 result.push('\\');
53 result.push(c);
54 }
55 _ => result.push(c),
56 }
57 }
58
59 result.push('"');
60 result
61}
62
63fn quote_backslash(path: &str) -> String {
68 if !needs_quoting(path) {
69 return path.to_string();
70 }
71
72 let mut result = String::with_capacity(path.len() * 2);
73
74 for c in path.chars() {
75 if SHELL_SPECIAL_CHARS.contains(&c) {
76 result.push('\\');
77 }
78 result.push(c);
79 }
80
81 result
82}
83
84pub fn quote_path(path: &Path, style: DroppedFileQuoteStyle) -> String {
86 let path_str = path.to_string_lossy();
87
88 match style {
89 DroppedFileQuoteStyle::SingleQuotes => quote_single(&path_str),
90 DroppedFileQuoteStyle::DoubleQuotes => quote_double(&path_str),
91 DroppedFileQuoteStyle::Backslash => quote_backslash(&path_str),
92 DroppedFileQuoteStyle::None => path_str.into_owned(),
93 }
94}
95
96pub fn quote_paths(paths: &[&Path], style: DroppedFileQuoteStyle) -> String {
98 paths
99 .iter()
100 .map(|p| quote_path(p, style))
101 .collect::<Vec<_>>()
102 .join(" ")
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use std::path::Path;
109
110 #[test]
111 fn test_simple_path_always_quoted() {
112 let path = Path::new("/usr/local/bin/program");
113 assert_eq!(
115 quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
116 "'/usr/local/bin/program'"
117 );
118 assert_eq!(
119 quote_path(path, DroppedFileQuoteStyle::DoubleQuotes),
120 "\"/usr/local/bin/program\""
121 );
122 assert_eq!(
124 quote_path(path, DroppedFileQuoteStyle::Backslash),
125 "/usr/local/bin/program"
126 );
127 assert_eq!(
129 quote_path(path, DroppedFileQuoteStyle::None),
130 "/usr/local/bin/program"
131 );
132 }
133
134 #[test]
135 fn test_path_with_spaces() {
136 let path = Path::new("/path/to/file with spaces.txt");
137 assert_eq!(
138 quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
139 "'/path/to/file with spaces.txt'"
140 );
141 assert_eq!(
142 quote_path(path, DroppedFileQuoteStyle::DoubleQuotes),
143 "\"/path/to/file with spaces.txt\""
144 );
145 assert_eq!(
146 quote_path(path, DroppedFileQuoteStyle::Backslash),
147 "/path/to/file\\ with\\ spaces.txt"
148 );
149 assert_eq!(
150 quote_path(path, DroppedFileQuoteStyle::None),
151 "/path/to/file with spaces.txt"
152 );
153 }
154
155 #[test]
156 fn test_path_with_single_quote() {
157 let path = Path::new("/path/to/it's a file.txt");
158 assert_eq!(
159 quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
160 "'/path/to/it'\\''s a file.txt'"
161 );
162 }
163
164 #[test]
165 fn test_path_with_dollar_sign() {
166 let path = Path::new("/path/to/$HOME/file.txt");
167 assert_eq!(
168 quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
169 "'/path/to/$HOME/file.txt'"
170 );
171 assert_eq!(
172 quote_path(path, DroppedFileQuoteStyle::DoubleQuotes),
173 "\"/path/to/\\$HOME/file.txt\""
174 );
175 assert_eq!(
176 quote_path(path, DroppedFileQuoteStyle::Backslash),
177 "/path/to/\\$HOME/file.txt"
178 );
179 }
180
181 #[test]
182 fn test_path_with_glob_chars() {
183 let path = Path::new("/path/to/*.txt");
184 assert_eq!(
185 quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
186 "'/path/to/*.txt'"
187 );
188 assert_eq!(
189 quote_path(path, DroppedFileQuoteStyle::Backslash),
190 "/path/to/\\*.txt"
191 );
192 }
193
194 #[test]
195 fn test_multiple_paths() {
196 let paths: Vec<&Path> = vec![
197 Path::new("/simple/path"),
198 Path::new("/path with spaces"),
199 Path::new("/path/with$dollar"),
200 ];
201 let result = quote_paths(&paths, DroppedFileQuoteStyle::SingleQuotes);
202 assert_eq!(
204 result,
205 "'/simple/path' '/path with spaces' '/path/with$dollar'"
206 );
207 }
208}