Skip to content

Commit 23320a2

Browse files
committed
Command: handle exe and batch files separately
1 parent d59cf56 commit 23320a2

File tree

3 files changed

+113
-22
lines changed

3 files changed

+113
-22
lines changed

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

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,89 @@ pub(crate) fn append_arg(cmd: &mut Vec<u16>, arg: &Arg, force_quotes: bool) -> i
299299
}
300300
Ok(())
301301
}
302+
303+
pub(crate) fn make_bat_command_line(
304+
script: &[u16],
305+
args: &[Arg],
306+
force_quotes: bool,
307+
) -> io::Result<Vec<u16>> {
308+
// Set the start of the command line to `cmd.exe /c "`
309+
// It is necessary to surround the command in an extra pair of quotes,
310+
// hence The trailing quote here. It will be closed after all arguments
311+
// have been added.
312+
let mut cmd: Vec<u16> = "cmd.exe /c \"".encode_utf16().collect();
313+
314+
// Push the script name surrounded by its quote pair.
315+
cmd.push(b'"' as u16);
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+
append_arg(&mut cmd, arg, force_quotes)?;
325+
}
326+
327+
// Close the quote we left opened earlier.
328+
cmd.push(b'"' as u16);
329+
330+
Ok(cmd)
331+
}
332+
333+
/// Takes a path and tries to return a non-verbatim path.
334+
///
335+
/// This is necessary because cmd.exe does not support verbatim paths.
336+
pub(crate) fn to_user_path(mut path: Vec<u16>) -> io::Result<Vec<u16>> {
337+
use crate::ptr;
338+
use crate::sys::windows::fill_utf16_buf;
339+
340+
// UTF-16 encoded code points, used in parsing and building UTF-16 paths.
341+
// All of these are in the ASCII range so they can be cast directly to `u16`.
342+
const SEP: u16 = b'\\' as _;
343+
const QUERY: u16 = b'?' as _;
344+
const COLON: u16 = b':' as _;
345+
const U: u16 = b'U' as _;
346+
const N: u16 = b'N' as _;
347+
const C: u16 = b'C' as _;
348+
349+
// Early return if the path is too long to remove the verbatim prefix.
350+
const LEGACY_MAX_PATH: usize = 260;
351+
if path.len() > LEGACY_MAX_PATH {
352+
return Ok(path);
353+
}
354+
355+
match &path[..] {
356+
// `\\?\C:\...` => `C:\...`
357+
[SEP, SEP, QUERY, SEP, _, COLON, SEP, ..] => unsafe {
358+
let lpfilename = path[4..].as_ptr();
359+
fill_utf16_buf(
360+
|buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()),
361+
|full_path: &[u16]| {
362+
if full_path == &path[4..path.len() - 1] { full_path.into() } else { path }
363+
},
364+
)
365+
},
366+
// `\\?\UNC\...` => `\\...`
367+
[SEP, SEP, QUERY, SEP, U, N, C, SEP, ..] => unsafe {
368+
// Change the `C` in `UNC\` to `\` so we can get a slice that starts with `\\`.
369+
path[6] = b'\\' as u16;
370+
let lpfilename = path[6..].as_ptr();
371+
fill_utf16_buf(
372+
|buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()),
373+
|full_path: &[u16]| {
374+
if full_path == &path[6..path.len() - 1] {
375+
full_path.into()
376+
} else {
377+
// Restore the 'C' in "UNC".
378+
path[6] = b'C' as u16;
379+
path
380+
}
381+
},
382+
)
383+
},
384+
// For everything else, leave the path unchanged.
385+
_ => Ok(path),
386+
}
387+
}

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

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,19 @@ impl Command {
267267
program.len().checked_sub(5).and_then(|i| program.get(i..)),
268268
Some([46, 98 | 66, 97 | 65, 116 | 84, 0] | [46, 99 | 67, 109 | 77, 100 | 68, 0])
269269
);
270-
let mut cmd_str =
271-
make_command_line(&program, &self.args, self.force_quotes_enabled, is_batch_file)?;
270+
let (program, mut cmd_str) = if is_batch_file {
271+
(
272+
command_prompt()?,
273+
args::make_bat_command_line(
274+
&args::to_user_path(program)?,
275+
&self.args,
276+
self.force_quotes_enabled,
277+
)?,
278+
)
279+
} else {
280+
let cmd_str = make_command_line(&self.program, &self.args, self.force_quotes_enabled)?;
281+
(program, cmd_str)
282+
};
272283
cmd_str.push(0); // add null terminator
273284

274285
// stolen from the libuv code.
@@ -719,30 +730,17 @@ fn zeroed_process_information() -> c::PROCESS_INFORMATION {
719730

720731
// Produces a wide string *without terminating null*; returns an error if
721732
// `prog` or any of the `args` contain a nul.
722-
fn make_command_line(
723-
prog: &[u16],
724-
args: &[Arg],
725-
force_quotes: bool,
726-
is_batch_file: bool,
727-
) -> io::Result<Vec<u16>> {
733+
fn make_command_line(argv0: &OsStr, args: &[Arg], force_quotes: bool) -> io::Result<Vec<u16>> {
728734
// Encode the command and arguments in a command line string such
729735
// that the spawned process may recover them using CommandLineToArgvW.
730736
let mut cmd: Vec<u16> = Vec::new();
731737

732-
// CreateFileW has special handling for .bat and .cmd files, which means we
733-
// need to add an extra pair of quotes surrounding the whole command line
734-
// so they are properly passed on to the script.
735-
// See issue #91991.
736-
if is_batch_file {
737-
cmd.push(b'"' as u16);
738-
}
739-
740738
// Always quote the program name so CreateProcess to avoid ambiguity when
741739
// the child process parses its arguments.
742740
// Note that quotes aren't escaped here because they can't be used in arg0.
743741
// But that's ok because file paths can't contain quotes.
744742
cmd.push(b'"' as u16);
745-
cmd.extend_from_slice(prog.strip_suffix(&[0]).unwrap_or(prog));
743+
cmd.extend(argv0.encode_wide());
746744
cmd.push(b'"' as u16);
747745

748746
for arg in args {
@@ -752,6 +750,16 @@ fn make_command_line(
752750
Ok(cmd)
753751
}
754752

753+
// Get `cmd.exe` for use with bat scripts, encoded as a UTF-16 string.
754+
fn command_prompt() -> io::Result<Vec<u16>> {
755+
let mut system: Vec<u16> = super::fill_utf16_buf(
756+
|buf, size| unsafe { c::GetSystemDirectoryW(buf, size) },
757+
|buf| buf.into(),
758+
)?;
759+
system.extend("\\cmd.exe".encode_utf16().chain([0]));
760+
Ok(system)
761+
}
762+
755763
fn make_envp(maybe_env: Option<BTreeMap<EnvKey, OsString>>) -> io::Result<(*mut c_void, Vec<u16>)> {
756764
// On Windows we pass an "environment block" which is not a char**, but
757765
// rather a concatenation of null-terminated k=v\0 sequences, with a final

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ use super::Arg;
33
use crate::env;
44
use crate::ffi::{OsStr, OsString};
55
use crate::process::Command;
6-
use crate::sys::to_u16s;
76

87
#[test]
98
fn test_raw_args() {
109
let command_line = &make_command_line(
11-
&to_u16s("quoted exe").unwrap(),
10+
OsStr::new("quoted exe"),
1211
&[
1312
Arg::Regular(OsString::from("quote me")),
1413
Arg::Raw(OsString::from("quote me *not*")),
@@ -17,7 +16,6 @@ fn test_raw_args() {
1716
Arg::Regular(OsString::from("optional-quotes")),
1817
],
1918
false,
20-
false,
2119
)
2220
.unwrap();
2321
assert_eq!(
@@ -30,10 +28,9 @@ fn test_raw_args() {
3028
fn test_make_command_line() {
3129
fn test_wrapper(prog: &str, args: &[&str], force_quotes: bool) -> String {
3230
let command_line = &make_command_line(
33-
&to_u16s(prog).unwrap(),
31+
OsStr::new(prog),
3432
&args.iter().map(|a| Arg::Regular(OsString::from(a))).collect::<Vec<_>>(),
3533
force_quotes,
36-
false,
3734
)
3835
.unwrap();
3936
String::from_utf16(command_line).unwrap()

0 commit comments

Comments
 (0)