ohmu
·
2026-10-15
1#include "common/path.h"
2#include "common/debug.h"
3#include "common/io.h"
4#include "common/arrutil.h"
5
6#if !_WIN32
7const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
8 char* path = getenv("PATH");
9 if (!path) {
10 FF_DEBUG("The PATH environment variable is not set");
11 return "$PATH not set";
12 }
13
14 #ifdef _WIN32
15 const bool appendExe = !ffStrEndsWithIgnCase(name, ".exe");
16 #endif
17
18 for (char* token = path; *token; path = token + 1) {
19 token = strchr(path,
20 #ifdef _WIN32
21 ';'
22 #else
23 ':'
24 #endif
25 );
26 if (!token) {
27 token = path + strlen(path);
28 }
29
30 ffStrbufSetNS(result, (uint32_t) (token - path), path);
31 ffStrbufEnsureEndsWithC(result,
32 #ifdef _WIN32
33 '\\'
34 #else
35 '/'
36 #endif
37 );
38 ffStrbufAppendS(result, name);
39 #ifdef _WIN32
40 if (appendExe) {
41 ffStrbufAppendS(result, ".exe");
42 }
43 if (!ffPathExists(result->chars, FF_PATHTYPE_FILE)) {
44 continue;
45 }
46 #else
47 if (access(result->chars, X_OK) != 0) {
48 continue;
49 }
50 #endif
51
52 return nullptr;
53 }
54 ffStrbufClear(result);
55 FF_DEBUG("%s is not executable in any of the %s entries", name, "PATH");
56 return "Executable not found";
57}
58#else
59 #include <windows.h>
60 #include <winioctl.h>
61 #include <errno.h>
62 #include <stdalign.h>
63
64const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
65 char buffer[MAX_PATH + 1];
66 DWORD length = SearchPathA(nullptr, name, ".exe", sizeof(buffer), buffer, nullptr);
67 if (length == 0) {
68 FF_DEBUG("SearchPathA(%s) failed: %s", name, ffDebugWin32Error(GetLastError()));
69 ffStrbufClear(result);
70 return "Executable not found";
71 }
72 ffStrbufSetS(result, buffer);
73 return nullptr;
74}
75
76static inline int winerr2Errno(DWORD err) {
77 switch (err) {
78 case ERROR_FILE_NOT_FOUND:
79 case ERROR_PATH_NOT_FOUND:
80 case ERROR_INVALID_NAME:
81 return ENOENT;
82 case ERROR_ACCESS_DENIED:
83 case ERROR_SHARING_VIOLATION:
84 case ERROR_LOCK_VIOLATION:
85 return EACCES;
86 case ERROR_BUFFER_OVERFLOW:
87 case ERROR_INSUFFICIENT_BUFFER:
88 return ENAMETOOLONG;
89 case ERROR_INVALID_PARAMETER:
90 case ERROR_NOT_A_REPARSE_POINT:
91 return EINVAL;
92 default:
93 return EIO;
94 }
95}
96
97char* frealpath(HANDLE hFile, char* resolved_name) {
98 if (__builtin_expect(hFile == INVALID_HANDLE_VALUE || !hFile, false)) {
99 errno = EINVAL;
100 return nullptr;
101 }
102
103 wchar_t resolvedNameW[MAX_PATH + 4]; /* +4 for "\\\\?\\" prefix */
104 DWORD lenW = GetFinalPathNameByHandleW(hFile, resolvedNameW, (DWORD) ARRAY_SIZE(resolvedNameW), FILE_NAME_NORMALIZED);
105
106 if (lenW == 0) {
107 errno = winerr2Errno(GetLastError());
108 return nullptr;
109 }
110 if (lenW >= ARRAY_SIZE(resolvedNameW)) {
111 errno = E2BIG;
112 return nullptr;
113 }
114 lenW++; // Include null terminator
115
116 wchar_t* srcW = resolvedNameW;
117 DWORD srcLenW = lenW;
118
119 if (srcLenW >= 8 && wcsncmp(resolvedNameW, L"\\\\?\\UNC\\", 8) == 0) {
120 /* Convert "\\?\UNC\server\share" to "\\server\share" */
121 srcW += 6;
122 srcLenW -= 6;
123 *srcW = L'\\';
124 } else if (srcLenW >= 4 && wcsncmp(resolvedNameW, L"\\\\?\\", 4) == 0) {
125 srcW += 4;
126 srcLenW -= 4;
127 }
128
129 if (resolved_name) {
130 ULONG outBytes = 0;
131 if (!NT_SUCCESS(RtlUnicodeToUTF8N(resolved_name, MAX_PATH, &outBytes, srcW, (ULONG) (srcLenW * sizeof(wchar_t))))) {
132 errno = E2BIG;
133 return nullptr;
134 }
135 } else {
136 /* UTF-8 worst-case: up to 4 bytes per UTF-16 code unit */
137 char tmp[(MAX_PATH + 4) * 4];
138 ULONG outBytes = 0;
139
140 if (!NT_SUCCESS(RtlUnicodeToUTF8N(tmp, (ULONG) sizeof(tmp), &outBytes, srcW, (ULONG) (srcLenW * sizeof(wchar_t))))) {
141 errno = E2BIG;
142 return nullptr;
143 }
144
145 resolved_name = (char*) malloc(outBytes);
146 if (!resolved_name) {
147 errno = ENOMEM;
148 return nullptr;
149 }
150
151 memcpy(resolved_name, tmp, outBytes);
152 }
153
154 return resolved_name;
155}
156
157char* realpath(const char* __restrict file_name, char* __restrict resolved_name) {
158 if (!file_name) {
159 errno = EINVAL;
160 return nullptr;
161 }
162
163 wchar_t fileNameW[MAX_PATH];
164 ULONG lenBytes = 0;
165
166 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &lenBytes, file_name, (ULONG) strlen(file_name) + 1))) {
167 errno = EINVAL;
168 return nullptr;
169 }
170
171 FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileW(
172 fileNameW,
173 0,
174 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
175 nullptr,
176 OPEN_EXISTING,
177 FILE_FLAG_BACKUP_SEMANTICS,
178 nullptr);
179
180 if (hFile == INVALID_HANDLE_VALUE) {
181 errno = winerr2Errno(GetLastError());
182 return nullptr;
183 }
184
185 return frealpath(hFile, resolved_name);
186}
187
188ssize_t freadlink(HANDLE hFile, char* buf, size_t bufsiz) {
189 if (__builtin_expect(hFile == INVALID_HANDLE_VALUE || !buf || bufsiz == 0, false)) {
190 errno = EINVAL;
191 return -1;
192 }
193
194 alignas(REPARSE_DATA_BUFFER) BYTE reparseBuf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
195 DWORD bytesReturned = 0;
196 if (!DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, nullptr, 0, reparseBuf, (DWORD) sizeof(reparseBuf), &bytesReturned, nullptr)) {
197 errno = winerr2Errno(GetLastError());
198 return -1;
199 }
200
201 REPARSE_DATA_BUFFER* rp = (REPARSE_DATA_BUFFER*) reparseBuf;
202 const wchar_t* targetW = nullptr;
203 USHORT targetBytes = 0;
204
205 if (rp->ReparseTag == IO_REPARSE_TAG_SYMLINK) {
206 if (rp->SymbolicLinkReparseBuffer.PrintNameLength > 0) {
207 targetW = rp->SymbolicLinkReparseBuffer.PathBuffer +
208 (rp->SymbolicLinkReparseBuffer.PrintNameOffset / sizeof(wchar_t));
209 targetBytes = rp->SymbolicLinkReparseBuffer.PrintNameLength;
210 } else {
211 targetW = rp->SymbolicLinkReparseBuffer.PathBuffer +
212 (rp->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(wchar_t));
213 targetBytes = rp->SymbolicLinkReparseBuffer.SubstituteNameLength;
214
215 if (targetBytes >= 8 &&
216 wcsncmp(targetW, L"\\??\\", 4) == 0) {
217 targetW += 4;
218 targetBytes -= 8;
219 }
220 }
221 } else if (rp->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) {
222 if (rp->MountPointReparseBuffer.PrintNameLength > 0) {
223 targetW = rp->MountPointReparseBuffer.PathBuffer +
224 (rp->MountPointReparseBuffer.PrintNameOffset / sizeof(wchar_t));
225 targetBytes = rp->MountPointReparseBuffer.PrintNameLength;
226 } else {
227 targetW = rp->MountPointReparseBuffer.PathBuffer +
228 (rp->MountPointReparseBuffer.SubstituteNameOffset / sizeof(wchar_t));
229 targetBytes = rp->MountPointReparseBuffer.SubstituteNameLength;
230
231 if (targetBytes >= 8 &&
232 wcsncmp(targetW, L"\\??\\", 4) == 0) {
233 targetW += 4;
234 targetBytes -= 8;
235 }
236 }
237 } else {
238 errno = EINVAL;
239 return -1;
240 }
241
242 ULONG outBytes = 0;
243 if (!NT_SUCCESS(RtlUnicodeToUTF8N(buf, (ULONG) bufsiz, &outBytes, targetW, targetBytes))) {
244 errno = E2BIG;
245 return -1;
246 }
247
248 // Not null-terminated
249 return (ssize_t) outBytes;
250}
251
252ssize_t readlink(const char* path, char* buf, size_t bufsiz) {
253 if (!path || !buf || bufsiz == 0) {
254 errno = EINVAL;
255 return -1;
256 }
257
258 wchar_t pathW[MAX_PATH];
259 ULONG pathWBytes = 0;
260 if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathW, (ULONG) sizeof(pathW), &pathWBytes, path, (ULONG) strlen(path) + 1))) {
261 errno = EINVAL;
262 return -1;
263 }
264
265 FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileW(
266 pathW,
267 0,
268 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
269 nullptr,
270 OPEN_EXISTING,
271 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
272 nullptr);
273
274 if (hFile == INVALID_HANDLE_VALUE) {
275 errno = winerr2Errno(GetLastError());
276 return -1;
277 }
278
279 return freadlink(hFile, buf, bufsiz);
280}
281#endif