enh(File): proper implementation of File::getExecutablePath#5186
enh(File): proper implementation of File::getExecutablePath#5186
Conversation
0c3342b to
d333c0c
Compare
ddcfe7a to
d79620a
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves executable resolution semantics in Poco::File (and related ProcessRunner behavior) by making getExecutablePath() a resolver (CWD/PATH on Unix; SearchPathW + PATHEXT on Windows), simplifying canExecute(), and aligning tests with the new behavior.
Changes:
- Reworked
File::absolutePath(),File::getExecutablePath(),File::canExecute(), andexistsAnywhere()behavior; introducedfindInPath()helper. - Implemented new platform-specific executable resolution logic on Unix/Windows.
- Updated
ProcessRunner::start()andFileTestexpectations to usegetExecutablePath()/canExecute().
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| Foundation/testsuite/src/FileTest.cpp | Updates tests to validate the new executable-resolution behavior. |
| Foundation/src/ProcessRunner.cpp | Uses getExecutablePath()/canExecute() to validate commands before launching. |
| Foundation/src/File_WIN32U.cpp | Implements Windows executable resolution with SearchPathW and PATHEXT; updates WinAPI calls. |
| Foundation/src/File_UNIX.cpp | Implements Unix executable resolution and simplifies canExecuteImpl() using access(X_OK). |
| Foundation/src/File.cpp | Adds shared findInPath() helper; updates absolutePath(), existsAnywhere(), and canExecute(). |
| Foundation/include/Poco/File.h | Updates documentation for the changed absolutePath(), getExecutablePath(), existsAnywhere(), and canExecute() semantics. |
Comments suppressed due to low confidence (1)
Foundation/src/ProcessRunner.cpp:276
- ProcessRunner::start() validates using getExecutablePath()/canExecute(), but on Windows getExecutablePathImpl()/canExecuteImpl() can now accept PATHEXT entries like .CMD/.BAT. Process::launchImpl() uses CreateProcessW, which cannot launch .cmd/.bat directly, so validation may succeed and launch still fails. Either restrict Windows canExecute/getExecutablePath to CreateProcess-launchable extensions (e.g. .exe/.com) or add explicit handling to run scripts via cmd.exe /c.
std::string execPath = File(_cmd).getExecutablePath();
if (execPath.empty())
{
throw Poco::FileNotFoundException(
Poco::format("ProcessRunner::start(%s): command not found", _cmd));
}
else if (!File(execPath).canExecute())
{
throw Poco::ExecuteFileException(
Poco::format("ProcessRunner::start(%s): cannot execute", _cmd));
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Test 5: Non-executable file returns empty for bare name | ||
| { | ||
| File f("subdir/myexecutable"); | ||
| std::string execPath = f.getExecutablePath(); | ||
| #if defined(POCO_OS_FAMILY_WINDOWS) | ||
| // Path::toString() returns native separators (backslashes on Windows) | ||
| assertEqual (R"(subdir\myexecutable.exe)", execPath); | ||
| #else | ||
| assertEqual ("subdir/myexecutable", execPath); | ||
| #endif | ||
| File f("nonexistent_file_xyz_12345"); | ||
| assertTrue (f.getExecutablePath().empty()); | ||
| } |
There was a problem hiding this comment.
Test 5 comment says "Non-executable file" but the test uses a clearly non-existent filename, so it doesn’t exercise the non-executable-path behavior. Either adjust the comment to match (non-existent) or create a real non-executable file (e.g. temp file without +x on Unix / a .txt on Windows) and assert getExecutablePath() returns empty for it.
Foundation/src/File_UNIX.cpp
Outdated
| return existsImpl() ? _path : std::string{}; | ||
|
|
There was a problem hiding this comment.
FileImpl::getExecutablePathImpl() returns _path for any existing path containing '/', without verifying executability or returning an absolute path for relative inputs (e.g. "./tool"). This contradicts File::getExecutablePath() docs (executable + fully resolved absolute path) and can return directories or non-executable files. Resolve to an absolute path and validate with canExecuteImpl() (or access(X_OK) + S_ISREG) before returning; otherwise return empty.
| return existsImpl() ? _path : std::string{}; | |
| { | |
| Poco::Path p(_path); | |
| if (!p.isAbsolute()) | |
| p.makeAbsolute(); | |
| const std::string exePath = p.toString(); | |
| struct stat st; | |
| if (::access(exePath.c_str(), X_OK) == 0 && | |
| ::stat(exePath.c_str(), &st) == 0 && | |
| S_ISREG(st.st_mode)) | |
| { | |
| return exePath; | |
| } | |
| return {}; | |
| } |
| std::string findInPath(const std::string& name) | ||
| { | ||
| Path curPath(Path::current()); | ||
| curPath.append(name); | ||
| if (File(curPath).exists()) | ||
| return curPath.toString(); | ||
|
|
||
| const std::string envPath = Environment::get("PATH", ""); | ||
| if (envPath.empty()) return {}; | ||
|
|
||
| const std::string pathSeparator(1, Path::pathSeparator()); | ||
| const StringTokenizer st(envPath, pathSeparator, | ||
| StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); | ||
|
|
||
| for (const auto& dir : st) | ||
| { | ||
| try | ||
| { | ||
| std::string candidate(dir); | ||
| if (candidate.size() && candidate.back() != Path::separator()) | ||
| candidate.append(1, Path::separator()); | ||
| candidate.append(name); | ||
| if (File(candidate).exists()) | ||
| return candidate; | ||
| } |
There was a problem hiding this comment.
findInPath() returns the first existing match in CWD/PATH and may return a relative path when PATH contains relative entries. When used by getExecutablePathImpl() this can produce non-absolute and/or non-executable results. Consider either (1) making the returned candidate absolute (Path(candidate).makeAbsolute()) and (2) providing a separate helper (or predicate) for "executable" resolution so getExecutablePathImpl() can filter out non-executable matches without changing existsAnywhere() semantics.
Foundation/src/File_WIN32U.cpp
Outdated
| // First, try the filename as-is | ||
| { | ||
| return _path; | ||
| WCHAR buf[MAX_PATH]; | ||
| DWORD len = ::SearchPathW(NULL, uname.c_str(), NULL, MAX_PATH, buf, NULL); | ||
| if (len > 0 && len < MAX_PATH) | ||
| { | ||
| std::string result; | ||
| UnicodeConverter::toUTF8(buf, len, result); | ||
| return result; | ||
| } |
There was a problem hiding this comment.
Windows getExecutablePathImpl(): the initial SearchPathW(NULL, name, NULL, ...) can return a match for non-executable files when the caller passes an absolute/relative path with an extension not in PATHEXT (e.g. C:\Windows\win.ini). This violates the getExecutablePath() contract of returning an executable match or empty. After SearchPathW succeeds, validate the resolved path with canExecuteImpl() (PATHEXT membership) and return empty if it’s not executable.
Foundation/src/File_WIN32U.cpp
Outdated
| WCHAR buf[MAX_PATH]; | ||
| DWORD len = ::SearchPathW(NULL, uname.c_str(), NULL, MAX_PATH, buf, NULL); | ||
| if (len > 0 && len < MAX_PATH) | ||
| { | ||
| std::string result; | ||
| UnicodeConverter::toUTF8(buf, len, result); | ||
| return result; | ||
| } | ||
| } | ||
| return p.setExtension("exe"s).toString(); | ||
|
|
||
| // Try each PATHEXT extension via SearchPathW | ||
| const std::string pathExt = Environment::get("PATHEXT", ".EXE"); | ||
| const StringTokenizer st(pathExt, ";", | ||
| StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); | ||
|
|
||
| for (const auto& ext : st) | ||
| { | ||
| std::wstring wExt; | ||
| UnicodeConverter::toUTF16(ext, wExt); | ||
| WCHAR buf[MAX_PATH]; | ||
| DWORD len = ::SearchPathW(NULL, uname.c_str(), wExt.c_str(), MAX_PATH, buf, NULL); | ||
| if (len > 0 && len < MAX_PATH) | ||
| { |
There was a problem hiding this comment.
Windows getExecutablePathImpl() truncates resolution to MAX_PATH; if SearchPathW returns a required length >= MAX_PATH, the function currently falls through and returns empty even though the executable exists. Consider a two-call pattern: call SearchPathW with 0/NULL to get required length, then allocate a buffer of that size (+1) and call again.
Foundation/src/File_UNIX.cpp
Outdated
| } | ||
| else handleLastErrorImpl(_path); | ||
| return false; | ||
| return ::stat(absolutePath.c_str(), &st) == 0 && S_ISREG(st.st_mode) && ::access(absolutePath.c_str(), X_OK) == 0; |
There was a problem hiding this comment.
UNIX canExecuteImpl() now uses ::access(X_OK), which checks permissions using the real UID/GID, while canReadImpl()/canWriteImpl() use effective IDs (geteuid/getegid). This changes behavior for setuid/setgid processes and is inconsistent within FileImpl. Consider keeping the prior euid/egid-based permission logic, or using faccessat(..., AT_EACCESS) where available with a fallback.
| return ::stat(absolutePath.c_str(), &st) == 0 && S_ISREG(st.st_mode) && ::access(absolutePath.c_str(), X_OK) == 0; | |
| if (::stat(absolutePath.c_str(), &st) != 0 || !S_ISREG(st.st_mode)) | |
| return false; | |
| uid_t euid = ::geteuid(); | |
| gid_t egid = ::getegid(); | |
| // root can execute any regular file that has at least one execute bit set | |
| if (euid == 0) | |
| return (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0; | |
| if (st.st_uid == euid) | |
| return (st.st_mode & S_IXUSR) != 0; | |
| else if (st.st_gid == egid) | |
| return (st.st_mode & S_IXGRP) != 0; | |
| else | |
| return (st.st_mode & S_IXOTH) != 0; |
|
@matejk see copilot suggestions, some make sense |
b1011b5 to
3c34255
Compare
3c34255 to
a6950e5
Compare
They really do. |
a6950e5 to
5d320ef
Compare
5d320ef to
7532504
Compare
Summary
getExecutablePathImpl()is now a full resolver: on Unix it searches cwd + PATH viafindInPath(), then verifies executability using euid/egid stat checks; on Windows it usesSearchPathW(two-call pattern for paths > MAX_PATH) with PATHEXT iteration. Paths containing a directory separator are resolved to absolute viaPath::makeAbsolute().canExecuteImpl()on Unix now uses euid/egid stat-based permission checks (consistent withcanReadImpl/canWriteImpl) with anS_ISREGguard. Windows checks extension against PATHEXT. VxWorks treats any regular file as executable (no permission bits).absolutePath()no longer searches PATH — it only resolves relative paths against cwdexistsAnywhere()reimplemented via sharedfindInPath()helper, fixed to not search PATH for absolute pathsfindInPath()extracted intoFile.cpp— always returns absolute paths viaPath::makeAbsolute(), with cached PATH tokenizationProcessRunner::start()updated to usegetExecutablePath()/canExecute()File_VX.cppsynced with Unix/Windows implementationsFollows up on #5180.
Performance Improvements
Environment::get("PATH")and string tokenization (thread-safe)Bug Fixes
existsAnywhere()incorrectly searching PATH for absolute pathsgetExecutablePathImpl()by removing redundant existence checkTest plan
Foundation-testrunner— FileTest and ProcessRunnerTest must passFile("ls").getExecutablePath()returns absolute path like/usr/bin/ls(Unix)File("nonexistent").getExecutablePath()returns empty stringgetExecutablePath()getExecutablePath()