Skip to content

Commit 76057dc

Browse files
committed
Auto merge of #1891 - ChrisDenton:win-args, r=RalfJung
Correct Windows argument handling Previously the command line string would have been incorrectly constructed if argv[0] contained a doublequote (`"`) or ended in a trailing backslash (`\`). This is a very rare edge case because, by convention, argv[0] is the path to the application and Windows file names cannot contain doublequotes. Fixes #1881
2 parents 6cf851f + cfd1316 commit 76057dc

File tree

1 file changed

+105
-10
lines changed

1 file changed

+105
-10
lines changed

src/eval.rs

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use std::convert::TryFrom;
44
use std::ffi::OsStr;
5+
use std::iter;
56

67
use log::info;
78

@@ -202,17 +203,8 @@ pub fn create_ecx<'mir, 'tcx: 'mir>(
202203
// Store command line as UTF-16 for Windows `GetCommandLineW`.
203204
{
204205
// Construct a command string with all the aguments.
205-
let mut cmd = String::new();
206-
for arg in config.args.iter() {
207-
if !cmd.is_empty() {
208-
cmd.push(' ');
209-
}
210-
cmd.push_str(&*shell_escape::windows::escape(arg.as_str().into()));
211-
}
212-
// Don't forget `0` terminator.
213-
cmd.push(std::char::from_u32(0).unwrap());
206+
let cmd_utf16: Vec<u16> = args_to_utf16_command_string(config.args.iter());
214207

215-
let cmd_utf16: Vec<u16> = cmd.encode_utf16().collect();
216208
let cmd_type = tcx.mk_array(tcx.types.u16, u64::try_from(cmd_utf16.len()).unwrap());
217209
let cmd_place =
218210
ecx.allocate(ecx.layout_of(cmd_type)?, MiriMemoryKind::Machine.into())?;
@@ -353,3 +345,106 @@ pub fn eval_entry<'tcx>(
353345
Err(e) => report_error(&ecx, e),
354346
}
355347
}
348+
349+
/// Turns an array of arguments into a Windows command line string.
350+
///
351+
/// The string will be UTF-16 encoded and NUL terminated.
352+
///
353+
/// Panics if the zeroth argument contains the `"` character because doublequotes
354+
/// in argv[0] cannot be encoded using the standard command line parsing rules.
355+
///
356+
/// Further reading:
357+
/// * [Parsing C++ command-line arguments](https://docs.microsoft.com/en-us/cpp/cpp/main-function-command-line-args?view=msvc-160#parsing-c-command-line-arguments)
358+
/// * [The C/C++ Parameter Parsing Rules](https://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULES)
359+
fn args_to_utf16_command_string<I, T>(mut args: I) -> Vec<u16>
360+
where
361+
I: Iterator<Item = T>,
362+
T: AsRef<str>,
363+
{
364+
// Parse argv[0]. Slashes aren't escaped. Literal double quotes are not allowed.
365+
let mut cmd = {
366+
let arg0 = if let Some(arg0) = args.next() {
367+
arg0
368+
} else {
369+
return vec![0];
370+
};
371+
let arg0 = arg0.as_ref();
372+
if arg0.contains('"') {
373+
panic!("argv[0] cannot contain a doublequote (\") character");
374+
} else {
375+
// Always surround argv[0] with quotes.
376+
let mut s = String::new();
377+
s.push('"');
378+
s.push_str(arg0);
379+
s.push('"');
380+
s
381+
}
382+
};
383+
384+
// Build the other arguments.
385+
for arg in args {
386+
let arg = arg.as_ref();
387+
cmd.push(' ');
388+
if arg.is_empty() {
389+
cmd.push_str("\"\"");
390+
} else if !arg.bytes().any(|c| matches!(c, b'"' | b'\t' | b' ')) {
391+
// No quote, tab, or space -- no escaping required.
392+
cmd.push_str(arg);
393+
} else {
394+
// Spaces and tabs are escaped by surrounding them in quotes.
395+
// Quotes are themselves escaped by using backslashes when in a
396+
// quoted block.
397+
// Backslashes only need to be escaped when one or more are directly
398+
// followed by a quote. Otherwise they are taken literally.
399+
400+
cmd.push('"');
401+
let mut chars = arg.chars().peekable();
402+
loop {
403+
let mut nslashes = 0;
404+
while let Some(&'\\') = chars.peek() {
405+
chars.next();
406+
nslashes += 1;
407+
}
408+
409+
match chars.next() {
410+
Some('"') => {
411+
cmd.extend(iter::repeat('\\').take(nslashes * 2 + 1));
412+
cmd.push('"');
413+
}
414+
Some(c) => {
415+
cmd.extend(iter::repeat('\\').take(nslashes));
416+
cmd.push(c);
417+
}
418+
None => {
419+
cmd.extend(iter::repeat('\\').take(nslashes * 2));
420+
break;
421+
}
422+
}
423+
}
424+
cmd.push('"');
425+
}
426+
}
427+
428+
if cmd.contains('\0') {
429+
panic!("interior null in command line arguments");
430+
}
431+
cmd.encode_utf16().chain(iter::once(0)).collect()
432+
}
433+
434+
#[cfg(test)]
435+
mod tests {
436+
use super::*;
437+
#[test]
438+
#[should_panic(expected = "argv[0] cannot contain a doublequote (\") character")]
439+
fn windows_argv0_panic_on_quote() {
440+
args_to_utf16_command_string(["\""].iter());
441+
}
442+
#[test]
443+
fn windows_argv0_no_escape() {
444+
// Ensure that a trailing backslash in argv[0] is not escaped.
445+
let cmd = String::from_utf16_lossy(&args_to_utf16_command_string(
446+
[r"C:\Program Files\", "arg1"].iter(),
447+
));
448+
assert_eq!(cmd.trim_end_matches("\0"), r#""C:\Program Files\" arg1"#);
449+
}
450+
}

0 commit comments

Comments
 (0)