Skip to content

Commit d9a1f9a

Browse files
committed
Windows: Resolve Command program without using the current directory
1 parent 07f54d9 commit d9a1f9a

File tree

4 files changed

+216
-29
lines changed

4 files changed

+216
-29
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,7 @@ if #[cfg(not(target_vendor = "uwp"))] {
734734
lpSecurityAttributes: LPSECURITY_ATTRIBUTES,
735735
) -> BOOL;
736736
pub fn SetThreadStackGuarantee(_size: *mut c_ulong) -> BOOL;
737+
pub fn GetWindowsDirectoryW(lpBuffer: LPWSTR, uSize: UINT) -> UINT;
737738
}
738739
}
739740
}
@@ -773,6 +774,7 @@ extern "system" {
773774
pub fn LeaveCriticalSection(CriticalSection: *mut CRITICAL_SECTION);
774775
pub fn DeleteCriticalSection(CriticalSection: *mut CRITICAL_SECTION);
775776

777+
pub fn GetSystemDirectoryW(lpBuffer: LPWSTR, uSize: UINT) -> UINT;
776778
pub fn RemoveDirectoryW(lpPathName: LPCWSTR) -> BOOL;
777779
pub fn SetFileAttributesW(lpFileName: LPCWSTR, dwFileAttributes: DWORD) -> BOOL;
778780
pub fn SetLastError(dwErrCode: DWORD);

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

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
use super::{c, fill_utf16_buf, to_u16s};
2-
use crate::ffi::OsStr;
2+
use crate::ffi::{OsStr, OsString};
33
use crate::io;
44
use crate::mem;
5-
use crate::path::Path;
6-
use crate::path::Prefix;
5+
use crate::path::{Path, PathBuf, Prefix};
76
use crate::ptr;
87

98
#[cfg(test)]
@@ -32,6 +31,25 @@ pub fn is_verbatim_sep(b: u8) -> bool {
3231
b == b'\\'
3332
}
3433

34+
/// Returns true if `path` looks like a lone filename.
35+
pub(crate) fn is_file_name(path: &OsStr) -> bool {
36+
!path.bytes().iter().copied().any(is_sep_byte)
37+
}
38+
pub(crate) fn has_trailing_slash(path: &OsStr) -> bool {
39+
let is_verbatim = path.bytes().starts_with(br"\\?\");
40+
let is_separator = if is_verbatim { is_verbatim_sep } else { is_sep_byte };
41+
if let Some(&c) = path.bytes().last() { is_separator(c) } else { false }
42+
}
43+
44+
/// Appends a suffix to a path.
45+
///
46+
/// Can be used to append an extension without removing an existing extension.
47+
pub(crate) fn append_suffix(path: PathBuf, suffix: &OsStr) -> PathBuf {
48+
let mut path = OsString::from(path);
49+
path.push(suffix);
50+
path.into()
51+
}
52+
3553
pub fn parse_prefix(path: &OsStr) -> Option<Prefix<'_>> {
3654
use Prefix::{DeviceNS, Disk, Verbatim, VerbatimDisk, VerbatimUNC, UNC};
3755

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

+141-26
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,24 @@ use crate::cmp;
77
use crate::collections::BTreeMap;
88
use crate::convert::{TryFrom, TryInto};
99
use crate::env;
10-
use crate::env::split_paths;
10+
use crate::env::consts::{EXE_EXTENSION, EXE_SUFFIX};
1111
use crate::ffi::{OsStr, OsString};
1212
use crate::fmt;
13-
use crate::fs;
1413
use crate::io::{self, Error, ErrorKind};
1514
use crate::mem;
1615
use crate::num::NonZeroI32;
17-
use crate::os::windows::ffi::OsStrExt;
16+
use crate::os::windows::ffi::{OsStrExt, OsStringExt};
1817
use crate::os::windows::io::{AsRawHandle, FromRawHandle, IntoRawHandle};
19-
use crate::path::Path;
18+
use crate::path::{Path, PathBuf};
2019
use crate::ptr;
2120
use crate::sys::c;
2221
use crate::sys::c::NonZeroDWORD;
23-
use crate::sys::cvt;
2422
use crate::sys::fs::{File, OpenOptions};
2523
use crate::sys::handle::Handle;
24+
use crate::sys::path;
2625
use crate::sys::pipe::{self, AnonPipe};
2726
use crate::sys::stdio;
27+
use crate::sys::{cvt, to_u16s};
2828
use crate::sys_common::mutex::StaticMutex;
2929
use crate::sys_common::process::{CommandEnv, CommandEnvs};
3030
use crate::sys_common::{AsInner, IntoInner};
@@ -258,31 +258,19 @@ impl Command {
258258
needs_stdin: bool,
259259
) -> io::Result<(Process, StdioPipes)> {
260260
let maybe_env = self.env.capture_if_changed();
261-
// To have the spawning semantics of unix/windows stay the same, we need
262-
// to read the *child's* PATH if one is provided. See #15149 for more
263-
// details.
264-
let program = maybe_env.as_ref().and_then(|env| {
265-
if let Some(v) = env.get(&EnvKey::new("PATH")) {
266-
// Split the value and test each path to see if the
267-
// program exists.
268-
for path in split_paths(&v) {
269-
let path = path
270-
.join(self.program.to_str().unwrap())
271-
.with_extension(env::consts::EXE_EXTENSION);
272-
if fs::metadata(&path).is_ok() {
273-
return Some(path.into_os_string());
274-
}
275-
}
276-
}
277-
None
278-
});
279261

280262
let mut si = zeroed_startupinfo();
281263
si.cb = mem::size_of::<c::STARTUPINFO>() as c::DWORD;
282264
si.dwFlags = c::STARTF_USESTDHANDLES;
283265

284-
let program = program.as_ref().unwrap_or(&self.program);
285-
let mut cmd_str = make_command_line(program, &self.args, self.force_quotes_enabled)?;
266+
let child_paths = if let Some(env) = maybe_env.as_ref() {
267+
env.get(&EnvKey::new("PATH")).map(|s| s.as_os_str())
268+
} else {
269+
None
270+
};
271+
let program = resolve_exe(&self.program, child_paths)?;
272+
let mut cmd_str =
273+
make_command_line(program.as_os_str(), &self.args, self.force_quotes_enabled)?;
286274
cmd_str.push(0); // add null terminator
287275

288276
// stolen from the libuv code.
@@ -321,9 +309,10 @@ impl Command {
321309
si.hStdOutput = stdout.as_raw_handle();
322310
si.hStdError = stderr.as_raw_handle();
323311

312+
let program = to_u16s(&program)?;
324313
unsafe {
325314
cvt(c::CreateProcessW(
326-
ptr::null(),
315+
program.as_ptr(),
327316
cmd_str.as_mut_ptr(),
328317
ptr::null_mut(),
329318
ptr::null_mut(),
@@ -361,6 +350,132 @@ impl fmt::Debug for Command {
361350
}
362351
}
363352

353+
// Resolve `exe_path` to the executable name.
354+
//
355+
// * If the path is simply a file name then use the paths given by `search_paths` to find the executable.
356+
// * Otherwise use the `exe_path` as given.
357+
//
358+
// This function may also append `.exe` to the name. The rationale for doing so is as follows:
359+
//
360+
// It is a very strong convention that Windows executables have the `exe` extension.
361+
// In Rust, it is common to omit this extension.
362+
// Therefore this functions first assumes `.exe` was intended.
363+
// It falls back to the plain file name if a full path is given and the extension is omitted
364+
// or if only a file name is given and it already contains an extension.
365+
fn resolve_exe<'a>(exe_path: &'a OsStr, child_paths: Option<&OsStr>) -> io::Result<PathBuf> {
366+
// Early return if there is no filename.
367+
if exe_path.is_empty() || path::has_trailing_slash(exe_path) {
368+
return Err(io::Error::new_const(
369+
io::ErrorKind::InvalidInput,
370+
&"program path has no file name",
371+
));
372+
}
373+
// Test if the file name has the `exe` extension.
374+
// This does a case-insensitive `ends_with`.
375+
let has_exe_suffix = if exe_path.len() >= EXE_SUFFIX.len() {
376+
exe_path.bytes()[exe_path.len() - EXE_SUFFIX.len()..]
377+
.eq_ignore_ascii_case(EXE_SUFFIX.as_bytes())
378+
} else {
379+
false
380+
};
381+
382+
// If `exe_path` is an absolute path or a sub-path then don't search `PATH` for it.
383+
if !path::is_file_name(exe_path) {
384+
if has_exe_suffix {
385+
// The application name is a path to a `.exe` file.
386+
// Let `CreateProcessW` figure out if it exists or not.
387+
return Ok(exe_path.into());
388+
}
389+
let mut path = PathBuf::from(exe_path);
390+
391+
// Append `.exe` if not already there.
392+
path = path::append_suffix(path, EXE_SUFFIX.as_ref());
393+
if path.try_exists().unwrap_or(false) {
394+
return Ok(path);
395+
} else {
396+
// It's ok to use `set_extension` here because the intent is to
397+
// remove the extension that was just added.
398+
path.set_extension("");
399+
return Ok(path);
400+
}
401+
} else {
402+
ensure_no_nuls(exe_path)?;
403+
// From the `CreateProcessW` docs:
404+
// > If the file name does not contain an extension, .exe is appended.
405+
// Note that this rule only applies when searching paths.
406+
let has_extension = exe_path.bytes().contains(&b'.');
407+
408+
// Search the directories given by `search_paths`.
409+
let result = search_paths(child_paths, |mut path| {
410+
path.push(&exe_path);
411+
if !has_extension {
412+
path.set_extension(EXE_EXTENSION);
413+
}
414+
if let Ok(true) = path.try_exists() { Some(path) } else { None }
415+
});
416+
if let Some(path) = result {
417+
return Ok(path);
418+
}
419+
}
420+
// If we get here then the executable cannot be found.
421+
Err(io::Error::new_const(io::ErrorKind::NotFound, &"program not found"))
422+
}
423+
424+
// Calls `f` for every path that should be used to find an executable.
425+
// Returns once `f` returns the path to an executable or all paths have been searched.
426+
fn search_paths<F>(child_paths: Option<&OsStr>, mut f: F) -> Option<PathBuf>
427+
where
428+
F: FnMut(PathBuf) -> Option<PathBuf>,
429+
{
430+
// 1. Child paths
431+
// This is for consistency with Rust's historic behaviour.
432+
if let Some(paths) = child_paths {
433+
for path in env::split_paths(paths).filter(|p| !p.as_os_str().is_empty()) {
434+
if let Some(path) = f(path) {
435+
return Some(path);
436+
}
437+
}
438+
}
439+
440+
// 2. Application path
441+
if let Ok(mut app_path) = env::current_exe() {
442+
app_path.pop();
443+
if let Some(path) = f(app_path) {
444+
return Some(path);
445+
}
446+
}
447+
448+
// 3 & 4. System paths
449+
// SAFETY: This uses `fill_utf16_buf` to safely call the OS functions.
450+
unsafe {
451+
if let Ok(Some(path)) = super::fill_utf16_buf(
452+
|buf, size| c::GetSystemDirectoryW(buf, size),
453+
|buf| f(PathBuf::from(OsString::from_wide(buf))),
454+
) {
455+
return Some(path);
456+
}
457+
#[cfg(not(target_vendor = "uwp"))]
458+
{
459+
if let Ok(Some(path)) = super::fill_utf16_buf(
460+
|buf, size| c::GetWindowsDirectoryW(buf, size),
461+
|buf| f(PathBuf::from(OsString::from_wide(buf))),
462+
) {
463+
return Some(path);
464+
}
465+
}
466+
}
467+
468+
// 5. Parent paths
469+
if let Some(parent_paths) = env::var_os("PATH") {
470+
for path in env::split_paths(&parent_paths).filter(|p| !p.as_os_str().is_empty()) {
471+
if let Some(path) = f(path) {
472+
return Some(path);
473+
}
474+
}
475+
}
476+
None
477+
}
478+
364479
impl Stdio {
365480
fn to_handle(&self, stdio_id: c::DWORD, pipe: &mut Option<AnonPipe>) -> io::Result<Handle> {
366481
match *self {

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

+52
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,55 @@ fn windows_env_unicode_case() {
128128
}
129129
}
130130
}
131+
132+
// UWP applications run in a restricted environment which means this test may not work.
133+
#[cfg(not(target_vendor = "uwp"))]
134+
#[test]
135+
fn windows_exe_resolver() {
136+
use super::resolve_exe;
137+
use crate::io;
138+
139+
// Test a full path, with and without the `exe` extension.
140+
let mut current_exe = env::current_exe().unwrap();
141+
assert!(resolve_exe(current_exe.as_ref(), None).is_ok());
142+
current_exe.set_extension("");
143+
assert!(resolve_exe(current_exe.as_ref(), None).is_ok());
144+
145+
// Test lone file names.
146+
assert!(resolve_exe(OsStr::new("cmd"), None).is_ok());
147+
assert!(resolve_exe(OsStr::new("cmd.exe"), None).is_ok());
148+
assert!(resolve_exe(OsStr::new("cmd.EXE"), None).is_ok());
149+
assert!(resolve_exe(OsStr::new("fc"), None).is_ok());
150+
151+
// Invalid file names should return InvalidInput.
152+
assert_eq!(resolve_exe(OsStr::new(""), None).unwrap_err().kind(), io::ErrorKind::InvalidInput);
153+
assert_eq!(
154+
resolve_exe(OsStr::new("\0"), None).unwrap_err().kind(),
155+
io::ErrorKind::InvalidInput
156+
);
157+
// Trailing slash, therefore there's no file name component.
158+
assert_eq!(
159+
resolve_exe(OsStr::new(r"C:\Path\to\"), None).unwrap_err().kind(),
160+
io::ErrorKind::InvalidInput
161+
);
162+
163+
/*
164+
Some of the following tests may need to be changed if you are deliberately
165+
changing the behaviour of `resolve_exe`.
166+
*/
167+
168+
let paths = env::var_os("PATH").unwrap();
169+
env::set_var("PATH", "");
170+
171+
assert_eq!(resolve_exe(OsStr::new("rustc"), None).unwrap_err().kind(), io::ErrorKind::NotFound);
172+
173+
let child_paths = Some(paths.as_os_str());
174+
assert!(resolve_exe(OsStr::new("rustc"), child_paths).is_ok());
175+
176+
// The resolver looks in system directories even when `PATH` is empty.
177+
assert!(resolve_exe(OsStr::new("cmd.exe"), None).is_ok());
178+
179+
// The application's directory is also searched.
180+
let current_exe = env::current_exe().unwrap();
181+
assert!(resolve_exe(current_exe.file_name().unwrap().as_ref(), None).is_ok());
182+
}

0 commit comments

Comments
 (0)