Skip to content

Commit 511c708

Browse files
committed
Windows: Allow running processes whose path exceeds the legacy MAX_PATH
Previously `Command` would simply fail when run using long paths. This also implements the `CreateProcessW` rules for appending `.exe`. This is to ensure both backwards compatibility and consistent behaviour between both long and short paths. However, this could be changed (and hopefully simplified) in the future.
1 parent 3ea37e4 commit 511c708

File tree

1 file changed

+82
-6
lines changed

1 file changed

+82
-6
lines changed

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

+82-6
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ use crate::sys::fs::{File, OpenOptions};
2525
use crate::sys::handle::Handle;
2626
use crate::sys::pipe::{self, AnonPipe};
2727
use crate::sys::stdio;
28+
use crate::sys::to_u16s;
2829
use crate::sys_common::mutex::StaticMutex;
2930
use crate::sys_common::process::{CommandEnv, CommandEnvs};
3031
use crate::sys_common::AsInner;
3132

33+
use super::path;
34+
3235
use libc::{c_void, EXIT_FAILURE, EXIT_SUCCESS};
3336

3437
////////////////////////////////////////////////////////////////////////////////
@@ -256,15 +259,17 @@ impl Command {
256259
}
257260
None
258261
});
262+
let program = program.as_ref().unwrap_or(&self.program);
263+
264+
let mut args = make_command_line(program, &self.args, self.force_quotes_enabled)?;
265+
args.push(0);
266+
267+
let application_name = CommandApp::new(program)?;
259268

260269
let mut si = zeroed_startupinfo();
261270
si.cb = mem::size_of::<c::STARTUPINFO>() as c::DWORD;
262271
si.dwFlags = c::STARTF_USESTDHANDLES;
263272

264-
let program = program.as_ref().unwrap_or(&self.program);
265-
let mut cmd_str = make_command_line(program, &self.args, self.force_quotes_enabled)?;
266-
cmd_str.push(0); // add null terminator
267-
268273
// stolen from the libuv code.
269274
let mut flags = self.flags | c::CREATE_UNICODE_ENVIRONMENT;
270275
if self.detach {
@@ -303,8 +308,8 @@ impl Command {
303308

304309
unsafe {
305310
cvt(c::CreateProcessW(
306-
ptr::null(),
307-
cmd_str.as_mut_ptr(),
311+
application_name.as_ptr(),
312+
args.as_mut_ptr(),
308313
ptr::null_mut(),
309314
ptr::null_mut(),
310315
c::TRUE,
@@ -552,6 +557,77 @@ fn zeroed_process_information() -> c::PROCESS_INFORMATION {
552557
}
553558
}
554559

560+
// Finds the application name that should be passed to `c::CreateProcessW`.
561+
struct CommandApp {
562+
application: Option<Vec<u16>>,
563+
}
564+
impl CommandApp {
565+
fn new(program: &OsStr) -> io::Result<Self> {
566+
let filename = match path::parse_filename(program) {
567+
Some(name) => name,
568+
None => {
569+
return Err(io::Error::new_const(
570+
io::ErrorKind::InvalidInput,
571+
&"the program name cannot be empty",
572+
));
573+
}
574+
};
575+
576+
if filename.len() == program.len() {
577+
// If the path is a plain file name then let the OS search for it.
578+
Ok(Self { application: None })
579+
} else {
580+
// Otherwise use the path, appending `.exe` if necessary.
581+
let mut utf16 = to_u16s(program)?;
582+
583+
// Possibly append `.exe` to the file name.
584+
//
585+
// The rules for doing so are fairly complex and are here to maintain
586+
// the previous behaviour. It should hopefully be simplified in the
587+
// future, once the preferred semantics are decided.
588+
//
589+
// Basically there are two different cases where `.exe` is added:
590+
//
591+
// If the path is absolute or starts with `.\` or `..\` then it will
592+
// first try the exact path given. If that path is not found then
593+
// `.exe` is appended and it's retried.
594+
//
595+
// Otherwise if the path is a plain file name or in a form such as
596+
// `directory\file` then `.exe` is always appended unless the file
597+
// name contains a `.` somewhere (i.e. it already has an extension).
598+
let has_root = match program.bytes() {
599+
// Drive paths such as `C:\`, `D:\`, etc.
600+
// This also includes relative drive paths like `C:`
601+
[_, b':', ..] => true,
602+
// Starts with: `\`, `.\` or `..\`
603+
[sep, ..] | [b'.', sep, ..] | [b'.', b'.', sep, ..] if path::is_sep_byte(*sep) => {
604+
true
605+
}
606+
// All other paths. For example:
607+
// `cmd`, `dir\cmd`, `dir\..\cmd`, etc.
608+
_ => false,
609+
};
610+
let has_extension = filename.bytes().contains(&b'.');
611+
612+
// If `try_exists` fails then treat it as existing and let
613+
// `c:CreateProcessW` try instead.
614+
if (has_root && !fs::try_exists(program).unwrap_or(true))
615+
|| (!has_root && !has_extension)
616+
{
617+
// remove null and add the `.exe` extension.
618+
utf16.pop();
619+
utf16.extend(env::consts::EXE_SUFFIX.encode_utf16());
620+
utf16.push(0);
621+
}
622+
623+
Ok(Self { application: Some(utf16) })
624+
}
625+
}
626+
fn as_ptr(&self) -> *const u16 {
627+
if let Some(app) = &self.application { app.as_ptr() } else { ptr::null() }
628+
}
629+
}
630+
555631
enum Quote {
556632
// Every arg is quoted
557633
Always,

0 commit comments

Comments
 (0)