From 4d1e950326ed938c67473e8b8273a0e643124283 Mon Sep 17 00:00:00 2001 From: Edward Thomson Date: Thu, 30 Oct 2025 14:13:27 -0700 Subject: [PATCH] fs: improve executable search on Windows Ensure that when we look for an executable on Windows that we add executable suffixes (`.exe`, `.cmd`). Without this, we would not support looking for (eg) `ssh`, since we actually need to identify a file named `ssh.exe` (or `ssh.cmd`) in `PATH`. --- src/util/fs_path.c | 53 +++++++++++++++++++++++++++++++-------- src/util/win32/path_w32.c | 49 +++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 15 deletions(-) diff --git a/src/util/fs_path.c b/src/util/fs_path.c index 255fefe32..ff0836ff8 100644 --- a/src/util/fs_path.c +++ b/src/util/fs_path.c @@ -611,6 +611,23 @@ bool git_fs_path_isfile(const char *path) return S_ISREG(st.st_mode) != 0; } +#ifdef GIT_WIN32 + +bool git_fs_path_isexecutable(const char *path) +{ + struct stat st; + + GIT_ASSERT_ARG_WITH_RETVAL(path, false); + + if (git__suffixcmp_icase(path, ".exe") != 0 && + git__suffixcmp_icase(path, ".cmd") != 0) + return false; + + return (p_stat(path, &st) == 0); +} + +#else + bool git_fs_path_isexecutable(const char *path) { struct stat st; @@ -623,6 +640,8 @@ bool git_fs_path_isexecutable(const char *path) ((st.st_mode & S_IXUSR) != 0); } +#endif + bool git_fs_path_islink(const char *path) { struct stat st; @@ -2032,9 +2051,10 @@ int git_fs_path_owner_is_system(bool *out, const char *path) return git_fs_path_owner_is(out, path, GIT_FS_PATH_OWNER_ADMINISTRATOR); } -int git_fs_path_find_executable(git_str *fullpath, const char *executable) -{ #ifdef GIT_WIN32 + +static int find_executable(git_str *fullpath, const char *executable) +{ git_win32_path fullpath_w, executable_w; int error; @@ -2047,20 +2067,17 @@ int git_fs_path_find_executable(git_str *fullpath, const char *executable) error = git_str_put_w(fullpath, fullpath_w, wcslen(fullpath_w)); return error; +} + #else + +static int find_executable(git_str *fullpath, const char *executable) +{ git_str path = GIT_STR_INIT; const char *current_dir, *term; size_t current_dirlen; bool found = false; - /* For qualified paths we do not look in PATH */ - if (strchr(executable, '/') != NULL) { - if (!git_fs_path_isexecutable(executable)) - return GIT_ENOTFOUND; - - return git_str_puts(fullpath, executable); - } - if (git__getenv(&path, "PATH") < 0) return -1; @@ -2102,5 +2119,19 @@ int git_fs_path_find_executable(git_str *fullpath, const char *executable) git_str_clear(fullpath); return GIT_ENOTFOUND; -#endif +} + +#endif + +int git_fs_path_find_executable(git_str *fullpath, const char *executable) +{ + /* For qualified paths we do not look in PATH */ + if (strchr(executable, '/') != NULL) { + if (!git_fs_path_isexecutable(executable)) + return GIT_ENOTFOUND; + + return git_str_puts(fullpath, executable); + } + + return find_executable(fullpath, executable); } diff --git a/src/util/win32/path_w32.c b/src/util/win32/path_w32.c index 7a559e45c..2e90d2e8e 100644 --- a/src/util/win32/path_w32.c +++ b/src/util/win32/path_w32.c @@ -253,26 +253,67 @@ static void win32_path_iter_dispose(struct win32_path_iter *iter) iter->current_dir = NULL; } +struct executable_suffix { + const wchar_t *suffix; + size_t len; +}; + int git_win32_path_find_executable(git_win32_path fullpath, wchar_t *exe) { struct win32_path_iter path_iter; const wchar_t *dir; size_t dir_len, exe_len = wcslen(exe); bool found = false; + static struct executable_suffix suffixes[] = { { NULL, 0 }, { L".exe", 4 }, { L".cmd", 4 } }; + size_t skip_bare = 1, i; if (win32_path_iter_init(&path_iter) < 0) return -1; - while (win32_path_iter_next(&dir, &dir_len, &path_iter) != GIT_ITEROVER) { - if (git_win32_path_join(fullpath, dir, dir_len, exe, exe_len) < 0) + /* see if the given executable has an executable suffix; if so we will + * look for the explicit name directly, as well as with added suffixes. + */ + for (i = 0; i < ARRAY_SIZE(suffixes); i++) { + struct executable_suffix *suffix = &suffixes[i]; + + if (!suffix->len) continue; - if (_waccess(fullpath, 0) == 0) { - found = true; + if (exe_len < suffix->len) + continue; + + if (memcmp(&exe[exe_len - suffix->len], suffix->suffix, suffix->len) == 0) { + skip_bare = 0; break; } } + while (win32_path_iter_next(&dir, &dir_len, &path_iter) != GIT_ITEROVER && !found) { + /* + * if the given name has an executable suffix, then try looking for it + * directly. in all cases, append executable extensions + * (".exe", ".cmd"...) + */ + for (i = skip_bare; i < ARRAY_SIZE(suffixes); i++) { + struct executable_suffix *suffix = &suffixes[i]; + + if (git_win32_path_join(fullpath, dir, dir_len, exe, exe_len) < 0) + continue; + + if (suffix->len) { + if (dir_len + exe_len + 1 + suffix->len > MAX_PATH) + continue; + + wcscat(fullpath, suffix->suffix); + } + + if (_waccess(fullpath, 0) == 0) { + found = true; + break; + } + } + } + win32_path_iter_dispose(&path_iter); if (found)