diff --git a/RELEASES.md b/RELEASES.md
index a40b9797d4a3f49872ba57790a6b581f387e9225..e44001421810a1c15c114db340b30171a9c538b0 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -4,6 +4,7 @@ Version 1.77.2 (2024-04-09)
- [CVE-2024-24576: fix escaping of Windows batch file arguments in `std::process::Command`](https://blog.rust-lang.org/2024/04/09/cve-2024-24576.html)
+- Fix `Command`'s batch files argument escaping not working when file name has trailing whitespace or periods (CVE-2024-43402).
Version 1.72.0 (2023-08-24)
==========================
diff --git a/library/std/src/ffi/os_str.rs b/library/std/src/ffi/os_str.rs
index e7bad9d542c4d814df443c18b9781119507cf590..e5c1e6a4fbc3ef35fee570747bf1712716a7724e 100644
--- a/library/std/src/ffi/os_str.rs
+++ b/library/std/src/ffi/os_str.rs
@@ -707,7 +707,7 @@ pub fn new + ?Sized>(s: &S) -> &OsStr {
///
/// [conversions]: super#conversions
#[inline]
- #[unstable(feature = "os_str_bytes", issue = "111544")]
+ #[stable(feature = "os_str_bytes", since = "1.71.0")]
pub unsafe fn from_os_str_bytes_unchecked(bytes: &[u8]) -> &Self {
Self::from_inner(Slice::from_os_str_bytes_unchecked(bytes))
}
@@ -820,6 +820,26 @@ pub fn to_os_string(&self) -> OsString {
OsString { inner: self.inner.to_owned() }
}
+ /// Converts an OS string slice to a byte slice. To convert the byte slice back into an OS
+ /// string slice, use the [`OsStr::from_encoded_bytes_unchecked`] function.
+ ///
+ /// The byte encoding is an unspecified, platform-specific, self-synchronizing superset of UTF-8.
+ /// By being a self-synchronizing superset of UTF-8, this encoding is also a superset of 7-bit
+ /// ASCII.
+ ///
+ /// Note: As the encoding is unspecified, any sub-slice of bytes that is not valid UTF-8 should
+ /// be treated as opaque and only comparable within the same rust version built for the same
+ /// target platform. For example, sending the slice over the network or storing it in a file
+ /// will likely result in incompatible byte slices. See [`OsString`] for more encoding details
+ /// and [`std::ffi`] for platform-specific, specified conversions.
+ ///
+ /// [`std::ffi`]: crate::ffi
+ #[inline]
+ #[stable(feature = "os_str_bytes", since = "1.71.0")]
+ pub fn as_encoded_bytes(&self) -> &[u8] {
+ self.inner.as_encoded_bytes()
+ }
+
/// Checks whether the `OsStr` is empty.
///
/// # Examples
@@ -897,7 +917,7 @@ pub fn into_os_string(self: Box) -> OsString {
///
/// [`std::ffi`]: crate::ffi
#[inline]
- #[unstable(feature = "os_str_bytes", issue = "111544")]
+ #[stable(feature = "os_str_bytes", since = "1.71.0")]
pub fn as_os_str_bytes(&self) -> &[u8] {
self.inner.as_os_str_bytes()
}
diff --git a/library/std/src/sys/pal/windows/args.rs b/library/std/src/sys/pal/windows/args.rs
deleted file mode 100644
index 48bcb89e669ee98852de36940c2b5a3d96780912..0000000000000000000000000000000000000000
--- a/library/std/src/sys/pal/windows/args.rs
+++ /dev/null
@@ -1,460 +0,0 @@
-//! The Windows command line is just a string
-//!
-//!
-//! This module implements the parsing necessary to turn that string into a list of arguments.
-
-#[cfg(test)]
-mod tests;
-
-use super::os::current_exe;
-use crate::ffi::{OsStr, OsString};
-use crate::fmt;
-use crate::io;
-use crate::num::NonZeroU16;
-use crate::os::windows::prelude::*;
-use crate::path::{Path, PathBuf};
-use crate::sys::path::get_long_path;
-use crate::sys::process::ensure_no_nuls;
-use crate::sys::{c, to_u16s};
-use crate::sys_common::wstr::WStrUnits;
-use crate::sys_common::AsInner;
-use crate::vec;
-
-use crate::iter;
-
-/// This is the const equivalent to `NonZeroU16::new(n).unwrap()`
-///
-/// FIXME: This can be removed once `Option::unwrap` is stably const.
-/// See the `const_option` feature (#67441).
-const fn non_zero_u16(n: u16) -> NonZeroU16 {
- match NonZeroU16::new(n) {
- Some(n) => n,
- None => panic!("called `unwrap` on a `None` value"),
- }
-}
-
-pub fn args() -> Args {
- // SAFETY: `GetCommandLineW` returns a pointer to a null terminated UTF-16
- // string so it's safe for `WStrUnits` to use.
- unsafe {
- let lp_cmd_line = c::GetCommandLineW();
- let parsed_args_list = parse_lp_cmd_line(WStrUnits::new(lp_cmd_line), || {
- current_exe().map(PathBuf::into_os_string).unwrap_or_else(|_| OsString::new())
- });
-
- Args { parsed_args_list: parsed_args_list.into_iter() }
- }
-}
-
-/// Implements the Windows command-line argument parsing algorithm.
-///
-/// Microsoft's documentation for the Windows CLI argument format can be found at
-///
-///
-/// A more in-depth explanation is here:
-///
-///
-/// Windows includes a function to do command line parsing in shell32.dll.
-/// However, this is not used for two reasons:
-///
-/// 1. Linking with that DLL causes the process to be registered as a GUI application.
-/// GUI applications add a bunch of overhead, even if no windows are drawn. See
-/// .
-///
-/// 2. It does not follow the modern C/C++ argv rules outlined in the first two links above.
-///
-/// This function was tested for equivalence to the C/C++ parsing rules using an
-/// extensive test suite available at
-/// .
-fn parse_lp_cmd_line<'a, F: Fn() -> OsString>(
- lp_cmd_line: Option>,
- exe_name: F,
-) -> Vec {
- const BACKSLASH: NonZeroU16 = non_zero_u16(b'\\' as u16);
- const QUOTE: NonZeroU16 = non_zero_u16(b'"' as u16);
- const TAB: NonZeroU16 = non_zero_u16(b'\t' as u16);
- const SPACE: NonZeroU16 = non_zero_u16(b' ' as u16);
-
- let mut ret_val = Vec::new();
- // If the cmd line pointer is null or it points to an empty string then
- // return the name of the executable as argv[0].
- if lp_cmd_line.as_ref().and_then(|cmd| cmd.peek()).is_none() {
- ret_val.push(exe_name());
- return ret_val;
- }
- let mut code_units = lp_cmd_line.unwrap();
-
- // The executable name at the beginning is special.
- let mut in_quotes = false;
- let mut cur = Vec::new();
- for w in &mut code_units {
- match w {
- // A quote mark always toggles `in_quotes` no matter what because
- // there are no escape characters when parsing the executable name.
- QUOTE => in_quotes = !in_quotes,
- // If not `in_quotes` then whitespace ends argv[0].
- SPACE | TAB if !in_quotes => break,
- // In all other cases the code unit is taken literally.
- _ => cur.push(w.get()),
- }
- }
- // Skip whitespace.
- code_units.advance_while(|w| w == SPACE || w == TAB);
- ret_val.push(OsString::from_wide(&cur));
-
- // Parse the arguments according to these rules:
- // * All code units are taken literally except space, tab, quote and backslash.
- // * When not `in_quotes`, space and tab separate arguments. Consecutive spaces and tabs are
- // treated as a single separator.
- // * A space or tab `in_quotes` is taken literally.
- // * A quote toggles `in_quotes` mode unless it's escaped. An escaped quote is taken literally.
- // * A quote can be escaped if preceded by an odd number of backslashes.
- // * If any number of backslashes is immediately followed by a quote then the number of
- // backslashes is halved (rounding down).
- // * Backslashes not followed by a quote are all taken literally.
- // * If `in_quotes` then a quote can also be escaped using another quote
- // (i.e. two consecutive quotes become one literal quote).
- let mut cur = Vec::new();
- let mut in_quotes = false;
- while let Some(w) = code_units.next() {
- match w {
- // If not `in_quotes`, a space or tab ends the argument.
- SPACE | TAB if !in_quotes => {
- ret_val.push(OsString::from_wide(&cur[..]));
- cur.truncate(0);
-
- // Skip whitespace.
- code_units.advance_while(|w| w == SPACE || w == TAB);
- }
- // Backslashes can escape quotes or backslashes but only if consecutive backslashes are followed by a quote.
- BACKSLASH => {
- let backslash_count = code_units.advance_while(|w| w == BACKSLASH) + 1;
- if code_units.peek() == Some(QUOTE) {
- cur.extend(iter::repeat(BACKSLASH.get()).take(backslash_count / 2));
- // The quote is escaped if there are an odd number of backslashes.
- if backslash_count % 2 == 1 {
- code_units.next();
- cur.push(QUOTE.get());
- }
- } else {
- // If there is no quote on the end then there is no escaping.
- cur.extend(iter::repeat(BACKSLASH.get()).take(backslash_count));
- }
- }
- // If `in_quotes` and not backslash escaped (see above) then a quote either
- // unsets `in_quote` or is escaped by another quote.
- QUOTE if in_quotes => match code_units.peek() {
- // Two consecutive quotes when `in_quotes` produces one literal quote.
- Some(QUOTE) => {
- cur.push(QUOTE.get());
- code_units.next();
- }
- // Otherwise set `in_quotes`.
- Some(_) => in_quotes = false,
- // The end of the command line.
- // Push `cur` even if empty, which we do by breaking while `in_quotes` is still set.
- None => break,
- },
- // If not `in_quotes` and not BACKSLASH escaped (see above) then a quote sets `in_quote`.
- QUOTE => in_quotes = true,
- // Everything else is always taken literally.
- _ => cur.push(w.get()),
- }
- }
- // Push the final argument, if any.
- if !cur.is_empty() || in_quotes {
- ret_val.push(OsString::from_wide(&cur[..]));
- }
- ret_val
-}
-
-pub struct Args {
- parsed_args_list: vec::IntoIter,
-}
-
-impl fmt::Debug for Args {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- self.parsed_args_list.as_slice().fmt(f)
- }
-}
-
-impl Iterator for Args {
- type Item = OsString;
- fn next(&mut self) -> Option {
- self.parsed_args_list.next()
- }
- fn size_hint(&self) -> (usize, Option) {
- self.parsed_args_list.size_hint()
- }
-}
-
-impl DoubleEndedIterator for Args {
- fn next_back(&mut self) -> Option {
- self.parsed_args_list.next_back()
- }
-}
-
-impl ExactSizeIterator for Args {
- fn len(&self) -> usize {
- self.parsed_args_list.len()
- }
-}
-
-#[derive(Debug)]
-pub(crate) enum Arg {
- /// Add quotes (if needed)
- Regular(OsString),
- /// Append raw string without quoting
- Raw(OsString),
-}
-
-enum Quote {
- // Every arg is quoted
- Always,
- // Whitespace and empty args are quoted
- Auto,
- // Arg appended without any changes (#29494)
- Never,
-}
-
-pub(crate) fn append_arg(cmd: &mut Vec, arg: &Arg, force_quotes: bool) -> io::Result<()> {
- let (arg, quote) = match arg {
- Arg::Regular(arg) => (arg, if force_quotes { Quote::Always } else { Quote::Auto }),
- Arg::Raw(arg) => (arg, Quote::Never),
- };
-
- // If an argument has 0 characters then we need to quote it to ensure
- // that it actually gets passed through on the command line or otherwise
- // it will be dropped entirely when parsed on the other end.
- ensure_no_nuls(arg)?;
- let arg_bytes = arg.as_encoded_bytes();
- let (quote, escape) = match quote {
- Quote::Always => (true, true),
- Quote::Auto => {
- (arg_bytes.iter().any(|c| *c == b' ' || *c == b'\t') || arg_bytes.is_empty(), true)
- }
- Quote::Never => (false, false),
- };
- if quote {
- cmd.push('"' as u16);
- }
-
- let mut backslashes: usize = 0;
- for x in arg.encode_wide() {
- if escape {
- if x == '\\' as u16 {
- backslashes += 1;
- } else {
- if x == '"' as u16 {
- // Add n+1 backslashes to total 2n+1 before internal '"'.
- cmd.extend((0..=backslashes).map(|_| '\\' as u16));
- }
- backslashes = 0;
- }
- }
- cmd.push(x);
- }
-
- if quote {
- // Add n backslashes to total 2n before ending '"'.
- cmd.extend((0..backslashes).map(|_| '\\' as u16));
- cmd.push('"' as u16);
- }
- Ok(())
-}
-
-fn append_bat_arg(cmd: &mut Vec, arg: &OsStr, mut quote: bool) -> io::Result<()> {
- ensure_no_nuls(arg)?;
- // If an argument has 0 characters then we need to quote it to ensure
- // that it actually gets passed through on the command line or otherwise
- // it will be dropped entirely when parsed on the other end.
- //
- // We also need to quote the argument if it ends with `\` to guard against
- // bat usage such as `"%~2"` (i.e. force quote arguments) otherwise a
- // trailing slash will escape the closing quote.
- if arg.is_empty() || arg.as_encoded_bytes().last() == Some(&b'\\') {
- quote = true;
- }
- for cp in arg.as_inner().inner.code_points() {
- if let Some(cp) = cp.to_char() {
- // Rather than trying to find every ascii symbol that must be quoted,
- // we assume that all ascii symbols must be quoted unless they're known to be good.
- // We also quote Unicode control blocks for good measure.
- // Note an unquoted `\` is fine so long as the argument isn't otherwise quoted.
- static UNQUOTED: &str = r"#$*+-./:?@\_";
- let ascii_needs_quotes =
- cp.is_ascii() && !(cp.is_ascii_alphanumeric() || UNQUOTED.contains(cp));
- if ascii_needs_quotes || cp.is_control() {
- quote = true;
- }
- }
- }
-
- if quote {
- cmd.push('"' as u16);
- }
- // Loop through the string, escaping `\` only if followed by `"`.
- // And escaping `"` by doubling them.
- let mut backslashes: usize = 0;
- for x in arg.encode_wide() {
- if x == '\\' as u16 {
- backslashes += 1;
- } else {
- if x == '"' as u16 {
- // Add n backslashes to total 2n before internal `"`.
- cmd.extend((0..backslashes).map(|_| '\\' as u16));
- // Appending an additional double-quote acts as an escape.
- cmd.push(b'"' as u16)
- } else if x == '%' as u16 || x == '\r' as u16 {
- // yt-dlp hack: replaces `%` with `%%cd:~,%` to stop %VAR% being expanded as an environment variable.
- //
- // # Explanation
- //
- // cmd supports extracting a substring from a variable using the following syntax:
- // %variable:~start_index,end_index%
- //
- // In the above command `cd` is used as the variable and the start_index and end_index are left blank.
- // `cd` is a built-in variable that dynamically expands to the current directory so it's always available.
- // Explicitly omitting both the start and end index creates a zero-length substring.
- //
- // Therefore it all resolves to nothing. However, by doing this no-op we distract cmd.exe
- // from potentially expanding %variables% in the argument.
- cmd.extend_from_slice(&[
- '%' as u16, '%' as u16, 'c' as u16, 'd' as u16, ':' as u16, '~' as u16,
- ',' as u16,
- ]);
- }
- backslashes = 0;
- }
- cmd.push(x);
- }
- if quote {
- // Add n backslashes to total 2n before ending `"`.
- cmd.extend((0..backslashes).map(|_| '\\' as u16));
- cmd.push('"' as u16);
- }
- Ok(())
-}
-
-pub(crate) fn make_bat_command_line(
- script: &[u16],
- args: &[Arg],
- force_quotes: bool,
-) -> io::Result> {
- const INVALID_ARGUMENT_ERROR: io::Error =
- io::const_io_error!(io::ErrorKind::InvalidInput, r#"batch file arguments are invalid"#);
- // 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.
- // Using /e:ON enables "command extensions" which is essential for the `%` hack to work.
- let mut cmd: Vec = "cmd.exe /e:ON /v:OFF /d /c \"".encode_utf16().collect();
-
- // Push the script name surrounded by its quote pair.
- cmd.push(b'"' as u16);
- // Windows file names cannot contain a `"` character or end with `\\`.
- // If the script name does then return an error.
- if script.contains(&(b'"' as u16)) || script.last() == Some(&(b'\\' as u16)) {
- return Err(io::const_io_error!(
- io::ErrorKind::InvalidInput,
- "Windows file names may not contain `\"` or end with `\\`"
- ));
- }
- 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);
- match arg {
- Arg::Regular(arg_os) => {
- let arg_bytes = arg_os.as_encoded_bytes();
- // Disallow \r and \n as they may truncate the arguments.
- const DISALLOWED: &[u8] = b"\r\n";
- if arg_bytes.iter().any(|c| DISALLOWED.contains(c)) {
- return Err(INVALID_ARGUMENT_ERROR);
- }
- append_bat_arg(&mut cmd, arg_os, force_quotes)?;
- }
- _ => {
- // Raw arguments are passed on as-is.
- // It's the user's responsibility to properly handle arguments in this case.
- 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(path: &Path) -> io::Result> {
- from_wide_to_user_path(to_u16s(path)?)
-}
-pub(crate) fn from_wide_to_user_path(mut path: Vec) -> io::Result> {
- use super::fill_utf16_buf;
- use crate::ptr;
-
- // 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] {
- let mut path: Vec = full_path.into();
- path.push(0);
- path
- } 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] {
- let mut path: Vec = full_path.into();
- path.push(0);
- path
- } else {
- // Restore the 'C' in "UNC".
- path[6] = b'C' as u16;
- path
- }
- },
- )
- },
- // For everything else, leave the path unchanged.
- _ => get_long_path(path, false),
- }
-}
diff --git a/library/std/src/sys/windows/api.rs b/library/std/src/sys/windows/api.rs
new file mode 100644
index 0000000000000000000000000000000000000000..00f14c42750558cc08c875917833da7ff5c2d4ef
--- /dev/null
+++ b/library/std/src/sys/windows/api.rs
@@ -0,0 +1,82 @@
+/// Creates a UTF-16 string from a str without null termination.
+pub macro utf16($str:expr) {{
+ const UTF8: &str = $str;
+ const UTF16_LEN: usize = crate::sys::windows::api::utf16_len(UTF8);
+ const UTF16: [u16; UTF16_LEN] = crate::sys::windows::api::to_utf16(UTF8);
+ &UTF16
+}}
+
+/// Gets the UTF-16 length of a UTF-8 string, for use in the wide_str macro.
+pub const fn utf16_len(s: &str) -> usize {
+ let s = s.as_bytes();
+ let mut i = 0;
+ let mut len = 0;
+ while i < s.len() {
+ // the length of a UTF-8 encoded code-point is given by the number of
+ // leading ones, except in the case of ASCII.
+ let utf8_len = match s[i].leading_ones() {
+ 0 => 1,
+ n => n as usize,
+ };
+ i += utf8_len;
+ // Note that UTF-16 surrogates (U+D800 to U+DFFF) are not encodable as UTF-8,
+ // so (unlike with WTF-8) we don't have to worry about how they'll get re-encoded.
+ len += if utf8_len < 4 { 1 } else { 2 };
+ }
+ len
+}
+
+/// Const convert UTF-8 to UTF-16, for use in the wide_str macro.
+///
+/// Note that this is designed for use in const contexts so is not optimized.
+pub const fn to_utf16(s: &str) -> [u16; UTF16_LEN] {
+ let mut output = [0_u16; UTF16_LEN];
+ let mut pos = 0;
+ let s = s.as_bytes();
+ let mut i = 0;
+ while i < s.len() {
+ match s[i].leading_ones() {
+ // Decode UTF-8 based on its length.
+ // See https://en.wikipedia.org/wiki/UTF-8
+ 0 => {
+ // ASCII is the same in both encodings
+ output[pos] = s[i] as u16;
+ i += 1;
+ pos += 1;
+ }
+ 2 => {
+ // Bits: 110xxxxx 10xxxxxx
+ output[pos] = ((s[i] as u16 & 0b11111) << 6) | (s[i + 1] as u16 & 0b111111);
+ i += 2;
+ pos += 1;
+ }
+ 3 => {
+ // Bits: 1110xxxx 10xxxxxx 10xxxxxx
+ output[pos] = ((s[i] as u16 & 0b1111) << 12)
+ | ((s[i + 1] as u16 & 0b111111) << 6)
+ | (s[i + 2] as u16 & 0b111111);
+ i += 3;
+ pos += 1;
+ }
+ 4 => {
+ // Bits: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ let mut c = ((s[i] as u32 & 0b111) << 18)
+ | ((s[i + 1] as u32 & 0b111111) << 12)
+ | ((s[i + 2] as u32 & 0b111111) << 6)
+ | (s[i + 3] as u32 & 0b111111);
+ // re-encode as UTF-16 (see https://en.wikipedia.org/wiki/UTF-16)
+ // - Subtract 0x10000 from the code point
+ // - For the high surrogate, shift right by 10 then add 0xD800
+ // - For the low surrogate, take the low 10 bits then add 0xDC00
+ c -= 0x10000;
+ output[pos] = ((c >> 10) + 0xD800) as u16;
+ output[pos + 1] = ((c & 0b1111111111) + 0xDC00) as u16;
+ i += 4;
+ pos += 2;
+ }
+ // valid UTF-8 cannot have any other values
+ _ => unreachable!(),
+ }
+ }
+ output
+}
diff --git a/library/std/src/sys/windows/mod.rs b/library/std/src/sys/windows/mod.rs
index bcc172b0fae36a08101efb86ad00a1ed9ee56839..c7aa869f68a04b231275453c15330f6c5dee8631 100644
--- a/library/std/src/sys/windows/mod.rs
+++ b/library/std/src/sys/windows/mod.rs
@@ -12,6 +12,8 @@
#[macro_use]
pub mod compat;
+pub mod api;
+
pub mod alloc;
pub mod args;
pub mod c;
diff --git a/library/std/src/sys/windows/os_str.rs b/library/std/src/sys/windows/os_str.rs
index 16c4f55c6879a7af92789cc6eda65ea92a223945..f1e07ffbccaf2a5046b456af6d2125a83e6d30ad 100644
--- a/library/std/src/sys/windows/os_str.rs
+++ b/library/std/src/sys/windows/os_str.rs
@@ -151,6 +151,11 @@ pub fn into_rc(&self) -> Rc {
}
impl Slice {
+ #[inline]
+ pub fn as_encoded_bytes(&self) -> &[u8] {
+ &self.inner.as_inner()
+ }
+
#[inline]
pub fn as_os_str_bytes(&self) -> &[u8] {
self.inner.as_bytes()
diff --git a/library/std/src/sys/windows/path.rs b/library/std/src/sys/windows/path.rs
index c9c2d10e6c444f42bddaef7294ca96b464ac577e..ed4f0f75f37eb82e1a7650e2a50aaec180d99b82 100644
--- a/library/std/src/sys/windows/path.rs
+++ b/library/std/src/sys/windows/path.rs
@@ -3,6 +3,7 @@
use crate::io;
use crate::path::{Path, PathBuf, Prefix};
use crate::ptr;
+use crate::sys::api::utf16;
#[cfg(test)]
mod tests;
@@ -20,6 +21,10 @@ pub fn is_verbatim_sep(b: u8) -> bool {
b == b'\\'
}
+pub fn is_verbatim(path: &[u16]) -> bool {
+ path.starts_with(utf16!(r"\\?\")) || path.starts_with(utf16!(r"\??\"))
+}
+
/// Returns true if `path` looks like a lone filename.
pub(crate) fn is_file_name(path: &OsStr) -> bool {
!path.as_os_str_bytes().iter().copied().any(is_sep_byte)
diff --git a/library/std/src/sys/windows/process.rs b/library/std/src/sys/windows/process.rs
index e3493cbb85094732ee1838405722621118c51c85..96175e11c9bfadcd31e32f079989ce21f5ca1a01 100644
--- a/library/std/src/sys/windows/process.rs
+++ b/library/std/src/sys/windows/process.rs
@@ -259,10 +259,24 @@ pub fn spawn(
};
let program = resolve_exe(&self.program, || env::var_os("PATH"), child_paths)?;
// Case insensitive "ends_with" of UTF-16 encoded ".bat" or ".cmd"
- let is_batch_file = matches!(
- 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 has_bat_extension = |program: &[u16]| {
+ matches!(
+ // Case insensitive "ends_with" of UTF-16 encoded ".bat" or ".cmd"
+ program.len().checked_sub(4).and_then(|i| program.get(i..)),
+ Some([46, 98 | 66, 97 | 65, 116 | 84] | [46, 99 | 67, 109 | 77, 100 | 68])
+ )
+ };
+ let is_batch_file = if path::is_verbatim(&program) {
+ has_bat_extension(&program[..program.len() - 1])
+ } else {
+ super::fill_utf16_buf(
+ |buffer, size| unsafe {
+ // resolve the path so we can test the final file name.
+ c::GetFullPathNameW(program.as_ptr(), size, buffer, ptr::null_mut())
+ },
+ |program| has_bat_extension(program),
+ )?
+ };
let (program, mut cmd_str) = if is_batch_file {
(
command_prompt()?,
diff --git a/tests/ui/std/windows-bat-args.rs b/tests/ui/std/windows-bat-args.rs
index d2d5fe76c84d53915aed1043500bc94c9d111e61..7d1715954019bde6f2bbfcafc6b19a5248757ca3 100644
--- a/tests/ui/std/windows-bat-args.rs
+++ b/tests/ui/std/windows-bat-args.rs
@@ -32,7 +32,9 @@ fn parent() {
let bat2 = String::from(bat.to_str().unwrap());
bat.set_file_name("windows-bat-args3.bat");
let bat3 = String::from(bat.to_str().unwrap());
- let bat = [bat1.as_str(), bat2.as_str(), bat3.as_str()];
+ bat.set_file_name("windows-bat-args1.bat .. ");
+ let bat4 = String::from(bat.to_str().unwrap());
+ let bat = [bat1.as_str(), bat2.as_str(), bat3.as_str(), bat4.as_str()];
check_args(&bat, &["a", "b"]).unwrap();
check_args(&bat, &["c is for cat", "d is for dog"]).unwrap();
diff --git a/tests/ui/std/windows-bat-args1.bat b/tests/ui/std/windows-bat-args1.bat
index edd36bd5530e96c9256e410f6e3adec3f6c8c4a7..15d32263f9541902a852304b29ca9e9b24b2cf5c 100644
--- a/tests/ui/std/windows-bat-args1.bat
+++ b/tests/ui/std/windows-bat-args1.bat
@@ -1 +1 @@
-@a.exe %*
+@a.exe %*
\ No newline at end of file
diff --git a/tests/ui/std/windows-bat-args2.bat b/tests/ui/std/windows-bat-args2.bat
index 8d5a7dd8a9e00769877a5ad197b1cda819e14204..b5326eee1d7ca9702d6a2bbdd7e0fea35d12c25a 100644
--- a/tests/ui/std/windows-bat-args2.bat
+++ b/tests/ui/std/windows-bat-args2.bat
@@ -1 +1 @@
-@a.exe %1 %2 %3 %4 %5 %6 %7 %8 %9
+@a.exe %1 %2 %3 %4 %5 %6 %7 %8 %9
\ No newline at end of file
diff --git a/tests/ui/std/windows-bat-args3.bat b/tests/ui/std/windows-bat-args3.bat
index 7fe360a6d36d0d2485bb7cd890818de735239372..1f29aceb12f024a64fabcb097cab7000c9e93086 100644
--- a/tests/ui/std/windows-bat-args3.bat
+++ b/tests/ui/std/windows-bat-args3.bat
@@ -1 +1 @@
-@a.exe "%~1" "%~2" "%~3" "%~4" "%~5" "%~6" "%~7" "%~8" "%~9"
+@a.exe "%~1" "%~2" "%~3" "%~4" "%~5" "%~6" "%~7" "%~8" "%~9"
\ No newline at end of file