master ohmu/fastfetch-openbsd / src / common / impl / FFPlatform_unix.c
ohmu  ·  2026-10-15
  1#include "FFPlatform_private.h"
  2#include "common/FFstrbuf.h"
  3#include "common/arrutil.h"
  4#include "common/strutil.h"
  5#include "common/io.h"
  6#include "common/path.h"
  7#include "common/mallocHelper.h"
  8
  9#include <unistd.h>
 10#include <pwd.h>
 11#include <limits.h>
 12#include <sys/utsname.h>
 13#include <paths.h>
 14
 15#ifdef __APPLE__
 16    #include <mach-o/dyld.h>
 17    #include <sys/sysctl.h>
 18#elif defined(__FreeBSD__) || defined(__NetBSD__)
 19    #include <sys/sysctl.h>
 20#elif defined(__OpenBSD__)
 21    #include <sys/sysctl.h>
 22    #include <sys/stat.h>
 23    #include "common/path.h"
 24#elif defined(__HAIKU__)
 25    #include <image.h>
 26    #include <OS.h>
 27#endif
 28
 29static void getExePath(FFPlatform* platform) {
 30    char exePath[PATH_MAX];
 31#if defined(__linux__) || defined(__GNU__)
 32    ssize_t exePathLen = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
 33    if (exePathLen > 0) {
 34        exePath[exePathLen] = '\0';
 35    }
 36#elif defined(__APPLE__)
 37    uint32_t exePathLen = sizeof(exePath);
 38    if (_NSGetExecutablePath(exePath, &exePathLen) == 0) {
 39        exePathLen = (uint32_t) strlen(exePath);
 40    } else {
 41        exePathLen = 0;
 42    }
 43#elif defined(__FreeBSD__) || defined(__NetBSD__)
 44    size_t exePathLen = sizeof(exePath);
 45    if (sysctl(
 46            (int[]) { CTL_KERN,
 47    #ifdef __FreeBSD__
 48                KERN_PROC,
 49                KERN_PROC_PATHNAME,
 50                (pid_t) platform->pid
 51    #else
 52                KERN_PROC_ARGS,
 53                (pid_t) platform->pid,
 54                KERN_PROC_PATHNAME
 55    #endif
 56            },
 57            4,
 58            exePath,
 59            &exePathLen,
 60            nullptr,
 61            0) < 0)
 62        exePathLen = 0;
 63    else {
 64        exePathLen--; // remove terminating NUL
 65    }
 66#elif defined(__OpenBSD__)
 67#if FF_HAVE_GETEXECPATH
 68    // https://man.openbsd.org/getexecpath.3
 69    size_t exePathLen = getexecpath(exePath, sizeof(exePath)) == 0 ? strlen(exePath) : 0;
 70#else
 71    // OpenBSD (before 8.0) doesn't have a reliable way to get the executable path.
 72    // Current implementation uses argv[0], which can be easily spoofed.
 73    // See #2195
 74    size_t exePathLen = 0;
 75    {
 76        char argvBuf[ARG_MAX];
 77        size_t argvSize = sizeof(argvBuf);
 78        int argvMib[] = { CTL_KERN, KERN_PROC_ARGS, (pid_t) platform->pid, KERN_PROC_ARGV };
 79        if (sysctl(argvMib, ARRAY_SIZE(argvMib), argvBuf, &argvSize, nullptr, 0) == 0) {
 80            // The buffer is filled with an array of char pointers followed by the strings themselves
 81            char** argv = (char**) argvBuf;
 82            if (argv[0] && (char*) argv[0] >= argvBuf && (char*) argv[0] < argvBuf + argvSize) {
 83                char* arg0 = argv[0];
 84                if (arg0[0]) {
 85                    if (strchr(arg0, '/') != nullptr) // likely a path (absolute or relative)
 86                    {
 87                        exePathLen = strlen(arg0);
 88                        if (exePathLen < ARRAY_SIZE(exePath)) {
 89                            memcpy(exePath, arg0, exePathLen);
 90                            exePath[exePathLen] = '\0';
 91                        } else {
 92                            exePathLen = 0;
 93                        }
 94                    } else {
 95                        FF_STRBUF_AUTO_DESTROY tmpPath = ffStrbufCreate();
 96                        if (ffFindExecutableInPath(arg0, &tmpPath) == nullptr && tmpPath.length < ARRAY_SIZE(exePath)) {
 97                            memcpy(exePath, tmpPath.chars, tmpPath.length + 1);
 98                            exePathLen = tmpPath.length;
 99                        }
100                    }
101
102                    if (exePathLen > 0) {
103                        struct stat st;
104                        if (stat(exePath, &st) == 0 && S_ISREG(st.st_mode)) {
105                            // Replicate kvm_getfiles()'s live path: {CTL_KERN, KERN_FILE, KERN_FILE_BYPID, pid, esize, count}
106                            int fileMib[6] = { CTL_KERN, KERN_FILE, KERN_FILE_BYPID, (pid_t) platform->pid, (int) sizeof(struct kinfo_file), 0 };
107                            size_t fileSize = 0;
108                            if (sysctl(fileMib, ARRAY_SIZE(fileMib), nullptr, &fileSize, nullptr, 0) == 0) {
109                                fileSize += fileSize / 8; // add 12.5% for the table growing between the two calls
110                                size_t fileCapacity = fileSize;
111                                FF_AUTO_FREE struct kinfo_file* kf = (struct kinfo_file*) malloc(fileCapacity);
112                                if (kf) {
113                                    int rv;
114                                    // The table only grows, so the loop terminates as long as it grows by less
115                                    // than 12.5% per retry. Cap it anyway: spinning on a pathological growth
116                                    // rate would hang fastfetch, and giving up only costs the executable check
117                                    // below, which already assumes the path is correct when the list is missing.
118                                    int retries = 5;
119                                    while (true) {
120                                        fileMib[5] = (int) (fileCapacity / sizeof(struct kinfo_file));
121                                        fileSize = fileCapacity; // sysctl takes the size of the buffer as input
122                                        rv = sysctl(fileMib, ARRAY_SIZE(fileMib), kf, &fileSize, nullptr, 0);
123                                        if (rv == 0 || errno != ENOMEM || retries-- == 0) {
124                                            break;
125                                        }
126                                        // The file table grew between the two calls. The buffer has to grow
127                                        // before the retry: the next call advertises fileCapacity as the size
128                                        // of kf, and the kernel would copy that many bytes into the old
129                                        // allocation.
130                                        // OpenBSD writes back the size it needs, FreeBSD only the number of
131                                        // bytes it managed to copy, so take the larger one and add ~10% on
132                                        // top -- the growth libprocstat uses for this very sysctl.
133                                        size_t newCapacity = fileSize > fileCapacity ? fileSize : fileCapacity;
134                                        newCapacity += newCapacity / 8;
135                                        struct kinfo_file* newKf = (struct kinfo_file*) realloc(kf, newCapacity);
136                                        if (newKf == nullptr) {
137                                            rv = -1;
138                                            break;
139                                        }
140                                        kf = newKf;
141                                        fileCapacity = newCapacity;
142                                    }
143
144                                    if (rv == 0) {
145                                        int cntp = (int) (fileSize / sizeof(struct kinfo_file));
146                                        int i;
147                                        for (i = 0; i < cntp; i++) {
148                                            if (kf[i].fd_fd == KERN_FILE_TEXT) {
149                                                // KERN_FILE_TEXT is the executable file, not a shared library, and should be unique in the list.
150                                                if (st.st_dev != (dev_t) kf[i].va_fsid || st.st_ino != (ino_t) kf[i].va_fileid) {
151                                                    i = -1;
152                                                }
153                                                break;
154                                            }
155                                        }
156                                        if (i < 0) {
157                                            exePathLen = 0;
158                                        }
159                                    }
160                                    // If we can't get the list of open files, we can't verify that the file is actually the executable
161                                    // Assume it is
162                                }
163                            }
164                        } else {
165                            exePathLen = 0;
166                        }
167                    }
168                }
169            }
170        }
171    }
172#endif
173#elif defined(__sun)
174    ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1);
175    if (exePathLen > 0) {
176        exePath[exePathLen] = '\0';
177    }
178#elif defined(__HAIKU__)
179    size_t exePathLen = 0;
180    image_info info;
181    int32 cookie = 0;
182
183    while (get_next_image_info(B_CURRENT_TEAM, &cookie, &info) == B_OK) {
184        if (info.type == B_APP_IMAGE) {
185            exePathLen = strlcpy(exePath, info.name, sizeof(exePath));
186            break;
187        }
188    }
189#endif
190    if (exePathLen > 0) {
191        ffStrbufEnsureFree(&platform->exePath, PATH_MAX);
192        if (realpath(exePath, platform->exePath.chars)) {
193            ffStrbufRecalculateLength(&platform->exePath);
194        } else {
195            ffStrbufSetNS(&platform->exePath, (uint32_t) exePathLen, exePath);
196        }
197    }
198}
199
200static void platformPathAddEnv(FFlist* dirs, const char* env) {
201    const char* envValue = getenv(env);
202    if (!ffStrSet(envValue)) {
203        return;
204    }
205
206    FF_STRBUF_AUTO_DESTROY value = ffStrbufCreateA(64);
207    ffStrbufAppendS(&value, envValue);
208
209    uint32_t startIndex = 0;
210    while (startIndex < value.length) {
211        uint32_t colonIndex = ffStrbufNextIndexC(&value, startIndex, ':');
212        value.chars[colonIndex] = '\0';
213
214        if (!ffStrSet(value.chars + startIndex)) {
215            startIndex = colonIndex + 1;
216            continue;
217        }
218
219        ffPlatformPathAddAbsolute(dirs, value.chars + startIndex);
220
221        startIndex = colonIndex + 1;
222    }
223}
224
225static void getHomeDir(FFPlatform* platform, const struct passwd* pwd) {
226    const char* home = pwd ? pwd->pw_dir : getenv("HOME");
227    ffStrbufAppendS(&platform->homeDir, home);
228    ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
229}
230
231static void getCacheDir(FFPlatform* platform) {
232    const char* cache = getenv("XDG_CACHE_HOME");
233    if (ffStrSet(cache)) {
234        ffStrbufAppendS(&platform->cacheDir, cache);
235        ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
236    } else {
237        ffStrbufAppend(&platform->cacheDir, &platform->homeDir);
238        ffStrbufAppendS(&platform->cacheDir, ".cache/");
239    }
240}
241
242static void getConfigDirs(FFPlatform* platform) {
243    // Always make sure `${XDG_CONFIG_HOME:-$HOME/.config}` is the first entry
244    platformPathAddEnv(&platform->configDirs, "XDG_CONFIG_HOME");
245    ffPlatformPathAddHome(&platform->configDirs, platform, ".config/");
246
247#if defined(__APPLE__)
248    ffPlatformPathAddHome(&platform->configDirs, platform, "Library/Preferences/");
249    ffPlatformPathAddHome(&platform->configDirs, platform, "Library/Application Support/");
250#endif
251#if defined(__HAIKU__)
252    ffPlatformPathAddHome(&platform->configDirs, platform, "config/settings/");
253#endif
254
255    ffPlatformPathAddHome(&platform->configDirs, platform, "");
256    platformPathAddEnv(&platform->configDirs, "XDG_CONFIG_DIRS");
257
258#if !defined(__APPLE__)
259    ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_ETC "/xdg/");
260#endif
261
262    ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_ETC "/");
263    ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_INSTALL_SYSCONF "/");
264}
265
266static void getDataDirs(FFPlatform* platform) {
267    platformPathAddEnv(&platform->dataDirs, "XDG_DATA_HOME");
268    ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/");
269
270    // Add ${currentExePath}/../share
271    if (platform->exePath.length > 0) {
272        FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&platform->exePath);
273        ffStrbufSubstrBeforeLastC(&path, '/');
274        ffStrbufSubstrBeforeLastC(&path, '/');
275        ffStrbufAppendS(&path, "/share");
276        ffPlatformPathAddAbsolute(&platform->dataDirs, path.chars);
277    }
278
279#ifdef __APPLE__
280    ffPlatformPathAddHome(&platform->dataDirs, platform, "Library/Application Support/");
281#endif
282
283    ffPlatformPathAddHome(&platform->dataDirs, platform, "");
284    platformPathAddEnv(&platform->dataDirs, "XDG_DATA_DIRS");
285#if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
286    ffPlatformPathAddAbsolute(&platform->dataDirs, FF_PATH_PKG_BASE "/share/");
287#endif
288    ffPlatformPathAddAbsolute(&platform->dataDirs, FASTFETCH_TARGET_DIR_USR "/local/share/");
289    ffPlatformPathAddAbsolute(&platform->dataDirs, FASTFETCH_TARGET_DIR_USR "/share/");
290}
291
292static void getUserName(FFPlatform* platform, const struct passwd* pwd) {
293    if (pwd) {
294        ffStrbufSetS(&platform->userName, pwd->pw_name);
295        ffStrbufSetS(&platform->fullUserName, pwd->pw_gecos);
296        ffStrbufTrimSpace(&platform->fullUserName);
297    } else {
298        ffStrbufSetS(&platform->userName, getenv("USER"));
299    }
300}
301
302static void getHostName(FFPlatform* platform, const struct utsname* uts) {
303    ffStrbufAppendS(&platform->hostName, uts->nodename);
304}
305
306static void getUserShell(FFPlatform* platform, const struct passwd* pwd) {
307    const char* shell = pwd ? pwd->pw_shell : getenv("SHELL");
308
309    ffStrbufAppendS(&platform->userShell, shell);
310}
311
312static void getSysinfo(FFPlatformSysinfo* info, const struct utsname* uts) {
313    ffStrbufAppendS(&info->name, uts->sysname);
314    ffStrbufAppendS(&info->release, uts->release);
315    ffStrbufAppendS(&info->version, uts->version);
316#ifdef __HAIKU__
317    /* historical reason */
318    if (ffStrEquals(uts->machine, "BePC")) {
319        ffStrbufSetStatic(&info->architecture, "i386");
320    } else
321#endif
322        ffStrbufAppendS(&info->architecture, uts->machine);
323
324    // _SC_PAGESIZE is one of the sysconf names <unistd.h> requires every implementation to define,
325    // and the only failure POSIX defines for sysconf is an invalid name, so this does not fail.
326    // It replaces the former sysctl(CTL_HW, HW_PAGESIZE) call on FreeBSD / macOS / OpenBSD / NetBSD.
327    info->pageSizeShift = (uint32_t) __builtin_ctzl((unsigned long) sysconf(_SC_PAGESIZE));
328}
329
330static void getCwd(FFPlatform* platform) {
331    char cwd[PATH_MAX];
332    if (getcwd(cwd, sizeof(cwd)) != nullptr) {
333        ffStrbufSetS(&platform->cwd, cwd);
334        ffStrbufEnsureEndsWithC(&platform->cwd, '/');
335    }
336}
337
338void ffPlatformInitImpl(FFPlatform* platform) {
339    platform->pid = (uint32_t) getpid();
340    platform->uid = getuid();
341#if !__ANDROID__
342    struct passwd* pwd = getpwuid(platform->uid);
343#else
344    // On Android, /etc/passwd is empty, and getpwuid() will return `/data` for pw_dir
345    struct passwd* pwd = nullptr;
346#endif
347
348    struct utsname uts;
349    if (uname(&uts) < 0) {
350        memset(&uts, 0, sizeof(uts));
351    }
352
353    getExePath(platform);
354    getCwd(platform);
355    getHomeDir(platform, pwd);
356    getCacheDir(platform);
357    getConfigDirs(platform);
358    getDataDirs(platform);
359
360    getUserName(platform, pwd);
361    getHostName(platform, &uts);
362    getUserShell(platform, pwd);
363
364    getSysinfo(&platform->sysinfo, &uts);
365}