Skip to content

Commit 756ffb8

Browse files
committed
Auto merge of #95246 - ChrisDenton:command-args, r=joshtriplett
Windows Command: Don't run batch files using verbatim paths Fixes #95178 Note that the first commit does some minor refactoring (moving command line argument building to args.rs). The actual changes are in the second.
2 parents fedbe5d + 4a0ec50 commit 756ffb8

File tree

5 files changed

+226
-104
lines changed

5 files changed

+226
-104
lines changed

library/std/src/process/tests.rs

+21
Original file line numberDiff line numberDiff line change
@@ -435,3 +435,24 @@ fn run_bat_script() {
435435
assert!(output.status.success());
436436
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "Hello, fellow Rustaceans!");
437437
}
438+
439+
// See issue #95178
440+
#[test]
441+
#[cfg(windows)]
442+
fn run_canonical_bat_script() {
443+
let tempdir = crate::sys_common::io::test::tmpdir();
444+
let script_path = tempdir.join("hello.cmd");
445+
446+
crate::fs::write(&script_path, "@echo Hello, %~1!").unwrap();
447+
448+
// Try using a canonical path
449+
let output = Command::new(&script_path.canonicalize().unwrap())
450+
.arg("fellow Rustaceans")
451+
.stdout(crate::process::Stdio::piped())
452+
.spawn()
453+
.unwrap()
454+
.wait_with_output()
455+
.unwrap();
456+
assert!(output.status.success());
457+
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "Hello, fellow Rustaceans!");
458+
}

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

+159
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ mod tests;
88

99
use crate::ffi::OsString;
1010
use crate::fmt;
11+
use crate::io;
1112
use crate::marker::PhantomData;
1213
use crate::num::NonZeroU16;
1314
use crate::os::windows::prelude::*;
1415
use crate::path::PathBuf;
1516
use crate::ptr::NonNull;
1617
use crate::sys::c;
18+
use crate::sys::process::ensure_no_nuls;
1719
use crate::sys::windows::os::current_exe;
1820
use crate::vec;
1921

@@ -234,3 +236,160 @@ impl Iterator for WStrUnits<'_> {
234236
}
235237
}
236238
}
239+
240+
#[derive(Debug)]
241+
pub(crate) enum Arg {
242+
/// Add quotes (if needed)
243+
Regular(OsString),
244+
/// Append raw string without quoting
245+
Raw(OsString),
246+
}
247+
248+
enum Quote {
249+
// Every arg is quoted
250+
Always,
251+
// Whitespace and empty args are quoted
252+
Auto,
253+
// Arg appended without any changes (#29494)
254+
Never,
255+
}
256+
257+
pub(crate) fn append_arg(cmd: &mut Vec<u16>, arg: &Arg, force_quotes: bool) -> io::Result<()> {
258+
let (arg, quote) = match arg {
259+
Arg::Regular(arg) => (arg, if force_quotes { Quote::Always } else { Quote::Auto }),
260+
Arg::Raw(arg) => (arg, Quote::Never),
261+
};
262+
263+
// If an argument has 0 characters then we need to quote it to ensure
264+
// that it actually gets passed through on the command line or otherwise
265+
// it will be dropped entirely when parsed on the other end.
266+
ensure_no_nuls(arg)?;
267+
let arg_bytes = arg.bytes();
268+
let (quote, escape) = match quote {
269+
Quote::Always => (true, true),
270+
Quote::Auto => {
271+
(arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') || arg_bytes.is_empty(), true)
272+
}
273+
Quote::Never => (false, false),
274+
};
275+
if quote {
276+
cmd.push('"' as u16);
277+
}
278+
279+
let mut backslashes: usize = 0;
280+
for x in arg.encode_wide() {
281+
if escape {
282+
if x == '\\' as u16 {
283+
backslashes += 1;
284+
} else {
285+
if x == '"' as u16 {
286+
// Add n+1 backslashes to total 2n+1 before internal '"'.
287+
cmd.extend((0..=backslashes).map(|_| '\\' as u16));
288+
}
289+
backslashes = 0;
290+
}
291+
}
292+
cmd.push(x);
293+
}
294+
295+
if quote {
296+
// Add n backslashes to total 2n before ending '"'.
297+
cmd.extend((0..backslashes).map(|_| '\\' as u16));
298+
cmd.push('"' as u16);
299+
}
300+
Ok(())
301+
}
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+
// Windows file names cannot contain a `"` character or end with `\\`.
317+
// If the script name does then return an error.
318+
if script.contains(&(b'"' as u16)) || script.last() == Some(&(b'\\' as u16)) {
319+
return Err(io::const_io_error!(
320+
io::ErrorKind::InvalidInput,
321+
"Windows file names may not contain `\"` or end with `\\`"
322+
));
323+
}
324+
cmd.extend_from_slice(script.strip_suffix(&[0]).unwrap_or(script));
325+
cmd.push(b'"' as u16);
326+
327+
// Append the arguments.
328+
// FIXME: This needs tests to ensure that the arguments are properly
329+
// reconstructed by the batch script by default.
330+
for arg in args {
331+
cmd.push(' ' as u16);
332+
append_arg(&mut cmd, arg, force_quotes)?;
333+
}
334+
335+
// Close the quote we left opened earlier.
336+
cmd.push(b'"' as u16);
337+
338+
Ok(cmd)
339+
}
340+
341+
/// Takes a path and tries to return a non-verbatim path.
342+
///
343+
/// This is necessary because cmd.exe does not support verbatim paths.
344+
pub(crate) fn to_user_path(mut path: Vec<u16>) -> io::Result<Vec<u16>> {
345+
use crate::ptr;
346+
use crate::sys::windows::fill_utf16_buf;
347+
348+
// UTF-16 encoded code points, used in parsing and building UTF-16 paths.
349+
// All of these are in the ASCII range so they can be cast directly to `u16`.
350+
const SEP: u16 = b'\\' as _;
351+
const QUERY: u16 = b'?' as _;
352+
const COLON: u16 = b':' as _;
353+
const U: u16 = b'U' as _;
354+
const N: u16 = b'N' as _;
355+
const C: u16 = b'C' as _;
356+
357+
// Early return if the path is too long to remove the verbatim prefix.
358+
const LEGACY_MAX_PATH: usize = 260;
359+
if path.len() > LEGACY_MAX_PATH {
360+
return Ok(path);
361+
}
362+
363+
match &path[..] {
364+
// `\\?\C:\...` => `C:\...`
365+
[SEP, SEP, QUERY, SEP, _, COLON, SEP, ..] => unsafe {
366+
let lpfilename = path[4..].as_ptr();
367+
fill_utf16_buf(
368+
|buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()),
369+
|full_path: &[u16]| {
370+
if full_path == &path[4..path.len() - 1] { full_path.into() } else { path }
371+
},
372+
)
373+
},
374+
// `\\?\UNC\...` => `\\...`
375+
[SEP, SEP, QUERY, SEP, U, N, C, SEP, ..] => unsafe {
376+
// Change the `C` in `UNC\` to `\` so we can get a slice that starts with `\\`.
377+
path[6] = b'\\' as u16;
378+
let lpfilename = path[6..].as_ptr();
379+
fill_utf16_buf(
380+
|buffer, size| c::GetFullPathNameW(lpfilename, size, buffer, ptr::null_mut()),
381+
|full_path: &[u16]| {
382+
if full_path == &path[6..path.len() - 1] {
383+
full_path.into()
384+
} else {
385+
// Restore the 'C' in "UNC".
386+
path[6] = b'C' as u16;
387+
path
388+
}
389+
},
390+
)
391+
},
392+
// For everything else, leave the path unchanged.
393+
_ => Ok(path),
394+
}
395+
}

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

+30-86
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use crate::os::windows::ffi::{OsStrExt, OsStringExt};
1717
use crate::os::windows::io::{AsRawHandle, FromRawHandle, IntoRawHandle};
1818
use crate::path::{Path, PathBuf};
1919
use crate::ptr;
20+
use crate::sys::args::{self, Arg};
2021
use crate::sys::c;
2122
use crate::sys::c::NonZeroDWORD;
2223
use crate::sys::cvt;
@@ -27,7 +28,7 @@ use crate::sys::pipe::{self, AnonPipe};
2728
use crate::sys::stdio;
2829
use crate::sys_common::mutex::StaticMutex;
2930
use crate::sys_common::process::{CommandEnv, CommandEnvs};
30-
use crate::sys_common::{AsInner, IntoInner};
31+
use crate::sys_common::IntoInner;
3132

3233
use libc::{c_void, EXIT_FAILURE, EXIT_SUCCESS};
3334

@@ -147,7 +148,7 @@ impl AsRef<OsStr> for EnvKey {
147148
}
148149
}
149150

150-
fn ensure_no_nuls<T: AsRef<OsStr>>(str: T) -> io::Result<T> {
151+
pub(crate) fn ensure_no_nuls<T: AsRef<OsStr>>(str: T) -> io::Result<T> {
151152
if str.as_ref().encode_wide().any(|b| b == 0) {
152153
Err(io::const_io_error!(ErrorKind::InvalidInput, "nul byte found in provided data"))
153154
} else {
@@ -182,14 +183,6 @@ pub struct StdioPipes {
182183
pub stderr: Option<AnonPipe>,
183184
}
184185

185-
#[derive(Debug)]
186-
enum Arg {
187-
/// Add quotes (if needed)
188-
Regular(OsString),
189-
/// Append raw string without quoting
190-
Raw(OsString),
191-
}
192-
193186
impl Command {
194187
pub fn new(program: &OsStr) -> Command {
195188
Command {
@@ -275,8 +268,19 @@ impl Command {
275268
program.len().checked_sub(5).and_then(|i| program.get(i..)),
276269
Some([46, 98 | 66, 97 | 65, 116 | 84, 0] | [46, 99 | 67, 109 | 77, 100 | 68, 0])
277270
);
278-
let mut cmd_str =
279-
make_command_line(&program, &self.args, self.force_quotes_enabled, is_batch_file)?;
271+
let (program, mut cmd_str) = if is_batch_file {
272+
(
273+
command_prompt()?,
274+
args::make_bat_command_line(
275+
&args::to_user_path(program)?,
276+
&self.args,
277+
self.force_quotes_enabled,
278+
)?,
279+
)
280+
} else {
281+
let cmd_str = make_command_line(&self.program, &self.args, self.force_quotes_enabled)?;
282+
(program, cmd_str)
283+
};
280284
cmd_str.push(0); // add null terminator
281285

282286
// stolen from the libuv code.
@@ -730,96 +734,36 @@ fn zeroed_process_information() -> c::PROCESS_INFORMATION {
730734
}
731735
}
732736

733-
enum Quote {
734-
// Every arg is quoted
735-
Always,
736-
// Whitespace and empty args are quoted
737-
Auto,
738-
// Arg appended without any changes (#29494)
739-
Never,
740-
}
741-
742737
// Produces a wide string *without terminating null*; returns an error if
743738
// `prog` or any of the `args` contain a nul.
744-
fn make_command_line(
745-
prog: &[u16],
746-
args: &[Arg],
747-
force_quotes: bool,
748-
is_batch_file: bool,
749-
) -> io::Result<Vec<u16>> {
739+
fn make_command_line(argv0: &OsStr, args: &[Arg], force_quotes: bool) -> io::Result<Vec<u16>> {
750740
// Encode the command and arguments in a command line string such
751741
// that the spawned process may recover them using CommandLineToArgvW.
752742
let mut cmd: Vec<u16> = Vec::new();
753743

754-
// CreateFileW has special handling for .bat and .cmd files, which means we
755-
// need to add an extra pair of quotes surrounding the whole command line
756-
// so they are properly passed on to the script.
757-
// See issue #91991.
758-
if is_batch_file {
759-
cmd.push(b'"' as u16);
760-
}
761-
762744
// Always quote the program name so CreateProcess to avoid ambiguity when
763745
// the child process parses its arguments.
764746
// Note that quotes aren't escaped here because they can't be used in arg0.
765747
// But that's ok because file paths can't contain quotes.
766748
cmd.push(b'"' as u16);
767-
cmd.extend_from_slice(prog.strip_suffix(&[0]).unwrap_or(prog));
749+
cmd.extend(argv0.encode_wide());
768750
cmd.push(b'"' as u16);
769751

770752
for arg in args {
771753
cmd.push(' ' as u16);
772-
let (arg, quote) = match arg {
773-
Arg::Regular(arg) => (arg, if force_quotes { Quote::Always } else { Quote::Auto }),
774-
Arg::Raw(arg) => (arg, Quote::Never),
775-
};
776-
append_arg(&mut cmd, arg, quote)?;
777-
}
778-
if is_batch_file {
779-
cmd.push(b'"' as u16);
780-
}
781-
return Ok(cmd);
782-
783-
fn append_arg(cmd: &mut Vec<u16>, arg: &OsStr, quote: Quote) -> io::Result<()> {
784-
// If an argument has 0 characters then we need to quote it to ensure
785-
// that it actually gets passed through on the command line or otherwise
786-
// it will be dropped entirely when parsed on the other end.
787-
ensure_no_nuls(arg)?;
788-
let arg_bytes = &arg.as_inner().inner.as_inner();
789-
let (quote, escape) = match quote {
790-
Quote::Always => (true, true),
791-
Quote::Auto => {
792-
(arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') || arg_bytes.is_empty(), true)
793-
}
794-
Quote::Never => (false, false),
795-
};
796-
if quote {
797-
cmd.push('"' as u16);
798-
}
799-
800-
let mut backslashes: usize = 0;
801-
for x in arg.encode_wide() {
802-
if escape {
803-
if x == '\\' as u16 {
804-
backslashes += 1;
805-
} else {
806-
if x == '"' as u16 {
807-
// Add n+1 backslashes to total 2n+1 before internal '"'.
808-
cmd.extend((0..=backslashes).map(|_| '\\' as u16));
809-
}
810-
backslashes = 0;
811-
}
812-
}
813-
cmd.push(x);
814-
}
815-
816-
if quote {
817-
// Add n backslashes to total 2n before ending '"'.
818-
cmd.extend((0..backslashes).map(|_| '\\' as u16));
819-
cmd.push('"' as u16);
820-
}
821-
Ok(())
754+
args::append_arg(&mut cmd, arg, force_quotes)?;
822755
}
756+
Ok(cmd)
757+
}
758+
759+
// Get `cmd.exe` for use with bat scripts, encoded as a UTF-16 string.
760+
fn command_prompt() -> io::Result<Vec<u16>> {
761+
let mut system: Vec<u16> = super::fill_utf16_buf(
762+
|buf, size| unsafe { c::GetSystemDirectoryW(buf, size) },
763+
|buf| buf.into(),
764+
)?;
765+
system.extend("\\cmd.exe".encode_utf16().chain([0]));
766+
Ok(system)
823767
}
824768

825769
fn make_envp(maybe_env: Option<BTreeMap<EnvKey, OsString>>) -> io::Result<(*mut c_void, Vec<u16>)> {

0 commit comments

Comments
 (0)