Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unescaped command-line arguments for Windows #85832

Merged
merged 4 commits into from
Jul 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions library/std/src/os/windows/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -125,6 +126,13 @@ pub trait CommandExt: Sealed {
/// [2]: <https://msdn.microsoft.com/en-us/library/17w5ykft.aspx>
#[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<S: AsRef<OsStr>>(&mut self, text_to_append_as_is: S) -> &mut process::Command;
}

#[stable(feature = "windows_process_extensions", since = "1.16.0")]
Expand All @@ -138,4 +146,9 @@ impl CommandExt for process::Command {
self.as_inner_mut().force_quotes(enabled);
self
}

fn raw_arg<S: AsRef<OsStr>>(&mut self, raw_text: S) -> &mut process::Command {
self.as_inner_mut().raw_arg(raw_text.as_ref());
self
}
}
77 changes: 57 additions & 20 deletions library/std/src/sys/windows/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ fn ensure_no_nuls<T: AsRef<OsStr>>(str: T) -> io::Result<T> {

pub struct Command {
program: OsString,
args: Vec<OsString>,
args: Vec<Arg>,
env: CommandEnv,
cwd: Option<OsString>,
flags: u32,
Expand All @@ -161,6 +161,14 @@ pub struct StdioPipes {
pub stderr: Option<AnonPipe>,
}

#[derive(Debug)]
enum Arg {
/// Add quotes (if needed)
Regular(OsString),
kornelski marked this conversation as resolved.
Show resolved Hide resolved
/// Append raw string without quoting
Raw(OsString),
}

impl Command {
pub fn new(program: &OsStr) -> Command {
Command {
Expand All @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -315,9 +327,13 @@ impl Command {

impl fmt::Debug for Command {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.program)?;
self.program.fmt(f)?;
for arg in &self.args {
write!(f, " {:?}", arg)?;
f.write_str(" ")?;
match arg {
Arg::Regular(s) => s.fmt(f),
Arg::Raw(s) => f.write_str(&s.to_string_lossy()),
}?;
}
Ok(())
}
Expand Down Expand Up @@ -536,44 +552,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<Vec<u16>> {
fn make_command_line(prog: &OsStr, args: &[Arg], force_quotes: bool) -> io::Result<Vec<u16>> {
// Encode the command and arguments in a command line string such
// that the spawned process may recover them using CommandLineToArgvW.
let mut cmd: Vec<u16> = 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<u16>, arg: &OsStr, force_quotes: bool) -> io::Result<()> {
fn append_arg(cmd: &mut Vec<u16>, 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);
}
Expand Down Expand Up @@ -626,13 +661,15 @@ fn make_dirp(d: Option<&OsString>) -> io::Result<(*const u16, Vec<u16>)> {
}

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<usize>) {
self.iter.size_hint()
Expand Down
28 changes: 27 additions & 1 deletion library/std/src/sys/windows/process/tests.rs
Original file line number Diff line number Diff line change
@@ -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::<Vec<OsString>>(),
&args.iter().map(|a| Arg::Regular(OsString::from(a))).collect::<Vec<_>>(),
force_quotes,
)
.unwrap();
Expand All @@ -17,6 +38,11 @@ fn test_make_command_line() {

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

assert_eq!(test_wrapper("prog", &[r"C:\"], false), r#""prog" C:\"#);
assert_eq!(test_wrapper("prog", &[r"2slashes\\"], false), r#""prog" 2slashes\\"#);
assert_eq!(test_wrapper("prog", &[r" C:\"], false), r#""prog" " C:\\""#);
assert_eq!(test_wrapper("prog", &[r" 2slashes\\"], false), r#""prog" " 2slashes\\\\""#);

assert_eq!(
test_wrapper("C:\\Program Files\\blah\\blah.exe", &["aaa"], false),
"\"C:\\Program Files\\blah\\blah.exe\" aaa"
Expand Down