diff --git a/library/std/src/os/windows/process.rs b/library/std/src/os/windows/process.rs index 67756b15531bf..48c3d7bbd1789 100644 --- a/library/std/src/os/windows/process.rs +++ b/library/std/src/os/windows/process.rs @@ -2,6 +2,7 @@ #![stable(feature = "process_extensions", since = "1.2.0")] +use crate::ffi::OsStr; use crate::os::windows::io::{AsRawHandle, FromRawHandle, IntoRawHandle, RawHandle}; use crate::process; use crate::sealed::Sealed; @@ -125,6 +126,13 @@ pub trait CommandExt: Sealed { /// [2]: #[unstable(feature = "windows_process_extensions_force_quotes", issue = "82227")] fn force_quotes(&mut self, enabled: bool) -> &mut process::Command; + + /// Append literal text to the command line without any quoting or escaping. + /// + /// This is useful for passing arguments to `cmd.exe /c`, which doesn't follow + /// `CommandLineToArgvW` escaping rules. + #[unstable(feature = "windows_process_extensions_raw_arg", issue = "29494")] + fn raw_arg(&mut self, text_to_append_as_is: &OsStr) -> &mut process::Command; } #[stable(feature = "windows_process_extensions", since = "1.16.0")] @@ -138,4 +146,9 @@ impl CommandExt for process::Command { self.as_inner_mut().force_quotes(enabled); self } + + fn raw_arg(&mut self, raw_text: &OsStr) -> &mut process::Command { + self.as_inner_mut().raw_arg(raw_text); + self + } } diff --git a/library/std/src/sys/windows/process.rs b/library/std/src/sys/windows/process.rs index b082e21ab3bcc..1b685fcccb16f 100644 --- a/library/std/src/sys/windows/process.rs +++ b/library/std/src/sys/windows/process.rs @@ -137,7 +137,7 @@ fn ensure_no_nuls>(str: T) -> io::Result { pub struct Command { program: OsString, - args: Vec, + args: Vec, env: CommandEnv, cwd: Option, flags: u32, @@ -161,6 +161,14 @@ pub struct StdioPipes { pub stderr: Option, } +#[derive(Debug)] +enum Arg { + /// Add quotes (if needed) + Regular(OsString), + /// Append raw string without quoting + Raw(OsString), +} + impl Command { pub fn new(program: &OsStr) -> Command { Command { @@ -178,7 +186,7 @@ impl Command { } pub fn arg(&mut self, arg: &OsStr) { - self.args.push(arg.to_os_string()) + self.args.push(Arg::Regular(arg.to_os_string())) } pub fn env_mut(&mut self) -> &mut CommandEnv { &mut self.env @@ -203,6 +211,10 @@ impl Command { self.force_quotes_enabled = enabled; } + pub fn raw_arg(&mut self, command_str_to_append: &OsStr) { + self.args.push(Arg::Raw(command_str_to_append.to_os_string())) + } + pub fn get_program(&self) -> &OsStr { &self.program } @@ -536,44 +548,63 @@ fn zeroed_process_information() -> c::PROCESS_INFORMATION { } } +enum Quote { + // Every arg is quoted + Always, + // Whitespace and empty args are quoted + Auto, + // Arg appended without any changes (#29494) + Never, +} + // Produces a wide string *without terminating null*; returns an error if // `prog` or any of the `args` contain a nul. -fn make_command_line(prog: &OsStr, args: &[OsString], force_quotes: bool) -> io::Result> { +fn make_command_line(prog: &OsStr, args: &[Arg], force_quotes: bool) -> io::Result> { // Encode the command and arguments in a command line string such // that the spawned process may recover them using CommandLineToArgvW. let mut cmd: Vec = Vec::new(); // Always quote the program name so CreateProcess doesn't interpret args as // part of the name if the binary wasn't found first time. - append_arg(&mut cmd, prog, true)?; + append_arg(&mut cmd, prog, Quote::Always)?; for arg in args { cmd.push(' ' as u16); - append_arg(&mut cmd, arg, force_quotes)?; + let (arg, quote) = match arg { + Arg::Regular(arg) => (arg, if force_quotes { Quote::Always } else { Quote::Auto }), + Arg::Raw(arg) => (arg, Quote::Never), + }; + append_arg(&mut cmd, arg, quote)?; } return Ok(cmd); - fn append_arg(cmd: &mut Vec, arg: &OsStr, force_quotes: bool) -> io::Result<()> { + fn append_arg(cmd: &mut Vec, arg: &OsStr, quote: Quote) -> io::Result<()> { // If an argument has 0 characters then we need to quote it to ensure // that it actually gets passed through on the command line or otherwise // it will be dropped entirely when parsed on the other end. ensure_no_nuls(arg)?; let arg_bytes = &arg.as_inner().inner.as_inner(); - let quote = force_quotes - || arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') - || arg_bytes.is_empty(); + let (quote, escape) = match quote { + Quote::Always => (true, true), + Quote::Auto => { + (arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') || arg_bytes.is_empty(), true) + } + Quote::Never => (false, false), + }; if quote { cmd.push('"' as u16); } let mut backslashes: usize = 0; for x in arg.encode_wide() { - if x == '\\' as u16 { - backslashes += 1; - } else { - if x == '"' as u16 { - // Add n+1 backslashes to total 2n+1 before internal '"'. - cmd.extend((0..=backslashes).map(|_| '\\' as u16)); + if escape { + if x == '\\' as u16 { + backslashes += 1; + } else { + if x == '"' as u16 { + // Add n+1 backslashes to total 2n+1 before internal '"'. + cmd.extend((0..=backslashes).map(|_| '\\' as u16)); + } + backslashes = 0; } - backslashes = 0; } cmd.push(x); } @@ -626,13 +657,15 @@ fn make_dirp(d: Option<&OsString>) -> io::Result<(*const u16, Vec)> { } pub struct CommandArgs<'a> { - iter: crate::slice::Iter<'a, OsString>, + iter: crate::slice::Iter<'a, Arg>, } impl<'a> Iterator for CommandArgs<'a> { type Item = &'a OsStr; fn next(&mut self) -> Option<&'a OsStr> { - self.iter.next().map(|s| s.as_ref()) + self.iter.next().map(|arg| match arg { + Arg::Regular(s) | Arg::Raw(s) => s.as_ref(), + }) } fn size_hint(&self) -> (usize, Option) { self.iter.size_hint() diff --git a/library/std/src/sys/windows/process/tests.rs b/library/std/src/sys/windows/process/tests.rs index 331bc9db3b1f0..3b65856dcaca6 100644 --- a/library/std/src/sys/windows/process/tests.rs +++ b/library/std/src/sys/windows/process/tests.rs @@ -1,14 +1,35 @@ use super::make_command_line; +use super::Arg; use crate::env; use crate::ffi::{OsStr, OsString}; use crate::process::Command; +#[test] +fn test_raw_args() { + let command_line = &make_command_line( + OsStr::new("quoted exe"), + &[ + Arg::Regular(OsString::from("quote me")), + Arg::Raw(OsString::from("quote me *not*")), + Arg::Raw(OsString::from("\t\\")), + Arg::Raw(OsString::from("internal \\\"backslash-\"quote")), + Arg::Regular(OsString::from("optional-quotes")), + ], + false, + ) + .unwrap(); + assert_eq!( + String::from_utf16(command_line).unwrap(), + "\"quoted exe\" \"quote me\" quote me *not* \t\\ internal \\\"backslash-\"quote optional-quotes" + ); +} + #[test] fn test_make_command_line() { fn test_wrapper(prog: &str, args: &[&str], force_quotes: bool) -> String { let command_line = &make_command_line( OsStr::new(prog), - &args.iter().map(|a| OsString::from(a)).collect::>(), + &args.iter().map(|a| Arg::Regular(OsString::from(a))).collect::>(), force_quotes, ) .unwrap();