]>
Commit | Line | Data |
---|---|---|
c30ab7b3 SL |
1 | #![allow(dead_code)] // runtime init functions not used during testing |
2 | ||
532ac7d7 XL |
3 | use crate::ffi::OsString; |
4 | use crate::fmt; | |
60c5eb7d | 5 | use crate::os::windows::prelude::*; |
532ac7d7 | 6 | use crate::path::PathBuf; |
60c5eb7d XL |
7 | use crate::slice; |
8 | use crate::sys::c; | |
9 | use crate::sys::windows::os::current_exe; | |
10 | use crate::vec; | |
532ac7d7 | 11 | |
0731742a | 12 | use core::iter; |
c30ab7b3 | 13 | |
60c5eb7d | 14 | pub unsafe fn init(_argc: isize, _argv: *const *const u8) {} |
c30ab7b3 | 15 | |
60c5eb7d | 16 | pub unsafe fn cleanup() {} |
c30ab7b3 SL |
17 | |
18 | pub fn args() -> Args { | |
19 | unsafe { | |
0731742a | 20 | let lp_cmd_line = c::GetCommandLineW(); |
60c5eb7d XL |
21 | let parsed_args_list = parse_lp_cmd_line(lp_cmd_line as *const u16, || { |
22 | current_exe().map(PathBuf::into_os_string).unwrap_or_else(|_| OsString::new()) | |
23 | }); | |
0731742a XL |
24 | |
25 | Args { parsed_args_list: parsed_args_list.into_iter() } | |
26 | } | |
27 | } | |
28 | ||
29 | /// Implements the Windows command-line argument parsing algorithm. | |
30 | /// | |
31 | /// Microsoft's documentation for the Windows CLI argument format can be found at | |
32 | /// <https://docs.microsoft.com/en-us/previous-versions//17w5ykft(v=vs.85)>. | |
33 | /// | |
34 | /// Windows includes a function to do this in shell32.dll, | |
35 | /// but linking with that DLL causes the process to be registered as a GUI application. | |
36 | /// GUI applications add a bunch of overhead, even if no windows are drawn. See | |
37 | /// <https://randomascii.wordpress.com/2018/12/03/a-not-called-function-can-cause-a-5x-slowdown/>. | |
38 | /// | |
39 | /// This function was tested for equivalence to the shell32.dll implementation in | |
40 | /// Windows 10 Pro v1803, using an exhaustive test suite available at | |
41 | /// <https://gist.github.com/notriddle/dde431930c392e428055b2dc22e638f5> or | |
42 | /// <https://paste.gg/p/anonymous/47d6ed5f5bd549168b1c69c799825223>. | |
60c5eb7d XL |
43 | unsafe fn parse_lp_cmd_line<F: Fn() -> OsString>( |
44 | lp_cmd_line: *const u16, | |
45 | exe_name: F, | |
46 | ) -> Vec<OsString> { | |
0731742a XL |
47 | const BACKSLASH: u16 = '\\' as u16; |
48 | const QUOTE: u16 = '"' as u16; | |
49 | const TAB: u16 = '\t' as u16; | |
50 | const SPACE: u16 = ' ' as u16; | |
51 | let mut ret_val = Vec::new(); | |
52 | if lp_cmd_line.is_null() || *lp_cmd_line == 0 { | |
53 | ret_val.push(exe_name()); | |
54 | return ret_val; | |
55 | } | |
56 | let mut cmd_line = { | |
57 | let mut end = 0; | |
58 | while *lp_cmd_line.offset(end) != 0 { | |
59 | end += 1; | |
60 | } | |
61 | slice::from_raw_parts(lp_cmd_line, end as usize) | |
62 | }; | |
63 | // The executable name at the beginning is special. | |
64 | cmd_line = match cmd_line[0] { | |
65 | // The executable name ends at the next quote mark, | |
66 | // no matter what. | |
67 | QUOTE => { | |
68 | let args = { | |
69 | let mut cut = cmd_line[1..].splitn(2, |&c| c == QUOTE); | |
70 | if let Some(exe) = cut.next() { | |
71 | ret_val.push(OsString::from_wide(exe)); | |
72 | } | |
73 | cut.next() | |
74 | }; | |
75 | if let Some(args) = args { | |
76 | args | |
77 | } else { | |
78 | return ret_val; | |
79 | } | |
80 | } | |
81 | // Implement quirk: when they say whitespace here, | |
82 | // they include the entire ASCII control plane: | |
83 | // "However, if lpCmdLine starts with any amount of whitespace, CommandLineToArgvW | |
84 | // will consider the first argument to be an empty string. Excess whitespace at the | |
85 | // end of lpCmdLine is ignored." | |
532ac7d7 | 86 | 0..=SPACE => { |
0731742a XL |
87 | ret_val.push(OsString::new()); |
88 | &cmd_line[1..] | |
60c5eb7d | 89 | } |
0731742a XL |
90 | // The executable name ends at the next whitespace, |
91 | // no matter what. | |
92 | _ => { | |
93 | let args = { | |
94 | let mut cut = cmd_line.splitn(2, |&c| c > 0 && c <= SPACE); | |
95 | if let Some(exe) = cut.next() { | |
96 | ret_val.push(OsString::from_wide(exe)); | |
97 | } | |
98 | cut.next() | |
99 | }; | |
100 | if let Some(args) = args { | |
101 | args | |
102 | } else { | |
103 | return ret_val; | |
104 | } | |
105 | } | |
106 | }; | |
107 | let mut cur = Vec::new(); | |
108 | let mut in_quotes = false; | |
109 | let mut was_in_quotes = false; | |
110 | let mut backslash_count: usize = 0; | |
111 | for &c in cmd_line { | |
112 | match c { | |
113 | // backslash | |
114 | BACKSLASH => { | |
115 | backslash_count += 1; | |
116 | was_in_quotes = false; | |
60c5eb7d | 117 | } |
0731742a XL |
118 | QUOTE if backslash_count % 2 == 0 => { |
119 | cur.extend(iter::repeat(b'\\' as u16).take(backslash_count / 2)); | |
120 | backslash_count = 0; | |
121 | if was_in_quotes { | |
122 | cur.push('"' as u16); | |
123 | was_in_quotes = false; | |
124 | } else { | |
125 | was_in_quotes = in_quotes; | |
126 | in_quotes = !in_quotes; | |
127 | } | |
128 | } | |
129 | QUOTE if backslash_count % 2 != 0 => { | |
130 | cur.extend(iter::repeat(b'\\' as u16).take(backslash_count / 2)); | |
131 | backslash_count = 0; | |
132 | was_in_quotes = false; | |
133 | cur.push(b'"' as u16); | |
134 | } | |
135 | SPACE | TAB if !in_quotes => { | |
136 | cur.extend(iter::repeat(b'\\' as u16).take(backslash_count)); | |
137 | if !cur.is_empty() || was_in_quotes { | |
138 | ret_val.push(OsString::from_wide(&cur[..])); | |
139 | cur.truncate(0); | |
140 | } | |
141 | backslash_count = 0; | |
142 | was_in_quotes = false; | |
143 | } | |
144 | _ => { | |
145 | cur.extend(iter::repeat(b'\\' as u16).take(backslash_count)); | |
146 | backslash_count = 0; | |
147 | was_in_quotes = false; | |
148 | cur.push(c); | |
149 | } | |
150 | } | |
151 | } | |
152 | cur.extend(iter::repeat(b'\\' as u16).take(backslash_count)); | |
153 | // include empty quoted strings at the end of the arguments list | |
154 | if !cur.is_empty() || was_in_quotes || in_quotes { | |
155 | ret_val.push(OsString::from_wide(&cur[..])); | |
c30ab7b3 | 156 | } |
0731742a | 157 | ret_val |
c30ab7b3 SL |
158 | } |
159 | ||
160 | pub struct Args { | |
0731742a | 161 | parsed_args_list: vec::IntoIter<OsString>, |
c30ab7b3 SL |
162 | } |
163 | ||
041b39d2 XL |
164 | pub struct ArgsInnerDebug<'a> { |
165 | args: &'a Args, | |
166 | } | |
167 | ||
168 | impl<'a> fmt::Debug for ArgsInnerDebug<'a> { | |
532ac7d7 | 169 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
0731742a | 170 | self.args.parsed_args_list.as_slice().fmt(f) |
041b39d2 XL |
171 | } |
172 | } | |
173 | ||
174 | impl Args { | |
532ac7d7 | 175 | pub fn inner_debug(&self) -> ArgsInnerDebug<'_> { |
60c5eb7d | 176 | ArgsInnerDebug { args: self } |
041b39d2 XL |
177 | } |
178 | } | |
179 | ||
c30ab7b3 SL |
180 | impl Iterator for Args { |
181 | type Item = OsString; | |
60c5eb7d XL |
182 | fn next(&mut self) -> Option<OsString> { |
183 | self.parsed_args_list.next() | |
184 | } | |
185 | fn size_hint(&self) -> (usize, Option<usize>) { | |
186 | self.parsed_args_list.size_hint() | |
187 | } | |
c30ab7b3 SL |
188 | } |
189 | ||
190 | impl DoubleEndedIterator for Args { | |
60c5eb7d XL |
191 | fn next_back(&mut self) -> Option<OsString> { |
192 | self.parsed_args_list.next_back() | |
193 | } | |
c30ab7b3 SL |
194 | } |
195 | ||
196 | impl ExactSizeIterator for Args { | |
60c5eb7d XL |
197 | fn len(&self) -> usize { |
198 | self.parsed_args_list.len() | |
199 | } | |
c30ab7b3 SL |
200 | } |
201 | ||
0731742a XL |
202 | #[cfg(test)] |
203 | mod tests { | |
532ac7d7 | 204 | use crate::ffi::OsString; |
60c5eb7d | 205 | use crate::sys::windows::args::*; |
0731742a XL |
206 | |
207 | fn chk(string: &str, parts: &[&str]) { | |
208 | let mut wide: Vec<u16> = OsString::from(string).encode_wide().collect(); | |
209 | wide.push(0); | |
210 | let parsed = unsafe { | |
211 | parse_lp_cmd_line(wide.as_ptr() as *const u16, || OsString::from("TEST.EXE")) | |
212 | }; | |
213 | let expected: Vec<OsString> = parts.iter().map(|k| OsString::from(k)).collect(); | |
214 | assert_eq!(parsed.as_slice(), expected.as_slice()); | |
215 | } | |
216 | ||
217 | #[test] | |
218 | fn empty() { | |
219 | chk("", &["TEST.EXE"]); | |
220 | chk("\0", &["TEST.EXE"]); | |
221 | } | |
222 | ||
223 | #[test] | |
224 | fn single_words() { | |
225 | chk("EXE one_word", &["EXE", "one_word"]); | |
226 | chk("EXE a", &["EXE", "a"]); | |
227 | chk("EXE 😅", &["EXE", "😅"]); | |
228 | chk("EXE 😅🤦", &["EXE", "😅🤦"]); | |
229 | } | |
230 | ||
231 | #[test] | |
232 | fn official_examples() { | |
233 | chk(r#"EXE "abc" d e"#, &["EXE", "abc", "d", "e"]); | |
234 | chk(r#"EXE a\\\b d"e f"g h"#, &["EXE", r#"a\\\b"#, "de fg", "h"]); | |
235 | chk(r#"EXE a\\\"b c d"#, &["EXE", r#"a\"b"#, "c", "d"]); | |
236 | chk(r#"EXE a\\\\"b c" d e"#, &["EXE", r#"a\\b c"#, "d", "e"]); | |
237 | } | |
238 | ||
239 | #[test] | |
240 | fn whitespace_behavior() { | |
241 | chk(r#" test"#, &["", "test"]); | |
242 | chk(r#" test"#, &["", "test"]); | |
243 | chk(r#" test test2"#, &["", "test", "test2"]); | |
244 | chk(r#" test test2"#, &["", "test", "test2"]); | |
245 | chk(r#"test test2 "#, &["test", "test2"]); | |
246 | chk(r#"test test2 "#, &["test", "test2"]); | |
247 | chk(r#"test "#, &["test"]); | |
248 | } | |
249 | ||
250 | #[test] | |
251 | fn genius_quotes() { | |
252 | chk(r#"EXE "" """#, &["EXE", "", ""]); | |
253 | chk(r#"EXE "" """"#, &["EXE", "", "\""]); | |
254 | chk( | |
255 | r#"EXE "this is """all""" in the same argument""#, | |
60c5eb7d | 256 | &["EXE", "this is \"all\" in the same argument"], |
0731742a XL |
257 | ); |
258 | chk(r#"EXE "a"""#, &["EXE", "a\""]); | |
259 | chk(r#"EXE "a"" a"#, &["EXE", "a\"", "a"]); | |
260 | // quotes cannot be escaped in command names | |
261 | chk(r#""EXE" check"#, &["EXE", "check"]); | |
262 | chk(r#""EXE check""#, &["EXE check"]); | |
263 | chk(r#""EXE """for""" check"#, &["EXE ", r#"for""#, "check"]); | |
60c5eb7d | 264 | chk(r#""EXE \"for\" check"#, &[r#"EXE \"#, r#"for""#, "check"]); |
c30ab7b3 SL |
265 | } |
266 | } |