Skip to main content

std\sys\args/
windows.rs

1//! The Windows command line is just a string
2//! <https://docs.microsoft.com/en-us/archive/blogs/larryosterman/the-windows-command-line-is-just-a-string>
3//!
4//! This module implements the parsing necessary to turn that string into a list of arguments.
5
6#[cfg(test)]
7mod tests;
8
9pub use super::common::Args;
10use crate::ffi::{OsStr, OsString};
11use crate::num::NonZero;
12use crate::os::windows::prelude::*;
13use crate::path::{Path, PathBuf};
14use crate::sys::helpers::WStrUnits;
15use crate::sys::pal::{ensure_no_nuls, fill_utf16_buf};
16use crate::sys::path::get_long_path;
17use crate::sys::paths::current_exe;
18use crate::sys::{AsInner, c, to_u16s};
19use crate::{io, iter, ptr};
20
21pub fn args() -> Args {
22    // SAFETY: `GetCommandLineW` returns a pointer to a null terminated UTF-16
23    // string so it's safe for `WStrUnits` to use.
24    unsafe {
25        let lp_cmd_line = c::GetCommandLineW();
26        let parsed_args_list = parse_lp_cmd_line(WStrUnits::new(lp_cmd_line), || {
27            current_exe().map(PathBuf::into_os_string).unwrap_or_else(|_| OsString::new())
28        });
29
30        Args::new(parsed_args_list)
31    }
32}
33
34/// Implements the Windows command-line argument parsing algorithm.
35///
36/// Microsoft's documentation for the Windows CLI argument format can be found at
37/// <https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-160#parsing-c-command-line-arguments>
38///
39/// A more in-depth explanation is here:
40/// <https://daviddeley.com/autohotkey/parameters/parameters.htm#WIN>
41///
42/// Windows includes a function to do command line parsing in shell32.dll.
43/// However, this is not used for two reasons:
44///
45/// 1. Linking with that DLL causes the process to be registered as a GUI application.
46/// GUI applications add a bunch of overhead, even if no windows are drawn. See
47/// <https://randomascii.wordpress.com/2018/12/03/a-not-called-function-can-cause-a-5x-slowdown/>.
48///
49/// 2. It does not follow the modern C/C++ argv rules outlined in the first two links above.
50///
51/// This function was tested for equivalence to the C/C++ parsing rules using an
52/// extensive test suite available at
53/// <https://github.com/ChrisDenton/winarg/tree/std>.
54fn parse_lp_cmd_line<'a, F: Fn() -> OsString>(
55    lp_cmd_line: Option<WStrUnits<'a>>,
56    exe_name: F,
57) -> Vec<OsString> {
58    const BACKSLASH: NonZero<u16> = NonZero::new(b'\\' as u16).unwrap();
59    const QUOTE: NonZero<u16> = NonZero::new(b'"' as u16).unwrap();
60    const TAB: NonZero<u16> = NonZero::new(b'\t' as u16).unwrap();
61    const SPACE: NonZero<u16> = NonZero::new(b' ' as u16).unwrap();
62
63    let mut ret_val = Vec::new();
64    // If the cmd line pointer is null or it points to an empty string then
65    // return the name of the executable as argv[0].
66    if lp_cmd_line.as_ref().and_then(|cmd| cmd.peek()).is_none() {
67        ret_val.push(exe_name());
68        return ret_val;
69    }
70    let mut code_units = lp_cmd_line.unwrap();
71
72    // The executable name at the beginning is special.
73    let mut in_quotes = false;
74    let mut cur = Vec::new();
75    for w in &mut code_units {
76        match w {
77            // A quote mark always toggles `in_quotes` no matter what because
78            // there are no escape characters when parsing the executable name.
79            QUOTE => in_quotes = !in_quotes,
80            // If not `in_quotes` then whitespace ends argv[0].
81            SPACE | TAB if !in_quotes => break,
82            // In all other cases the code unit is taken literally.
83            _ => cur.push(w.get()),
84        }
85    }
86    // Skip whitespace.
87    code_units.advance_while(|w| w == SPACE || w == TAB);
88    ret_val.push(OsString::from_wide(&cur));
89
90    // Parse the arguments according to these rules:
91    // * All code units are taken literally except space, tab, quote and backslash.
92    // * When not `in_quotes`, space and tab separate arguments. Consecutive spaces and tabs are
93    // treated as a single separator.
94    // * A space or tab `in_quotes` is taken literally.
95    // * A quote toggles `in_quotes` mode unless it's escaped. An escaped quote is taken literally.
96    // * A quote can be escaped if preceded by an odd number of backslashes.
97    // * If any number of backslashes is immediately followed by a quote then the number of
98    // backslashes is halved (rounding down).
99    // * Backslashes not followed by a quote are all taken literally.
100    // * If `in_quotes` then a quote can also be escaped using another quote
101    // (i.e. two consecutive quotes become one literal quote).
102    let mut cur = Vec::new();
103    let mut in_quotes = false;
104    while let Some(w) = code_units.next() {
105        match w {
106            // If not `in_quotes`, a space or tab ends the argument.
107            SPACE | TAB if !in_quotes => {
108                ret_val.push(OsString::from_wide(&cur[..]));
109                cur.clear();
110
111                // Skip whitespace.
112                code_units.advance_while(|w| w == SPACE || w == TAB);
113            }
114            // Backslashes can escape quotes or backslashes but only if consecutive backslashes are followed by a quote.
115            BACKSLASH => {
116                let backslash_count = code_units.advance_while(|w| w == BACKSLASH) + 1;
117                if code_units.peek() == Some(QUOTE) {
118                    cur.extend(iter::repeat(BACKSLASH.get()).take(backslash_count / 2));
119                    // The quote is escaped if there are an odd number of backslashes.
120                    if backslash_count % 2 == 1 {
121                        code_units.next();
122                        cur.push(QUOTE.get());
123                    }
124                } else {
125                    // If there is no quote on the end then there is no escaping.
126                    cur.extend(iter::repeat(BACKSLASH.get()).take(backslash_count));
127                }
128            }
129            // If `in_quotes` and not backslash escaped (see above) then a quote either
130            // unsets `in_quote` or is escaped by another quote.
131            QUOTE if in_quotes => match code_units.peek() {
132                // Two consecutive quotes when `in_quotes` produces one literal quote.
133                Some(QUOTE) => {
134                    cur.push(QUOTE.get());
135                    code_units.next();
136                }
137                // Otherwise set `in_quotes`.
138                Some(_) => in_quotes = false,
139                // The end of the command line.
140                // Push `cur` even if empty, which we do by breaking while `in_quotes` is still set.
141                None => break,
142            },
143            // If not `in_quotes` and not BACKSLASH escaped (see above) then a quote sets `in_quote`.
144            QUOTE => in_quotes = true,
145            // Everything else is always taken literally.
146            _ => cur.push(w.get()),
147        }
148    }
149    // Push the final argument, if any.
150    if !cur.is_empty() || in_quotes {
151        ret_val.push(OsString::from_wide(&cur[..]));
152    }
153    ret_val
154}
155
156#[derive(Debug)]
157pub(crate) enum Arg {
158    /// Add quotes (if needed)
159    Regular(OsString),
160    /// Append raw string without quoting
161    Raw(OsString),
162}
163
164enum Quote {
165    // Every arg is quoted
166    Always,
167    // Whitespace and empty args are quoted
168    Auto,
169    // Arg appended without any changes (#29494)
170    Never,
171}
172
173pub(crate) fn append_arg(cmd: &mut Vec<u16>, arg: &Arg, force_quotes: bool) -> io::Result<()> {
174    let (arg, quote) = match arg {
175        Arg::Regular(arg) => (arg, if force_quotes { Quote::Always } else { Quote::Auto }),
176        Arg::Raw(arg) => (arg, Quote::Never),
177    };
178
179    // If an argument has 0 characters then we need to quote it to ensure
180    // that it actually gets passed through on the command line or otherwise
181    // it will be dropped entirely when parsed on the other end.
182    ensure_no_nuls(arg)?;
183    let arg_bytes = arg.as_encoded_bytes();
184    let (quote, escape) = match quote {
185        Quote::Always => (true, true),
186        Quote::Auto => {
187            (arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') || arg_bytes.is_empty(), true)
188        }
189        Quote::Never => (false, false),
190    };
191    if quote {
192        cmd.push('"' as u16);
193    }
194
195    let mut backslashes: usize = 0;
196    for x in arg.encode_wide() {
197        if escape {
198            if x == '\\' as u16 {
199                backslashes += 1;
200            } else {
201                if x == '"' as u16 {
202                    // Add n+1 backslashes to total 2n+1 before internal '"'.
203                    cmd.extend((0..=backslashes).map(|_| '\\' as u16));
204                }
205                backslashes = 0;
206            }
207        }
208        cmd.push(x);
209    }
210
211    if quote {
212        // Add n backslashes to total 2n before ending '"'.
213        cmd.extend((0..backslashes).map(|_| '\\' as u16));
214        cmd.push('"' as u16);
215    }
216    Ok(())
217}
218
219fn append_bat_arg(cmd: &mut Vec<u16>, arg: &OsStr, mut quote: bool) -> io::Result<()> {
220    ensure_no_nuls(arg)?;
221    // If an argument has 0 characters then we need to quote it to ensure
222    // that it actually gets passed through on the command line or otherwise
223    // it will be dropped entirely when parsed on the other end.
224    //
225    // We also need to quote the argument if it ends with `\` to guard against
226    // bat usage such as `"%~2"` (i.e. force quote arguments) otherwise a
227    // trailing slash will escape the closing quote.
228    if arg.is_empty() || arg.as_encoded_bytes().last() == Some(&b'\\') {
229        quote = true;
230    }
231    for cp in arg.as_inner().inner.code_points() {
232        if let Some(cp) = cp.to_char() {
233            // Rather than trying to find every ascii symbol that must be quoted,
234            // we assume that all ascii symbols must be quoted unless they're known to be good.
235            // We also quote Unicode control blocks for good measure.
236            // Note an unquoted `\` is fine so long as the argument isn't otherwise quoted.
237            static UNQUOTED: &str = r"#$*+-./:?@\_";
238            let ascii_needs_quotes =
239                cp.is_ascii() && !(cp.is_ascii_alphanumeric() || UNQUOTED.contains(cp));
240            if ascii_needs_quotes || cp.is_control() {
241                quote = true;
242            }
243        }
244    }
245
246    if quote {
247        cmd.push('"' as u16);
248    }
249    // Loop through the string, escaping `\` only if followed by `"`.
250    // And escaping `"` by doubling them.
251    let mut backslashes: usize = 0;
252    for x in arg.encode_wide() {
253        if x == '\\' as u16 {
254            backslashes += 1;
255        } else {
256            if x == '"' as u16 {
257                // Add n backslashes to total 2n before internal `"`.
258                cmd.extend((0..backslashes).map(|_| '\\' as u16));
259                // Appending an additional double-quote acts as an escape.
260                cmd.push(b'"' as u16)
261            } else if x == '%' as u16 || x == '\r' as u16 {
262                // yt-dlp hack: replaces `%` with `%%cd:~,%` to stop %VAR% being expanded as an environment variable.
263                //
264                // # Explanation
265                //
266                // cmd supports extracting a substring from a variable using the following syntax:
267                //     %variable:~start_index,end_index%
268                //
269                // In the above command `cd` is used as the variable and the start_index and end_index are left blank.
270                // `cd` is a built-in variable that dynamically expands to the current directory so it's always available.
271                // Explicitly omitting both the start and end index creates a zero-length substring.
272                //
273                // Therefore it all resolves to nothing. However, by doing this no-op we distract cmd.exe
274                // from potentially expanding %variables% in the argument.
275                cmd.extend_from_slice(&[
276                    '%' as u16, '%' as u16, 'c' as u16, 'd' as u16, ':' as u16, '~' as u16,
277                    ',' as u16,
278                ]);
279            }
280            backslashes = 0;
281        }
282        cmd.push(x);
283    }
284    if quote {
285        // Add n backslashes to total 2n before ending `"`.
286        cmd.extend((0..backslashes).map(|_| '\\' as u16));
287        cmd.push('"' as u16);
288    }
289    Ok(())
290}
291
292pub(crate) fn make_bat_command_line(
293    script: &[u16],
294    args: &[Arg],
295    force_quotes: bool,
296) -> io::Result<Vec<u16>> {
297    const INVALID_ARGUMENT_ERROR: io::Error =
298        io::const_error!(io::ErrorKind::InvalidInput, r#"batch file arguments are invalid"#);
299    // Set the start of the command line to `cmd.exe /c "`
300    // It is necessary to surround the command in an extra pair of quotes,
301    // hence the trailing quote here. It will be closed after all arguments
302    // have been added.
303    // Using /e:ON enables "command extensions" which is essential for the `%` hack to work.
304    let mut cmd: Vec<u16> = "cmd.exe /e:ON /v:OFF /d /c \"".encode_utf16().collect();
305
306    // Push the script name surrounded by its quote pair.
307    cmd.push(b'"' as u16);
308    // Windows file names cannot contain a `"` character or end with `\\`.
309    // If the script name does then return an error.
310    if script.contains(&(b'"' as u16)) || script.last() == Some(&(b'\\' as u16)) {
311        return Err(io::const_error!(
312            io::ErrorKind::InvalidInput,
313            "Windows file names may not contain `\"` or end with `\\`"
314        ));
315    }
316    cmd.extend_from_slice(script.strip_suffix(&[0]).unwrap_or(script));
317    cmd.push(b'"' as u16);
318
319    // Append the arguments.
320    // FIXME: This needs tests to ensure that the arguments are properly
321    // reconstructed by the batch script by default.
322    for arg in args {
323        cmd.push(' ' as u16);
324        match arg {
325            Arg::Regular(arg_os) => {
326                let arg_bytes = arg_os.as_encoded_bytes();
327                // Disallow \r and \n as they may truncate the arguments.
328                const DISALLOWED: &[u8] = b"\r\n";
329                if arg_bytes.iter().any(|c| DISALLOWED.contains(c)) {
330                    return Err(INVALID_ARGUMENT_ERROR);
331                }
332                append_bat_arg(&mut cmd, arg_os, force_quotes)?;
333            }
334            _ => {
335                // Raw arguments are passed on as-is.
336                // It's the user's responsibility to properly handle arguments in this case.
337                append_arg(&mut cmd, arg, force_quotes)?;
338            }
339        };
340    }
341
342    // Close the quote we left opened earlier.
343    cmd.push(b'"' as u16);
344
345    Ok(cmd)
346}
347
348/// Takes a path and tries to return a non-verbatim path.
349///
350/// This is necessary because cmd.exe does not support verbatim paths.
351pub(crate) fn to_user_path(path: &Path) -> io::Result<Vec<u16>> {
352    from_wide_to_user_path(to_u16s(path)?)
353}
354pub(crate) fn from_wide_to_user_path(mut path: Vec<u16>) -> io::Result<Vec<u16>> {
355    // UTF-16 encoded code points, used in parsing and building UTF-16 paths.
356    // All of these are in the ASCII range so they can be cast directly to `u16`.
357    const SEP: u16 = b'\\' as _;
358    const QUERY: u16 = b'?' as _;
359    const COLON: u16 = b':' as _;
360    const U: u16 = b'U' as _;
361    const N: u16 = b'N' as _;
362    const C: u16 = b'C' as _;
363
364    // Early return if the path is too long to remove the verbatim prefix.
365    const LEGACY_MAX_PATH: usize = 260;
366    if path.len() > LEGACY_MAX_PATH {
367        return Ok(path);
368    }
369
370    match &path[..] {
371        // `\\?\C:\...` => `C:\...`
372        [SEP, SEP, QUERY, SEP, _, COLON, SEP, ..] => unsafe {
373            let lpfilename = path[4..].as_ptr();
374            fill_utf16_buf(
375                |buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()),
376                |full_path: &[u16]| {
377                    if full_path == &path[4..path.len() - 1] {
378                        let mut path: Vec<u16> = full_path.into();
379                        path.push(0);
380                        path
381                    } else {
382                        path
383                    }
384                },
385            )
386        },
387        // `\\?\UNC\...` => `\\...`
388        [SEP, SEP, QUERY, SEP, U, N, C, SEP, ..] => unsafe {
389            // Change the `C` in `UNC\` to `\` so we can get a slice that starts with `\\`.
390            path[6] = b'\\' as u16;
391            let lpfilename = path[6..].as_ptr();
392            fill_utf16_buf(
393                |buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()),
394                |full_path: &[u16]| {
395                    if full_path == &path[6..path.len() - 1] {
396                        let mut path: Vec<u16> = full_path.into();
397                        path.push(0);
398                        path
399                    } else {
400                        // Restore the 'C' in "UNC".
401                        path[6] = b'C' as u16;
402                        path
403                    }
404                },
405            )
406        },
407        // For everything else, leave the path unchanged.
408        _ => get_long_path(path, false),
409    }
410}