Skip to content

Commit 3eff244

Browse files
committed
Auto merge of rust-lang#85832 - kornelski:raw_arg, r=yaahc
Unescaped command-line arguments for Windows Some Windows commands, expecially `cmd.exe /c`, have unusual quoting requirements which are incompatible with default rules assumed for `.arg()`. This adds `.unquoted_arg()` to `Command` via Windows `CommandExt` trait. Fixes rust-lang#29494
2 parents 619c27a + bc67f6b commit 3eff244

File tree

3 files changed

+97
-21
lines changed

3 files changed

+97
-21
lines changed

library/std/src/os/windows/process.rs

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
#![stable(feature = "process_extensions", since = "1.2.0")]
44

5+
use crate::ffi::OsStr;
56
use crate::os::windows::io::{AsRawHandle, FromRawHandle, IntoRawHandle, RawHandle};
67
use crate::process;
78
use crate::sealed::Sealed;
@@ -125,6 +126,13 @@ pub trait CommandExt: Sealed {
125126
/// [2]: <https://msdn.microsoft.com/en-us/library/17w5ykft.aspx>
126127
#[unstable(feature = "windows_process_extensions_force_quotes", issue = "82227")]
127128
fn force_quotes(&mut self, enabled: bool) -> &mut process::Command;
129+
130+
/// Append literal text to the command line without any quoting or escaping.
131+
///
132+
/// This is useful for passing arguments to `cmd.exe /c`, which doesn't follow
133+
/// `CommandLineToArgvW` escaping rules.
134+
#[unstable(feature = "windows_process_extensions_raw_arg", issue = "29494")]
135+
fn raw_arg<S: AsRef<OsStr>>(&mut self, text_to_append_as_is: S) -> &mut process::Command;
128136
}
129137

130138
#[stable(feature = "windows_process_extensions", since = "1.16.0")]
@@ -138,4 +146,9 @@ impl CommandExt for process::Command {
138146
self.as_inner_mut().force_quotes(enabled);
139147
self
140148
}
149+
150+
fn raw_arg<S: AsRef<OsStr>>(&mut self, raw_text: S) -> &mut process::Command {
151+
self.as_inner_mut().raw_arg(raw_text.as_ref());
152+
self
153+
}
141154
}

library/std/src/sys/windows/process.rs

+57-20
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ fn ensure_no_nuls<T: AsRef<OsStr>>(str: T) -> io::Result<T> {
137137

138138
pub struct Command {
139139
program: OsString,
140-
args: Vec<OsString>,
140+
args: Vec<Arg>,
141141
env: CommandEnv,
142142
cwd: Option<OsString>,
143143
flags: u32,
@@ -161,6 +161,14 @@ pub struct StdioPipes {
161161
pub stderr: Option<AnonPipe>,
162162
}
163163

164+
#[derive(Debug)]
165+
enum Arg {
166+
/// Add quotes (if needed)
167+
Regular(OsString),
168+
/// Append raw string without quoting
169+
Raw(OsString),
170+
}
171+
164172
impl Command {
165173
pub fn new(program: &OsStr) -> Command {
166174
Command {
@@ -178,7 +186,7 @@ impl Command {
178186
}
179187

180188
pub fn arg(&mut self, arg: &OsStr) {
181-
self.args.push(arg.to_os_string())
189+
self.args.push(Arg::Regular(arg.to_os_string()))
182190
}
183191
pub fn env_mut(&mut self) -> &mut CommandEnv {
184192
&mut self.env
@@ -203,6 +211,10 @@ impl Command {
203211
self.force_quotes_enabled = enabled;
204212
}
205213

214+
pub fn raw_arg(&mut self, command_str_to_append: &OsStr) {
215+
self.args.push(Arg::Raw(command_str_to_append.to_os_string()))
216+
}
217+
206218
pub fn get_program(&self) -> &OsStr {
207219
&self.program
208220
}
@@ -315,9 +327,13 @@ impl Command {
315327

316328
impl fmt::Debug for Command {
317329
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
318-
write!(f, "{:?}", self.program)?;
330+
self.program.fmt(f)?;
319331
for arg in &self.args {
320-
write!(f, " {:?}", arg)?;
332+
f.write_str(" ")?;
333+
match arg {
334+
Arg::Regular(s) => s.fmt(f),
335+
Arg::Raw(s) => f.write_str(&s.to_string_lossy()),
336+
}?;
321337
}
322338
Ok(())
323339
}
@@ -536,44 +552,63 @@ fn zeroed_process_information() -> c::PROCESS_INFORMATION {
536552
}
537553
}
538554

555+
enum Quote {
556+
// Every arg is quoted
557+
Always,
558+
// Whitespace and empty args are quoted
559+
Auto,
560+
// Arg appended without any changes (#29494)
561+
Never,
562+
}
563+
539564
// Produces a wide string *without terminating null*; returns an error if
540565
// `prog` or any of the `args` contain a nul.
541-
fn make_command_line(prog: &OsStr, args: &[OsString], force_quotes: bool) -> io::Result<Vec<u16>> {
566+
fn make_command_line(prog: &OsStr, args: &[Arg], force_quotes: bool) -> io::Result<Vec<u16>> {
542567
// Encode the command and arguments in a command line string such
543568
// that the spawned process may recover them using CommandLineToArgvW.
544569
let mut cmd: Vec<u16> = Vec::new();
545570
// Always quote the program name so CreateProcess doesn't interpret args as
546571
// part of the name if the binary wasn't found first time.
547-
append_arg(&mut cmd, prog, true)?;
572+
append_arg(&mut cmd, prog, Quote::Always)?;
548573
for arg in args {
549574
cmd.push(' ' as u16);
550-
append_arg(&mut cmd, arg, force_quotes)?;
575+
let (arg, quote) = match arg {
576+
Arg::Regular(arg) => (arg, if force_quotes { Quote::Always } else { Quote::Auto }),
577+
Arg::Raw(arg) => (arg, Quote::Never),
578+
};
579+
append_arg(&mut cmd, arg, quote)?;
551580
}
552581
return Ok(cmd);
553582

554-
fn append_arg(cmd: &mut Vec<u16>, arg: &OsStr, force_quotes: bool) -> io::Result<()> {
583+
fn append_arg(cmd: &mut Vec<u16>, arg: &OsStr, quote: Quote) -> io::Result<()> {
555584
// If an argument has 0 characters then we need to quote it to ensure
556585
// that it actually gets passed through on the command line or otherwise
557586
// it will be dropped entirely when parsed on the other end.
558587
ensure_no_nuls(arg)?;
559588
let arg_bytes = &arg.as_inner().inner.as_inner();
560-
let quote = force_quotes
561-
|| arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t')
562-
|| arg_bytes.is_empty();
589+
let (quote, escape) = match quote {
590+
Quote::Always => (true, true),
591+
Quote::Auto => {
592+
(arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') || arg_bytes.is_empty(), true)
593+
}
594+
Quote::Never => (false, false),
595+
};
563596
if quote {
564597
cmd.push('"' as u16);
565598
}
566599

567600
let mut backslashes: usize = 0;
568601
for x in arg.encode_wide() {
569-
if x == '\\' as u16 {
570-
backslashes += 1;
571-
} else {
572-
if x == '"' as u16 {
573-
// Add n+1 backslashes to total 2n+1 before internal '"'.
574-
cmd.extend((0..=backslashes).map(|_| '\\' as u16));
602+
if escape {
603+
if x == '\\' as u16 {
604+
backslashes += 1;
605+
} else {
606+
if x == '"' as u16 {
607+
// Add n+1 backslashes to total 2n+1 before internal '"'.
608+
cmd.extend((0..=backslashes).map(|_| '\\' as u16));
609+
}
610+
backslashes = 0;
575611
}
576-
backslashes = 0;
577612
}
578613
cmd.push(x);
579614
}
@@ -626,13 +661,15 @@ fn make_dirp(d: Option<&OsString>) -> io::Result<(*const u16, Vec<u16>)> {
626661
}
627662

628663
pub struct CommandArgs<'a> {
629-
iter: crate::slice::Iter<'a, OsString>,
664+
iter: crate::slice::Iter<'a, Arg>,
630665
}
631666

632667
impl<'a> Iterator for CommandArgs<'a> {
633668
type Item = &'a OsStr;
634669
fn next(&mut self) -> Option<&'a OsStr> {
635-
self.iter.next().map(|s| s.as_ref())
670+
self.iter.next().map(|arg| match arg {
671+
Arg::Regular(s) | Arg::Raw(s) => s.as_ref(),
672+
})
636673
}
637674
fn size_hint(&self) -> (usize, Option<usize>) {
638675
self.iter.size_hint()

library/std/src/sys/windows/process/tests.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
11
use super::make_command_line;
2+
use super::Arg;
23
use crate::env;
34
use crate::ffi::{OsStr, OsString};
45
use crate::process::Command;
56

7+
#[test]
8+
fn test_raw_args() {
9+
let command_line = &make_command_line(
10+
OsStr::new("quoted exe"),
11+
&[
12+
Arg::Regular(OsString::from("quote me")),
13+
Arg::Raw(OsString::from("quote me *not*")),
14+
Arg::Raw(OsString::from("\t\\")),
15+
Arg::Raw(OsString::from("internal \\\"backslash-\"quote")),
16+
Arg::Regular(OsString::from("optional-quotes")),
17+
],
18+
false,
19+
)
20+
.unwrap();
21+
assert_eq!(
22+
String::from_utf16(command_line).unwrap(),
23+
"\"quoted exe\" \"quote me\" quote me *not* \t\\ internal \\\"backslash-\"quote optional-quotes"
24+
);
25+
}
26+
627
#[test]
728
fn test_make_command_line() {
829
fn test_wrapper(prog: &str, args: &[&str], force_quotes: bool) -> String {
930
let command_line = &make_command_line(
1031
OsStr::new(prog),
11-
&args.iter().map(|a| OsString::from(a)).collect::<Vec<OsString>>(),
32+
&args.iter().map(|a| Arg::Regular(OsString::from(a))).collect::<Vec<_>>(),
1233
force_quotes,
1334
)
1435
.unwrap();
@@ -17,6 +38,11 @@ fn test_make_command_line() {
1738

1839
assert_eq!(test_wrapper("prog", &["aaa", "bbb", "ccc"], false), "\"prog\" aaa bbb ccc");
1940

41+
assert_eq!(test_wrapper("prog", &[r"C:\"], false), r#""prog" C:\"#);
42+
assert_eq!(test_wrapper("prog", &[r"2slashes\\"], false), r#""prog" 2slashes\\"#);
43+
assert_eq!(test_wrapper("prog", &[r" C:\"], false), r#""prog" " C:\\""#);
44+
assert_eq!(test_wrapper("prog", &[r" 2slashes\\"], false), r#""prog" " 2slashes\\\\""#);
45+
2046
assert_eq!(
2147
test_wrapper("C:\\Program Files\\blah\\blah.exe", &["aaa"], false),
2248
"\"C:\\Program Files\\blah\\blah.exe\" aaa"

0 commit comments

Comments
 (0)