Command: handle exe and batch files separately

This commit is contained in:
Chris Denton 2022-03-23 04:28:04 +00:00
parent d59cf5629e
commit 23320a2f83
No known key found for this signature in database
GPG Key ID: 713472F2F45627DE
3 changed files with 113 additions and 22 deletions

View File

@ -299,3 +299,89 @@ pub(crate) fn append_arg(cmd: &mut Vec<u16>, arg: &Arg, force_quotes: bool) -> i
}
Ok(())
}
pub(crate) fn make_bat_command_line(
script: &[u16],
args: &[Arg],
force_quotes: bool,
) -> io::Result<Vec<u16>> {
// Set the start of the command line to `cmd.exe /c "`
// It is necessary to surround the command in an extra pair of quotes,
// hence The trailing quote here. It will be closed after all arguments
// have been added.
let mut cmd: Vec<u16> = "cmd.exe /c \"".encode_utf16().collect();
// Push the script name surrounded by its quote pair.
cmd.push(b'"' as u16);
cmd.extend_from_slice(script.strip_suffix(&[0]).unwrap_or(script));
cmd.push(b'"' as u16);
// Append the arguments.
// FIXME: This needs tests to ensure that the arguments are properly
// reconstructed by the batch script by default.
for arg in args {
cmd.push(' ' as u16);
append_arg(&mut cmd, arg, force_quotes)?;
}
// Close the quote we left opened earlier.
cmd.push(b'"' as u16);
Ok(cmd)
}
/// Takes a path and tries to return a non-verbatim path.
///
/// This is necessary because cmd.exe does not support verbatim paths.
pub(crate) fn to_user_path(mut path: Vec<u16>) -> io::Result<Vec<u16>> {
use crate::ptr;
use crate::sys::windows::fill_utf16_buf;
// UTF-16 encoded code points, used in parsing and building UTF-16 paths.
// All of these are in the ASCII range so they can be cast directly to `u16`.
const SEP: u16 = b'\\' as _;
const QUERY: u16 = b'?' as _;
const COLON: u16 = b':' as _;
const U: u16 = b'U' as _;
const N: u16 = b'N' as _;
const C: u16 = b'C' as _;
// Early return if the path is too long to remove the verbatim prefix.
const LEGACY_MAX_PATH: usize = 260;
if path.len() > LEGACY_MAX_PATH {
return Ok(path);
}
match &path[..] {
// `\\?\C:\...` => `C:\...`
[SEP, SEP, QUERY, SEP, _, COLON, SEP, ..] => unsafe {
let lpfilename = path[4..].as_ptr();
fill_utf16_buf(
|buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()),
|full_path: &[u16]| {
if full_path == &path[4..path.len() - 1] { full_path.into() } else { path }
},
)
},
// `\\?\UNC\...` => `\\...`
[SEP, SEP, QUERY, SEP, U, N, C, SEP, ..] => unsafe {
// Change the `C` in `UNC\` to `\` so we can get a slice that starts with `\\`.
path[6] = b'\\' as u16;
let lpfilename = path[6..].as_ptr();
fill_utf16_buf(
|buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()),
|full_path: &[u16]| {
if full_path == &path[6..path.len() - 1] {
full_path.into()
} else {
// Restore the 'C' in "UNC".
path[6] = b'C' as u16;
path
}
},
)
},
// For everything else, leave the path unchanged.
_ => Ok(path),
}
}

View File

@ -267,8 +267,19 @@ impl Command {
program.len().checked_sub(5).and_then(|i| program.get(i..)),
Some([46, 98 | 66, 97 | 65, 116 | 84, 0] | [46, 99 | 67, 109 | 77, 100 | 68, 0])
);
let mut cmd_str =
make_command_line(&program, &self.args, self.force_quotes_enabled, is_batch_file)?;
let (program, mut cmd_str) = if is_batch_file {
(
command_prompt()?,
args::make_bat_command_line(
&args::to_user_path(program)?,
&self.args,
self.force_quotes_enabled,
)?,
)
} else {
let cmd_str = make_command_line(&self.program, &self.args, self.force_quotes_enabled)?;
(program, cmd_str)
};
cmd_str.push(0); // add null terminator
// stolen from the libuv code.
@ -719,30 +730,17 @@ fn zeroed_process_information() -> c::PROCESS_INFORMATION {
// 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: &[u16],
args: &[Arg],
force_quotes: bool,
is_batch_file: bool,
) -> io::Result<Vec<u16>> {
fn make_command_line(argv0: &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();
// CreateFileW has special handling for .bat and .cmd files, which means we
// need to add an extra pair of quotes surrounding the whole command line
// so they are properly passed on to the script.
// See issue #91991.
if is_batch_file {
cmd.push(b'"' as u16);
}
// Always quote the program name so CreateProcess to avoid ambiguity when
// the child process parses its arguments.
// Note that quotes aren't escaped here because they can't be used in arg0.
// But that's ok because file paths can't contain quotes.
cmd.push(b'"' as u16);
cmd.extend_from_slice(prog.strip_suffix(&[0]).unwrap_or(prog));
cmd.extend(argv0.encode_wide());
cmd.push(b'"' as u16);
for arg in args {
@ -752,6 +750,16 @@ fn make_command_line(
Ok(cmd)
}
// Get `cmd.exe` for use with bat scripts, encoded as a UTF-16 string.
fn command_prompt() -> io::Result<Vec<u16>> {
let mut system: Vec<u16> = super::fill_utf16_buf(
|buf, size| unsafe { c::GetSystemDirectoryW(buf, size) },
|buf| buf.into(),
)?;
system.extend("\\cmd.exe".encode_utf16().chain([0]));
Ok(system)
}
fn make_envp(maybe_env: Option<BTreeMap<EnvKey, OsString>>) -> io::Result<(*mut c_void, Vec<u16>)> {
// On Windows we pass an "environment block" which is not a char**, but
// rather a concatenation of null-terminated k=v\0 sequences, with a final

View File

@ -3,12 +3,11 @@ use super::Arg;
use crate::env;
use crate::ffi::{OsStr, OsString};
use crate::process::Command;
use crate::sys::to_u16s;
#[test]
fn test_raw_args() {
let command_line = &make_command_line(
&to_u16s("quoted exe").unwrap(),
OsStr::new("quoted exe"),
&[
Arg::Regular(OsString::from("quote me")),
Arg::Raw(OsString::from("quote me *not*")),
@ -17,7 +16,6 @@ fn test_raw_args() {
Arg::Regular(OsString::from("optional-quotes")),
],
false,
false,
)
.unwrap();
assert_eq!(
@ -30,10 +28,9 @@ fn test_raw_args() {
fn test_make_command_line() {
fn test_wrapper(prog: &str, args: &[&str], force_quotes: bool) -> String {
let command_line = &make_command_line(
&to_u16s(prog).unwrap(),
OsStr::new(prog),
&args.iter().map(|a| Arg::Regular(OsString::from(a))).collect::<Vec<_>>(),
force_quotes,
false,
)
.unwrap();
String::from_utf16(command_line).unwrap()