From 23320a2f8314a929020ea08b2e8d9e45116cf7d5 Mon Sep 17 00:00:00 2001 From: Chris Denton Date: Wed, 23 Mar 2022 04:28:04 +0000 Subject: [PATCH] Command: handle exe and batch files separately --- library/std/src/sys/windows/args.rs | 86 ++++++++++++++++++++ library/std/src/sys/windows/process.rs | 42 ++++++---- library/std/src/sys/windows/process/tests.rs | 7 +- 3 files changed, 113 insertions(+), 22 deletions(-) diff --git a/library/std/src/sys/windows/args.rs b/library/std/src/sys/windows/args.rs index b9c303f9cce9..1d3d3013e7e1 100644 --- a/library/std/src/sys/windows/args.rs +++ b/library/std/src/sys/windows/args.rs @@ -299,3 +299,89 @@ pub(crate) fn append_arg(cmd: &mut Vec, arg: &Arg, force_quotes: bool) -> i } Ok(()) } + +pub(crate) fn make_bat_command_line( + script: &[u16], + args: &[Arg], + force_quotes: bool, +) -> io::Result> { + // 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 = "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) -> io::Result> { + 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), + } +} diff --git a/library/std/src/sys/windows/process.rs b/library/std/src/sys/windows/process.rs index c98294069f9d..394513e13d31 100644 --- a/library/std/src/sys/windows/process.rs +++ b/library/std/src/sys/windows/process.rs @@ -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> { +fn make_command_line(argv0: &OsStr, args: &[Arg], force_quotes: bool) -> io::Result> { // Encode the command and arguments in a command line string such // that the spawned process may recover them using CommandLineToArgvW. let mut cmd: Vec = 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> { + let mut system: Vec = 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>) -> io::Result<(*mut c_void, Vec)> { // 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 diff --git a/library/std/src/sys/windows/process/tests.rs b/library/std/src/sys/windows/process/tests.rs index cd535afb4a33..be3a0f4ed52a 100644 --- a/library/std/src/sys/windows/process/tests.rs +++ b/library/std/src/sys/windows/process/tests.rs @@ -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::>(), force_quotes, - false, ) .unwrap(); String::from_utf16(command_line).unwrap()