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