fix(windows): prefer PATHEXT variants over bare filename in resolver (#5364)

`find_executable` used to short-circuit on the first exists()-and-is_file
match and only consult PATHEXT if no bare file was present. That returned
the wrong file for tools whose Windows installer ships both a Unix-style
launcher and a `.bat` alongside it. The reported case is Composer, whose
setup places bare `composer` (a Unix launcher, invalid PE on Windows) next
to `composer.bat`. The bare file was resolved and handed to CreateProcessW,
which rejected it with ERROR_BAD_EXE_FORMAT.

On Windows, when the candidate has no user-supplied extension, walk PATHEXT
first and only fall back to the bare match if nothing matches. This mirrors
cmd.exe's and powershell's resolution.
This commit is contained in:
divens
2026-07-21 12:18:04 +02:00
committed by GitHub
parent 68362d4cf0
commit 812ad861bc
2 changed files with 24 additions and 15 deletions
+1
View File
@@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* fix: occasional high resource utilization and sluggishness on machines with tens of thousands of concurent processes (https://github.com/zellij-org/zellij/pull/5324)
* feat: allow opening panes/tabs from the CLI without changing focus (https://github.com/zellij-org/zellij/pull/5346)
* feat: allow binding `SetDarkTheme`, `SetLightTheme` and `ToggleTheme` as direct keybinding actions (https://github.com/zellij-org/zellij/pull/5302)
* fix(windows): prefer PATHEXT variants over bare filename when resolving commands (fixes launching `composer` and similar) (https://github.com/zellij-org/zellij/pull/5364)
* feat: allow disabling ctrl-mouse-scroll to resize panes (https://github.com/zellij-org/zellij/pull/5283)
* fix: scrolling region line wrap behavior at bottom edge (https://github.com/zellij-org/zellij/pull/5357)
* fix: properly pad lines before CUF (https://github.com/zellij-org/zellij/pull/5377)
+23 -15
View File
@@ -37,28 +37,36 @@ pub use async_trait::async_trait;
/// Check whether a candidate path refers to an executable file, considering
/// PATHEXT extensions on Windows (e.g. `.exe`, `.cmd`).
///
/// On Windows, when the candidate has no extension we try each PATHEXT
/// variant BEFORE the bare match, mirroring cmd.exe's resolution. Tools like
/// Composer install both `composer` (a Unix launcher) and `composer.bat`
/// (the Windows launcher) side by side; returning the bare file would send
/// a non-PE binary to CreateProcessW and fail with ERROR_BAD_EXE_FORMAT.
fn find_executable(candidate: &std::path::Path) -> Option<PathBuf> {
if candidate.exists() && candidate.is_file() {
return Some(candidate.to_path_buf());
}
#[cfg(windows)]
{
if let Some(pathext) = env::var_os("PATHEXT") {
let pathext = pathext.to_string_lossy();
for ext in pathext.split(';') {
let ext = ext.trim();
if ext.is_empty() {
continue;
}
let mut with_ext = candidate.as_os_str().to_os_string();
with_ext.push(ext);
let with_ext_path = PathBuf::from(with_ext);
if with_ext_path.exists() && with_ext_path.is_file() {
return Some(with_ext_path);
if candidate.extension().is_none() {
if let Some(pathext) = env::var_os("PATHEXT") {
let pathext = pathext.to_string_lossy();
for ext in pathext.split(';') {
let ext = ext.trim();
if ext.is_empty() {
continue;
}
let mut with_ext = candidate.as_os_str().to_os_string();
with_ext.push(ext);
let with_ext_path = PathBuf::from(with_ext);
if with_ext_path.exists() && with_ext_path.is_file() {
return Some(with_ext_path);
}
}
}
}
}
if candidate.exists() && candidate.is_file() {
return Some(candidate.to_path_buf());
}
None
}