use std::collections::HashMap;
use std::path::Path;
#[path = "helper/src/win32_which.rs"]
mod win32_which;
pub(crate) fn locate_windows_executable(
args: &[String],
os_env_vars: Option<&HashMap<String, Option<String>>>,
working_dir: &Path,
) -> Result<Vec<String>, String> {
let cmd = Path::new(&args[0]);
if cmd.is_absolute() {
return Ok(args.to_vec());
}
let env_var = |name: &str| -> Option<Option<String>> {
os_env_vars.and_then(|env| {
env.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.clone())
})
};
let path_var = match env_var("PATH") {
Some(set_or_unset) => set_or_unset.unwrap_or_default(),
None => std::env::var("PATH").unwrap_or_default(),
};
let pathext = match env_var("PATHEXT") {
Some(set_or_unset) => set_or_unset.unwrap_or_default(),
None => std::env::var("PATHEXT").unwrap_or_default(),
};
let search_path = format!("{};{}", working_dir.display(), path_var);
match win32_which::locate_in(&args[0], &search_path, &pathext, working_dir) {
Some(found) => {
let mut result = args.to_vec();
result[0] = found.to_string_lossy().into_owned();
Ok(result)
}
None => Err(format!("Could not find executable file: {}", args[0])),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn touch(path: &Path) {
std::fs::write(path, "").unwrap();
}
fn env_with_path(dirs: &[&Path]) -> HashMap<String, Option<String>> {
let joined = dirs
.iter()
.map(|d| d.to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(";");
HashMap::from([("Path".to_string(), Some(joined))])
}
fn args(cmd: &str) -> Vec<String> {
vec![cmd.to_string(), "arg1".to_string()]
}
#[test]
fn bat_earlier_in_path_beats_exe_later() {
let tmp = tempfile::TempDir::new().unwrap();
let dir_a = tmp.path().join("a");
let dir_b = tmp.path().join("b");
std::fs::create_dir_all(&dir_a).unwrap();
std::fs::create_dir_all(&dir_b).unwrap();
touch(&dir_a.join("tool.bat"));
touch(&dir_b.join("tool.exe"));
let wd = tmp.path();
let resolved =
locate_windows_executable(&args("tool"), Some(&env_with_path(&[&dir_a, &dir_b])), wd)
.unwrap();
assert_eq!(
resolved[0].to_lowercase(),
dir_a.join("tool.bat").to_string_lossy().to_lowercase(),
"earliest PATH directory must win across extensions"
);
assert_eq!(resolved[1], "arg1", "remaining args are preserved");
}
#[test]
fn bare_name_resolves_bat_only_match() {
let tmp = tempfile::TempDir::new().unwrap();
let dir_a = tmp.path().join("a");
std::fs::create_dir_all(&dir_a).unwrap();
touch(&dir_a.join("onlybat.bat"));
let resolved = locate_windows_executable(
&args("onlybat"),
Some(&env_with_path(&[&dir_a])),
tmp.path(),
)
.unwrap();
assert_eq!(
resolved[0].to_lowercase(),
dir_a.join("onlybat.bat").to_string_lossy().to_lowercase()
);
}
#[test]
fn working_dir_wins_over_path() {
let tmp = tempfile::TempDir::new().unwrap();
let dir_a = tmp.path().join("a");
let wd = tmp.path().join("wd");
std::fs::create_dir_all(&dir_a).unwrap();
std::fs::create_dir_all(&wd).unwrap();
touch(&dir_a.join("dup.bat"));
touch(&wd.join("dup.bat"));
let resolved =
locate_windows_executable(&args("dup"), Some(&env_with_path(&[&dir_a])), &wd).unwrap();
assert_eq!(
resolved[0].to_lowercase(),
wd.join("dup.bat").to_string_lossy().to_lowercase()
);
}
#[test]
fn absent_command_is_error_not_process_path_fallback() {
let tmp = tempfile::TempDir::new().unwrap();
let empty = tmp.path().join("empty");
std::fs::create_dir_all(&empty).unwrap();
let err =
locate_windows_executable(&args("whoami"), Some(&env_with_path(&[&empty])), &empty)
.unwrap_err();
assert_eq!(err, "Could not find executable file: whoami");
}
#[test]
fn absolute_path_passthrough() {
let a = args(r"C:\Windows\System32\whoami.exe");
let resolved = locate_windows_executable(&a, None, Path::new(".")).unwrap();
assert_eq!(resolved, a);
}
#[test]
fn explicitly_unset_path_does_not_fall_back_to_process_path() {
let env: HashMap<String, Option<String>> = HashMap::from([("PATH".to_string(), None)]);
let err =
locate_windows_executable(&args("whoami"), Some(&env), Path::new(".")).unwrap_err();
assert_eq!(err, "Could not find executable file: whoami");
}
#[test]
fn explicitly_unset_path_still_searches_working_dir() {
let tmp = tempfile::TempDir::new().unwrap();
touch(&tmp.path().join("wdonly.bat"));
let env: HashMap<String, Option<String>> = HashMap::from([("PATH".to_string(), None)]);
let resolved = locate_windows_executable(&args("wdonly"), Some(&env), tmp.path()).unwrap();
assert_eq!(
resolved[0].to_lowercase(),
tmp.path()
.join("wdonly.bat")
.to_string_lossy()
.to_lowercase()
);
}
#[test]
fn explicitly_unset_pathext_uses_default_list() {
let tmp = tempfile::TempDir::new().unwrap();
touch(&tmp.path().join("tool.exe"));
std::fs::write(tmp.path().join("script.ps1"), "").unwrap();
let env: HashMap<String, Option<String>> = HashMap::from([
(
"PATH".to_string(),
Some(tmp.path().to_string_lossy().into_owned()),
),
("PATHEXT".to_string(), None),
]);
let resolved = locate_windows_executable(&args("tool"), Some(&env), tmp.path()).unwrap();
assert!(resolved[0].to_lowercase().ends_with("tool.exe"));
let err =
locate_windows_executable(&args("script.ps1"), Some(&env), tmp.path()).unwrap_err();
assert_eq!(err, "Could not find executable file: script.ps1");
}
#[test]
fn falls_back_to_process_path() {
let resolved =
locate_windows_executable(&args("whoami"), Some(&HashMap::new()), Path::new("."))
.unwrap();
assert!(
resolved[0].to_lowercase().ends_with("whoami.exe"),
"expected whoami.exe from process PATH; got {}",
resolved[0]
);
}
}