summaryrefslogtreecommitdiffstats
path: root/src/common/impl
diff options
context:
space:
mode:
Diffstat (limited to 'src/common/impl')
-rw-r--r--src/common/impl/FFPlatform.c94
-rw-r--r--src/common/impl/FFPlatform_private.h8
-rw-r--r--src/common/impl/FFPlatform_unix.c321
-rw-r--r--src/common/impl/FFPlatform_windows.c313
-rw-r--r--src/common/impl/FFlist.c34
-rw-r--r--src/common/impl/FFstrbuf.c903
-rw-r--r--src/common/impl/base64.c110
-rw-r--r--src/common/impl/binary_apple.c303
-rw-r--r--src/common/impl/binary_linux.c147
-rw-r--r--src/common/impl/binary_windows.c71
-rw-r--r--src/common/impl/commandoption.c261
-rw-r--r--src/common/impl/dbus.c334
-rw-r--r--src/common/impl/debug_windows.c55
-rw-r--r--src/common/impl/duration.c87
-rw-r--r--src/common/impl/edidHelper.c132
-rw-r--r--src/common/impl/font.c513
-rw-r--r--src/common/impl/format.c761
-rw-r--r--src/common/impl/frequency.c26
-rw-r--r--src/common/impl/init.c300
-rw-r--r--src/common/impl/io_unix.c333
-rw-r--r--src/common/impl/io_windows.c501
-rw-r--r--src/common/impl/jsonconfig.c365
-rw-r--r--src/common/impl/kmod_apple.c12
-rw-r--r--src/common/impl/kmod_bsd.c7
-rw-r--r--src/common/impl/kmod_linux.c25
-rw-r--r--src/common/impl/kmod_nbsd.c40
-rw-r--r--src/common/impl/kmod_nosupport.c5
-rw-r--r--src/common/impl/kmod_sunos.c23
-rw-r--r--src/common/impl/kmod_windows.c28
-rw-r--r--src/common/impl/library.c180
-rw-r--r--src/common/impl/lua.c233
-rw-r--r--src/common/impl/memrchr.c21
-rw-r--r--src/common/impl/netif.c22
-rw-r--r--src/common/impl/netif_apple.c218
-rw-r--r--src/common/impl/netif_bsd.c124
-rw-r--r--src/common/impl/netif_gnu.c36
-rw-r--r--src/common/impl/netif_haiku.c125
-rw-r--r--src/common/impl/netif_linux.c443
-rw-r--r--src/common/impl/netif_windows.c92
-rw-r--r--src/common/impl/networking_common.c192
-rw-r--r--src/common/impl/networking_linux.c502
-rw-r--r--src/common/impl/networking_windows.c376
-rw-r--r--src/common/impl/option.c185
-rw-r--r--src/common/impl/parsing.c136
-rw-r--r--src/common/impl/path.c277
-rw-r--r--src/common/impl/percent.c293
-rw-r--r--src/common/impl/printing.c156
-rw-r--r--src/common/impl/processing_linux.c672
-rw-r--r--src/common/impl/processing_windows.c281
-rw-r--r--src/common/impl/properties.c172
-rw-r--r--src/common/impl/settings.c609
-rw-r--r--src/common/impl/size.c42
-rw-r--r--src/common/impl/smbios.c805
-rw-r--r--src/common/impl/strutil.c80
-rw-r--r--src/common/impl/sysctl.c93
-rw-r--r--src/common/impl/temps.c165
-rw-r--r--src/common/impl/time.c97
-rw-r--r--src/common/impl/wcwidth.c31
58 files changed, 12770 insertions, 0 deletions
diff --git a/src/common/impl/FFPlatform.c b/src/common/impl/FFPlatform.c
new file mode 100644
index 0000000..ff4c45c
--- /dev/null
+++ b/src/common/impl/FFPlatform.c
@@ -0,0 +1,94 @@
+#include "FFPlatform_private.h"
+#include "common/strutil.h"
+#include "common/io.h"
+#include "detection/version/version.h"
+
+void ffPlatformInit(FFPlatform* platform) {
+ ffStrbufInit(&platform->homeDir);
+ ffStrbufInit(&platform->cacheDir);
+ ffListInit(&platform->configDirs);
+ ffListInit(&platform->dataDirs);
+ ffStrbufInit(&platform->exePath);
+ ffStrbufInit(&platform->cwd);
+
+ ffStrbufInit(&platform->userName);
+ ffStrbufInit(&platform->fullUserName);
+ ffStrbufInit(&platform->hostName);
+ ffStrbufInit(&platform->userShell);
+
+#ifdef _WIN32
+ ffStrbufInit(&platform->sid);
+#endif
+
+ FFPlatformSysinfo* info = &platform->sysinfo;
+
+ ffStrbufInit(&info->name);
+ ffStrbufInit(&info->release);
+ ffStrbufInit(&info->version);
+ ffStrbufInit(&info->architecture);
+ info->pageSize = 0;
+
+ ffPlatformInitImpl(platform);
+
+ if (info->name.length == 0) {
+ ffStrbufSetStatic(&info->name, ffVersionResult.sysName);
+ }
+
+ if (info->architecture.length == 0) {
+ ffStrbufSetStatic(&info->architecture, ffVersionResult.architecture);
+ }
+}
+
+void ffPlatformDestroy(FFPlatform* platform) {
+ ffStrbufDestroy(&platform->homeDir);
+ ffStrbufDestroy(&platform->cacheDir);
+
+ FF_LIST_FOR_EACH (FFstrbuf, dir, platform->configDirs) {
+ ffStrbufDestroy(dir);
+ }
+ ffListDestroy(&platform->configDirs);
+
+ FF_LIST_FOR_EACH (FFstrbuf, dir, platform->dataDirs) {
+ ffStrbufDestroy(dir);
+ }
+ ffListDestroy(&platform->dataDirs);
+ ffStrbufDestroy(&platform->exePath);
+ ffStrbufDestroy(&platform->cwd);
+
+ ffStrbufDestroy(&platform->userName);
+ ffStrbufDestroy(&platform->hostName);
+ ffStrbufDestroy(&platform->userShell);
+ ffStrbufDestroy(&platform->fullUserName);
+
+#ifdef _WIN32
+ ffStrbufDestroy(&platform->sid);
+#endif
+
+ FFPlatformSysinfo* info = &platform->sysinfo;
+ ffStrbufDestroy(&info->architecture);
+ ffStrbufDestroy(&info->name);
+ ffStrbufDestroy(&info->release);
+ ffStrbufDestroy(&info->version);
+}
+
+void ffPlatformPathAddAbsolute(FFlist* dirs, const char* path) {
+ if (!ffPathExists(path, FF_PATHTYPE_DIRECTORY)) {
+ return;
+ }
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateS(path);
+ ffStrbufEnsureEndsWithC(&buffer, '/');
+ if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
+ ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
+ }
+}
+
+void ffPlatformPathAddHome(FFlist* dirs, const FFPlatform* platform, const char* suffix) {
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateA(64);
+ ffStrbufAppend(&buffer, &platform->homeDir);
+ ffStrbufAppendS(&buffer, suffix);
+ ffStrbufEnsureEndsWithC(&buffer, '/');
+ if (ffPathExists(buffer.chars, FF_PATHTYPE_DIRECTORY) && !FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
+ ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
+ }
+}
diff --git a/src/common/impl/FFPlatform_private.h b/src/common/impl/FFPlatform_private.h
new file mode 100644
index 0000000..e9dfd71
--- /dev/null
+++ b/src/common/impl/FFPlatform_private.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "common/FFPlatform.h"
+
+void ffPlatformInitImpl(FFPlatform* platform);
+
+void ffPlatformPathAddAbsolute(FFlist* dirs, const char* path);
+void ffPlatformPathAddHome(FFlist* dirs, const FFPlatform* platform, const char* suffix);
diff --git a/src/common/impl/FFPlatform_unix.c b/src/common/impl/FFPlatform_unix.c
new file mode 100644
index 0000000..de69f6b
--- /dev/null
+++ b/src/common/impl/FFPlatform_unix.c
@@ -0,0 +1,321 @@
+#include "FFPlatform_private.h"
+#include "common/FFstrbuf.h"
+#include "common/arrutil.h"
+#include "common/strutil.h"
+#include "common/io.h"
+#include "fastfetch_config.h"
+
+#include <unistd.h>
+#include <pwd.h>
+#include <limits.h>
+#include <sys/utsname.h>
+#include <paths.h>
+
+#ifdef __APPLE__
+ #include <mach-o/dyld.h>
+ #include <sys/sysctl.h>
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+ #include <sys/sysctl.h>
+#elif defined(__OpenBSD__)
+ #include <sys/sysctl.h>
+ #include <sys/stat.h>
+ #include <kvm.h>
+ #include "common/path.h"
+#elif defined(__HAIKU__)
+ #include <image.h>
+ #include <OS.h>
+#endif
+
+static void getExePath(FFPlatform* platform) {
+ char exePath[PATH_MAX];
+#if defined(__linux__) || defined(__GNU__)
+ ssize_t exePathLen = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1);
+ if (exePathLen >= 0) {
+ exePath[exePathLen] = '\0';
+ }
+#elif defined(__APPLE__)
+ uint32_t exePathLen = sizeof(exePath);
+ if (_NSGetExecutablePath(exePath, &exePathLen) == 0) {
+ exePathLen = (uint32_t) strlen(exePath);
+ } else {
+ exePathLen = 0;
+ }
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+ size_t exePathLen = sizeof(exePath);
+ if (sysctl(
+ (int[]) { CTL_KERN,
+ #ifdef __FreeBSD__
+ KERN_PROC,
+ KERN_PROC_PATHNAME,
+ (pid_t) platform->pid
+ #else
+ KERN_PROC_ARGS,
+ (pid_t) platform->pid,
+ KERN_PROC_PATHNAME
+ #endif
+ },
+ 4,
+ exePath,
+ &exePathLen,
+ NULL,
+ 0) < 0)
+ exePathLen = 0;
+ else {
+ exePathLen--; // remove terminating NUL
+ }
+#elif defined(__OpenBSD__)
+ // OpenBSD doesn't have a reliable way to get the executable path.
+ // Current implementation uses argv[0], which can be easily spoofed.
+ // See #2195
+ size_t exePathLen = 0;
+ kvm_t* kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, NULL);
+ if (kd) {
+ int kpCount;
+ struct kinfo_proc* kp = kvm_getprocs(kd, KERN_PROC_PID, (pid_t) platform->pid, sizeof(*kp), &kpCount);
+ if (kp && kpCount == 1) {
+ char** argv = kvm_getargv(kd, kp, 0);
+ if (argv && argv[0]) {
+ char* arg0 = argv[0];
+ if (arg0[0]) {
+ if (strchr(arg0, '/') != NULL) // likely a path (absolute or relative)
+ {
+ exePathLen = strlen(arg0);
+ if (exePathLen < ARRAY_SIZE(exePath)) {
+ memcpy(exePath, arg0, exePathLen);
+ exePath[exePathLen] = '\0';
+ } else {
+ exePathLen = 0;
+ }
+ } else {
+ FF_STRBUF_AUTO_DESTROY tmpPath = ffStrbufCreate();
+ if (ffFindExecutableInPath(arg0, &tmpPath) == NULL && tmpPath.length < ARRAY_SIZE(exePath)) {
+ memcpy(exePath, tmpPath.chars, tmpPath.length + 1);
+ exePathLen = tmpPath.length;
+ }
+ }
+
+ if (exePathLen > 0) {
+ struct stat st;
+ if (stat(exePath, &st) == 0 && S_ISREG(st.st_mode)) {
+ int cntp;
+ struct kinfo_file* kf = kvm_getfiles(kd, KERN_FILE_BYPID, (pid_t) platform->pid, sizeof(*kf), &cntp);
+ if (kf) {
+ int i;
+ for (i = 0; i < cntp; i++) {
+ if (kf[i].fd_fd == KERN_FILE_TEXT) {
+ // KERN_FILE_TEXT is the executable file, not a shared library, and should be unique in the list.
+ if (st.st_dev != (dev_t) kf[i].va_fsid || st.st_ino != (ino_t) kf[i].va_fileid) {
+ i = -1;
+ }
+ break;
+ }
+ }
+ if (i < 0) {
+ exePathLen = 0;
+ }
+ } else {
+ // If we can't get the list of open files, we can't verify that the file is actually the executable
+ // Assume it is
+ }
+ } else {
+ exePathLen = 0;
+ }
+ }
+ }
+ }
+ }
+ kvm_close(kd);
+ }
+#elif defined(__sun)
+ ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1);
+ if (exePathLen >= 0) {
+ exePath[exePathLen] = '\0';
+ }
+#elif defined(__HAIKU__)
+ size_t exePathLen = 0;
+ image_info info;
+ int32 cookie = 0;
+
+ while (get_next_image_info(B_CURRENT_TEAM, &cookie, &info) == B_OK) {
+ if (info.type == B_APP_IMAGE) {
+ exePathLen = strlcpy(exePath, info.name, sizeof(exePath));
+ break;
+ }
+ }
+#endif
+ if (exePathLen > 0) {
+ ffStrbufEnsureFree(&platform->exePath, PATH_MAX);
+ if (realpath(exePath, platform->exePath.chars)) {
+ ffStrbufRecalculateLength(&platform->exePath);
+ } else {
+ ffStrbufSetNS(&platform->exePath, (uint32_t) exePathLen, exePath);
+ }
+ }
+}
+
+static void platformPathAddEnv(FFlist* dirs, const char* env) {
+ const char* envValue = getenv(env);
+ if (!ffStrSet(envValue)) {
+ return;
+ }
+
+ FF_STRBUF_AUTO_DESTROY value = ffStrbufCreateA(64);
+ ffStrbufAppendS(&value, envValue);
+
+ uint32_t startIndex = 0;
+ while (startIndex < value.length) {
+ uint32_t colonIndex = ffStrbufNextIndexC(&value, startIndex, ':');
+ value.chars[colonIndex] = '\0';
+
+ if (!ffStrSet(value.chars + startIndex)) {
+ startIndex = colonIndex + 1;
+ continue;
+ }
+
+ ffPlatformPathAddAbsolute(dirs, value.chars + startIndex);
+
+ startIndex = colonIndex + 1;
+ }
+}
+
+static void getHomeDir(FFPlatform* platform, const struct passwd* pwd) {
+ const char* home = pwd ? pwd->pw_dir : getenv("HOME");
+ ffStrbufAppendS(&platform->homeDir, home);
+ ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
+}
+
+static void getCacheDir(FFPlatform* platform) {
+ const char* cache = getenv("XDG_CACHE_HOME");
+ if (ffStrSet(cache)) {
+ ffStrbufAppendS(&platform->cacheDir, cache);
+ ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
+ } else {
+ ffStrbufAppend(&platform->cacheDir, &platform->homeDir);
+ ffStrbufAppendS(&platform->cacheDir, ".cache/");
+ }
+}
+
+static void getConfigDirs(FFPlatform* platform) {
+ // Always make sure `${XDG_CONFIG_HOME:-$HOME/.config}` is the first entry
+ platformPathAddEnv(&platform->configDirs, "XDG_CONFIG_HOME");
+ ffPlatformPathAddHome(&platform->configDirs, platform, ".config/");
+
+#if defined(__APPLE__)
+ ffPlatformPathAddHome(&platform->configDirs, platform, "Library/Preferences/");
+ ffPlatformPathAddHome(&platform->configDirs, platform, "Library/Application Support/");
+#endif
+#if defined(__HAIKU__)
+ ffPlatformPathAddHome(&platform->configDirs, platform, "config/settings/");
+#endif
+
+ ffPlatformPathAddHome(&platform->configDirs, platform, "");
+ platformPathAddEnv(&platform->configDirs, "XDG_CONFIG_DIRS");
+
+#if !defined(__APPLE__)
+ ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_ETC "/xdg/");
+#endif
+
+ ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_ETC "/");
+ ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_INSTALL_SYSCONF "/");
+}
+
+static void getDataDirs(FFPlatform* platform) {
+ platformPathAddEnv(&platform->dataDirs, "XDG_DATA_HOME");
+ ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/");
+
+ // Add ${currentExePath}/../share
+ if (platform->exePath.length > 0) {
+ FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&platform->exePath);
+ ffStrbufSubstrBeforeLastC(&path, '/');
+ ffStrbufSubstrBeforeLastC(&path, '/');
+ ffStrbufAppendS(&path, "/share");
+ ffPlatformPathAddAbsolute(&platform->dataDirs, path.chars);
+ }
+
+#ifdef __APPLE__
+ ffPlatformPathAddHome(&platform->dataDirs, platform, "Library/Application Support/");
+#endif
+
+ ffPlatformPathAddHome(&platform->dataDirs, platform, "");
+ platformPathAddEnv(&platform->dataDirs, "XDG_DATA_DIRS");
+#ifdef _PATH_LOCALBASE
+ ffPlatformPathAddAbsolute(&platform->dataDirs, _PATH_LOCALBASE "/share/");
+#endif
+ ffPlatformPathAddAbsolute(&platform->dataDirs, FASTFETCH_TARGET_DIR_USR "/local/share/");
+ ffPlatformPathAddAbsolute(&platform->dataDirs, FASTFETCH_TARGET_DIR_USR "/share/");
+}
+
+static void getUserName(FFPlatform* platform, const struct passwd* pwd) {
+ if (pwd) {
+ ffStrbufSetS(&platform->userName, pwd->pw_name);
+ ffStrbufSetS(&platform->fullUserName, pwd->pw_gecos);
+ ffStrbufTrimSpace(&platform->fullUserName);
+ } else {
+ ffStrbufSetS(&platform->userName, getenv("USER"));
+ }
+}
+
+static void getHostName(FFPlatform* platform, const struct utsname* uts) {
+ ffStrbufAppendS(&platform->hostName, uts->nodename);
+}
+
+static void getUserShell(FFPlatform* platform, const struct passwd* pwd) {
+ const char* shell = getenv("SHELL");
+ if (!ffStrSet(shell) && pwd) {
+ shell = pwd->pw_shell;
+ }
+
+ ffStrbufAppendS(&platform->userShell, shell);
+}
+
+static void getSysinfo(FFPlatformSysinfo* info, const struct utsname* uts) {
+ ffStrbufAppendS(&info->name, uts->sysname);
+ ffStrbufAppendS(&info->release, uts->release);
+ ffStrbufAppendS(&info->version, uts->version);
+#ifdef __HAIKU__
+ /* historical reason */
+ if (ffStrEquals(uts->machine, "BePC")) {
+ ffStrbufSetStatic(&info->architecture, "i386");
+ } else
+#endif
+ ffStrbufAppendS(&info->architecture, uts->machine);
+
+#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__)
+ size_t length = sizeof(info->pageSize);
+ sysctl((int[]) { CTL_HW, HW_PAGESIZE }, 2, &info->pageSize, &length, NULL, 0);
+#else
+ info->pageSize = (uint32_t) sysconf(_SC_PAGESIZE);
+#endif
+}
+
+static void getCwd(FFPlatform* platform) {
+ char cwd[PATH_MAX];
+ if (getcwd(cwd, sizeof(cwd)) != NULL) {
+ ffStrbufSetS(&platform->cwd, cwd);
+ ffStrbufEnsureEndsWithC(&platform->cwd, '/');
+ }
+}
+
+void ffPlatformInitImpl(FFPlatform* platform) {
+ platform->pid = (uint32_t) getpid();
+ platform->uid = getuid();
+ struct passwd* pwd = getpwuid(platform->uid);
+
+ struct utsname uts;
+ if (uname(&uts) < 0) {
+ memset(&uts, 0, sizeof(uts));
+ }
+
+ getExePath(platform);
+ getCwd(platform);
+ getHomeDir(platform, pwd);
+ getCacheDir(platform);
+ getConfigDirs(platform);
+ getDataDirs(platform);
+
+ getUserName(platform, pwd);
+ getHostName(platform, &uts);
+ getUserShell(platform, pwd);
+
+ getSysinfo(&platform->sysinfo, &uts);
+}
diff --git a/src/common/impl/FFPlatform_windows.c b/src/common/impl/FFPlatform_windows.c
new file mode 100644
index 0000000..083d20a
--- /dev/null
+++ b/src/common/impl/FFPlatform_windows.c
@@ -0,0 +1,313 @@
+#include "FFPlatform_private.h"
+#include "common/io.h"
+#include "common/library.h"
+#include "common/strutil.h"
+#include "common/windows/unicode.h"
+#include "common/windows/registry.h"
+#include "common/windows/nt.h"
+
+#include <stdalign.h>
+#include <windows.h>
+#include <shlobj.h>
+#include <sddl.h>
+
+#define SECURITY_WIN32 1 // For secext.h
+#include <secext.h>
+
+static void getExePath(FFPlatform* platform) {
+ wchar_t exePathW[MAX_PATH];
+
+ FF_AUTO_CLOSE_FD HANDLE hPath = CreateFileW(
+ ffGetPeb()->ProcessParameters->ImagePathName.Buffer,
+ GENERIC_READ,
+ FILE_SHARE_READ,
+ NULL,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS,
+ NULL);
+ if (hPath != INVALID_HANDLE_VALUE) {
+ DWORD len = GetFinalPathNameByHandleW(hPath, exePathW, MAX_PATH, FILE_NAME_NORMALIZED);
+ if (len > 0 && len < MAX_PATH) {
+ ffStrbufSetNWS(&platform->exePath, len, exePathW);
+ if (ffStrbufStartsWithS(&platform->exePath, "\\\\?\\")) {
+ ffStrbufSubstrAfter(&platform->exePath, 3);
+ }
+ }
+ }
+
+ if (platform->exePath.length == 0) {
+ PCUNICODE_STRING imagePathName = &ffGetPeb()->ProcessParameters->ImagePathName;
+ ffStrbufSetNWS(&platform->exePath, imagePathName->Length / sizeof(wchar_t), imagePathName->Buffer);
+ }
+
+ ffStrbufReplaceAllC(&platform->exePath, '\\', '/');
+}
+
+static void getHomeDir(FFPlatform* platform) {
+ PWSTR pPath = NULL;
+ if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_Profile, KF_FLAG_DEFAULT, NULL, &pPath))) {
+ ffStrbufSetWS(&platform->homeDir, pPath);
+ ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
+ ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
+ } else {
+ ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE"));
+ ffStrbufReplaceAllC(&platform->homeDir, '\\', '/');
+ ffStrbufEnsureEndsWithC(&platform->homeDir, '/');
+ }
+ CoTaskMemFree(pPath);
+}
+
+static void getCacheDir(FFPlatform* platform) {
+ PWSTR pPath = NULL;
+ if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, NULL, &pPath))) {
+ ffStrbufSetWS(&platform->cacheDir, pPath);
+ ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/');
+ ffStrbufEnsureEndsWithC(&platform->cacheDir, '/');
+ } else {
+ ffStrbufAppend(&platform->cacheDir, &platform->homeDir);
+ ffStrbufAppendS(&platform->cacheDir, "AppData/Local/");
+ }
+ CoTaskMemFree(pPath);
+}
+
+static void platformPathAddKnownFolder(FFlist* dirs, REFKNOWNFOLDERID folderId) {
+ PWSTR pPath = NULL;
+ if (SUCCEEDED(SHGetKnownFolderPath(folderId, KF_FLAG_DEFAULT, NULL, &pPath))) {
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateWS(pPath);
+ CoTaskMemFree(pPath);
+ ffStrbufReplaceAllC(&buffer, '\\', '/');
+ ffStrbufEnsureEndsWithC(&buffer, '/');
+ if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
+ ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
+ }
+ }
+}
+
+static void platformPathAddEnvSuffix(FFlist* dirs, const char* env, const char* suffix) {
+ const char* value = getenv(env);
+ if (!ffStrSet(value)) {
+ return;
+ }
+
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateA(64);
+ ffStrbufAppendS(&buffer, value);
+ ffStrbufReplaceAllC(&buffer, '\\', '/');
+ ffStrbufEnsureEndsWithC(&buffer, '/');
+ if (suffix) {
+ ffStrbufAppendS(&buffer, suffix);
+ ffStrbufEnsureEndsWithC(&buffer, '/');
+ }
+
+ if (ffPathExists(buffer.chars, FF_PATHTYPE_DIRECTORY) && !FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) {
+ ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer);
+ }
+}
+
+static void getConfigDirs(FFPlatform* platform) {
+ if (getenv("MSYSTEM")) {
+ // We are in MSYS2 / Git Bash
+ platformPathAddEnvSuffix(&platform->configDirs, "HOME", ".config/");
+ platformPathAddEnvSuffix(&platform->configDirs, "HOME", NULL);
+ platformPathAddEnvSuffix(&platform->configDirs, "MINGW_PREFIX", "etc");
+ }
+
+ ffPlatformPathAddHome(&platform->configDirs, platform, ".config/");
+ platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_ProgramData);
+ platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_RoamingAppData);
+ platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_LocalAppData);
+ ffPlatformPathAddHome(&platform->configDirs, platform, "");
+}
+
+static void getDataDirs(FFPlatform* platform) {
+ if (getenv("MSYSTEM") && getenv("HOME")) {
+ // We are in MSYS2 / Git Bash
+ platformPathAddEnvSuffix(&platform->dataDirs, "HOME", ".local/share/");
+ platformPathAddEnvSuffix(&platform->dataDirs, "HOME", NULL);
+ platformPathAddEnvSuffix(&platform->dataDirs, "MINGW_PREFIX", "share");
+ }
+ ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/");
+ platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_ProgramData);
+ platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_RoamingAppData);
+ platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_LocalAppData);
+ ffPlatformPathAddHome(&platform->dataDirs, platform, "");
+}
+
+static void getUserName(FFPlatform* platform) {
+ wchar_t buffer[256];
+ DWORD size = ARRAY_SIZE(buffer);
+ if (GetUserNameExW(NameDisplay, buffer, &size)) {
+ ffStrbufSetWS(&platform->fullUserName, buffer);
+ }
+
+ NTSYSAPI NTSTATUS NTAPI LsaGetUserName(
+ _Outptr_ PLSA_UNICODE_STRING * UserName,
+ _Outptr_opt_ PLSA_UNICODE_STRING * DomainName);
+ PLSA_UNICODE_STRING userName = NULL;
+ if (NT_SUCCESS(LsaGetUserName(&userName, NULL))) {
+ ffStrbufSetNWS(&platform->userName, userName->Length / sizeof(wchar_t), userName->Buffer);
+ RtlFreeUnicodeString(userName); // Required. userName.Buffer is allocated separately
+ LsaFreeMemory(userName);
+ } else {
+ ffStrbufSetS(&platform->userName, getenv("USERNAME"));
+ }
+
+ alignas(TOKEN_USER) char buf[SECURITY_MAX_SID_SIZE + sizeof(TOKEN_USER)];
+ if (NT_SUCCESS(NtQueryInformationToken(NtCurrentProcessToken(), TokenUser, buf, sizeof(buf), &size))) {
+ TOKEN_USER* tokenUser = (TOKEN_USER*) buf;
+ UNICODE_STRING sidString = { .Buffer = buffer, .Length = 0, .MaximumLength = sizeof(buffer) };
+ if (NT_SUCCESS(RtlConvertSidToUnicodeString(&sidString, tokenUser->User.Sid, FALSE))) {
+ ffStrbufSetNWS(&platform->sid, sidString.Length / sizeof(wchar_t), sidString.Buffer);
+ }
+ }
+}
+
+static void getHostName(FFPlatform* platform) {
+ wchar_t buffer[256];
+ DWORD len = ARRAY_SIZE(buffer);
+ if (GetComputerNameExW(ComputerNameDnsHostname, buffer, &len) && len > 0) {
+ ffStrbufSetNWS(&platform->hostName, len, buffer);
+ } else {
+ len = ARRAY_SIZE(buffer);
+ if (GetComputerNameExW(ComputerNameNetBIOS, buffer, &len) && len > 0) {
+ ffStrbufSetNWS(&platform->hostName, len, buffer);
+ }
+ }
+}
+
+static void getUserShell(FFPlatform* platform) {
+ // Works in MSYS2
+ const char* userShell = getenv("SHELL");
+ if (userShell) {
+ ffStrbufAppendS(&platform->userShell, userShell);
+ ffStrbufReplaceAllC(&platform->userShell, '\\', '/');
+ }
+}
+
+static const char* detectWine(void) {
+ const char* __cdecl wine_get_version(void);
+ void* hntdll = ffLibraryGetModule(L"ntdll.dll");
+ if (!hntdll) {
+ return NULL;
+ }
+ FF_LIBRARY_LOAD_SYMBOL_LAZY(hntdll, wine_get_version);
+ if (!ffwine_get_version) {
+ return NULL;
+ }
+ return ffwine_get_version();
+}
+
+static void getSystemReleaseAndVersion(FFPlatformSysinfo* info) {
+ FF_AUTO_CLOSE_FD HANDLE hKey = NULL;
+ if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", &hKey, NULL)) {
+ return;
+ }
+
+ uint32_t ubr = 0;
+ ffRegReadValues(hKey, 2, (FFRegValueArg[]) {
+ FF_ARG(ubr, L"UBR"),
+ FF_ARG(info->version, L"BuildLabEx"),
+ },
+ NULL);
+
+ PPEB_FULL peb = ffGetPeb();
+
+ ffStrbufSetF(&info->release,
+ "%u.%u.%u.%u",
+ (unsigned) peb->OSMajorVersion,
+ (unsigned) peb->OSMinorVersion,
+ (unsigned) peb->OSBuildNumber,
+ (unsigned) ubr);
+
+ const char* wineVersion = detectWine();
+ if (wineVersion) {
+ ffStrbufSetF(&info->name, "Wine_%s", wineVersion);
+ } else {
+ ffStrbufSetStatic(&info->name, "WIN32_NT");
+ }
+}
+
+static void getSystemPageSize(FFPlatformSysinfo* info) {
+ SYSTEM_BASIC_INFORMATION sbi;
+ if (NT_SUCCESS(NtQuerySystemInformation(SystemBasicInformation, &sbi, sizeof(sbi), NULL))) {
+ info->pageSize = sbi.PhysicalPageSize;
+ } else {
+ info->pageSize = 4096;
+ }
+}
+
+static void getSystemArchitecture(FFPlatformSysinfo* info) {
+ SYSTEM_PROCESSOR_INFORMATION spi;
+ if (NT_SUCCESS(NtQuerySystemInformation(SystemProcessorInformation, &spi, sizeof(spi), NULL))) {
+ switch (spi.ProcessorArchitecture) {
+ case PROCESSOR_ARCHITECTURE_AMD64:
+ ffStrbufSetStatic(&info->architecture, "x86_64");
+ break;
+ case PROCESSOR_ARCHITECTURE_IA64:
+ ffStrbufSetStatic(&info->architecture, "ia64");
+ break;
+ case PROCESSOR_ARCHITECTURE_INTEL:
+ switch (spi.ProcessorLevel) {
+ case 4:
+ ffStrbufSetStatic(&info->architecture, "i486");
+ break;
+ case 5:
+ ffStrbufSetStatic(&info->architecture, "i586");
+ break;
+ case 6:
+ ffStrbufSetStatic(&info->architecture, "i686");
+ break;
+ default:
+ ffStrbufSetStatic(&info->architecture, "i386");
+ break;
+ }
+ break;
+ case PROCESSOR_ARCHITECTURE_ARM64:
+ ffStrbufSetStatic(&info->architecture, "aarch64");
+ break;
+ case PROCESSOR_ARCHITECTURE_ARM:
+ ffStrbufSetStatic(&info->architecture, "arm");
+ break;
+ case PROCESSOR_ARCHITECTURE_PPC:
+ ffStrbufSetStatic(&info->architecture, "ppc");
+ break;
+ case PROCESSOR_ARCHITECTURE_MIPS:
+ ffStrbufSetStatic(&info->architecture, "mips");
+ break;
+ case PROCESSOR_ARCHITECTURE_ALPHA:
+ ffStrbufSetStatic(&info->architecture, "alpha");
+ break;
+ case PROCESSOR_ARCHITECTURE_ALPHA64:
+ ffStrbufSetStatic(&info->architecture, "alpha64");
+ break;
+ case PROCESSOR_ARCHITECTURE_UNKNOWN:
+ default:
+ ffStrbufSetStatic(&info->architecture, "unknown");
+ break;
+ }
+ }
+}
+
+static void getCwd(FFPlatform* platform) {
+ PCURDIR cwd = &ffGetPeb()->ProcessParameters->CurrentDirectory;
+ ffStrbufSetNWS(&platform->cwd, cwd->DosPath.Length / sizeof(WCHAR), cwd->DosPath.Buffer);
+ ffStrbufReplaceAllC(&platform->cwd, '\\', '/');
+ ffStrbufEnsureEndsWithC(&platform->cwd, '/');
+}
+
+void ffPlatformInitImpl(FFPlatform* platform) {
+ platform->pid = (uint32_t) (uintptr_t) ffGetTeb()->ClientId.UniqueProcess;
+ getExePath(platform);
+ getCwd(platform);
+ getHomeDir(platform);
+ getCacheDir(platform);
+ getConfigDirs(platform);
+ getDataDirs(platform);
+
+ getUserName(platform);
+ getHostName(platform);
+ getUserShell(platform);
+
+ getSystemReleaseAndVersion(&platform->sysinfo);
+ getSystemArchitecture(&platform->sysinfo);
+ getSystemPageSize(&platform->sysinfo);
+}
diff --git a/src/common/impl/FFlist.c b/src/common/impl/FFlist.c
new file mode 100644
index 0000000..eddd0fa
--- /dev/null
+++ b/src/common/impl/FFlist.c
@@ -0,0 +1,34 @@
+#include "common/FFlist.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+void* ffListAdd(FFlist* list, uint32_t elementSize) {
+ if (list->length == list->capacity) {
+ ffListReserve(list, elementSize, list->capacity == 0 ? FF_LIST_DEFAULT_ALLOC : list->capacity * 2);
+ }
+
+ ++list->length;
+ return ffListGet(list, elementSize, list->length - 1);
+}
+
+bool ffListShift(FFlist* list, uint32_t elementSize, void* __restrict result) {
+ if (list->length == 0) {
+ return false;
+ }
+
+ memcpy(result, list->data, elementSize);
+ memmove(list->data, list->data + elementSize, (size_t) elementSize * (list->length - 1));
+ --list->length;
+ return true;
+}
+
+bool ffListPop(FFlist* list, uint32_t elementSize, void* __restrict result) {
+ if (list->length == 0) {
+ return false;
+ }
+
+ memcpy(result, ffListGet(list, elementSize, list->length - 1), elementSize);
+ --list->length;
+ return true;
+}
diff --git a/src/common/impl/FFstrbuf.c b/src/common/impl/FFstrbuf.c
new file mode 100644
index 0000000..1f6477b
--- /dev/null
+++ b/src/common/impl/FFstrbuf.c
@@ -0,0 +1,903 @@
+#include "common/FFstrbuf.h"
+#include "common/mallocHelper.h"
+#include "common/strutil.h"
+
+#include <ctype.h>
+#include <inttypes.h>
+#include <math.h>
+
+char* CHAR_NULL_PTR = "";
+
+void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate) {
+ strbuf->allocated = allocate;
+
+ if (strbuf->allocated > 0) {
+ strbuf->chars = (char*) malloc(sizeof(char) * strbuf->allocated);
+ }
+
+ // This will set the length to zero and the null byte.
+ ffStrbufClear(strbuf);
+}
+
+void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
+ assert(format != NULL);
+
+ char* buffer = NULL;
+ int len = vasprintf(&buffer, format, arguments);
+ assert(len >= 0);
+
+ ffStrbufInitMoveNS(strbuf, (uint32_t) len, buffer);
+}
+
+// Takes ownership of `heapStr`. The caller must not free `heapStr` after calling this
+// function; the memory will be managed and freed via the associated FFstrbuf.
+void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr) {
+ assert(heapStr != NULL);
+
+ strbuf->length = length;
+ size_t allocSize = ffMallocUsableSize(heapStr);
+ if (allocSize == 0) {
+ allocSize = length + 1;
+ } else if (allocSize > UINT32_MAX) {
+ allocSize = UINT32_MAX;
+ }
+ strbuf->allocated = (uint32_t) allocSize;
+ strbuf->chars = heapStr;
+}
+
+void ffStrbufEnsureFree(FFstrbuf* strbuf, uint32_t free) {
+ if (ffStrbufGetFree(strbuf) >= free && !(strbuf->allocated == 0 && strbuf->length > 0)) {
+ return;
+ }
+
+ uint32_t allocate = strbuf->allocated;
+ if (allocate < FASTFETCH_STRBUF_DEFAULT_ALLOC) {
+ allocate = FASTFETCH_STRBUF_DEFAULT_ALLOC;
+ }
+
+ while ((strbuf->length + free + 1) > allocate) { // + 1 for the null byte
+ allocate *= 2;
+ }
+
+ if (strbuf->allocated == 0) {
+ char* newbuf = malloc(sizeof(*strbuf->chars) * allocate);
+ if (strbuf->length == 0) {
+ *newbuf = '\0';
+ } else {
+ memcpy(newbuf, strbuf->chars, strbuf->length + 1);
+ }
+ strbuf->chars = newbuf;
+ } else {
+ strbuf->chars = realloc(strbuf->chars, sizeof(*strbuf->chars) * allocate);
+ }
+
+ strbuf->allocated = allocate;
+}
+
+// Ensure that at least `free` bytes are available in the buffer besides the current length
+// for an empty buffer, free + 1 length memory will be allocated(+1 for the NUL)
+void ffStrbufEnsureFixedLengthFree(FFstrbuf* strbuf, uint32_t free) {
+ uint32_t oldFree = ffStrbufGetFree(strbuf);
+ if (oldFree >= free && !(strbuf->allocated == 0 && strbuf->length > 0)) {
+ return;
+ }
+
+ uint32_t newCap = strbuf->allocated + (free - oldFree);
+
+ if (strbuf->allocated == 0) {
+ newCap += strbuf->length + 1;
+ char* newbuf = malloc(sizeof(*strbuf->chars) * newCap);
+ if (strbuf->length == 0) {
+ *newbuf = '\0';
+ } else {
+ memcpy(newbuf, strbuf->chars, strbuf->length + 1);
+ }
+ strbuf->chars = newbuf;
+ } else {
+ strbuf->chars = realloc(strbuf->chars, sizeof(*strbuf->chars) * newCap);
+ }
+
+ strbuf->allocated = newCap;
+}
+
+void ffStrbufClear(FFstrbuf* strbuf) {
+ assert(strbuf != NULL);
+
+ if (strbuf->allocated == 0) {
+ strbuf->chars = CHAR_NULL_PTR;
+ } else {
+ strbuf->chars[0] = '\0';
+ }
+
+ strbuf->length = 0;
+}
+
+void ffStrbufAppendC(FFstrbuf* strbuf, char c) {
+ ffStrbufEnsureFree(strbuf, 1);
+ strbuf->chars[strbuf->length++] = c;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufAppendNC(FFstrbuf* strbuf, uint32_t num, char c) {
+ if (num == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, num);
+ memset(&strbuf->chars[strbuf->length], c, num);
+ strbuf->length += num;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufAppendNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
+ if (value == NULL || length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, length);
+ memcpy(&strbuf->chars[strbuf->length], value, length);
+ strbuf->length += length;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transformFunc)(int)) {
+ if (value == NULL) {
+ return;
+ }
+
+ // Ensure capacity > 0 or the modification below will fail
+ uint32_t length = (uint32_t) strlen(value);
+ if (length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, length);
+ for (uint32_t i = 0; value[i] != '\0'; i++) {
+ strbuf->chars[strbuf->length++] = (char) transformFunc(value[i]);
+ }
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments) {
+ assert(format != NULL);
+
+ va_list copy;
+ va_copy(copy, arguments);
+
+ uint32_t free = ffStrbufGetFree(strbuf);
+ int written = vsnprintf(strbuf->chars + strbuf->length, strbuf->allocated > 0 ? free + 1 : 0, format, arguments);
+
+ if (written > 0 && (uint32_t) written > free) {
+ ffStrbufEnsureFree(strbuf, (uint32_t) written);
+ written = vsnprintf(strbuf->chars + strbuf->length, (uint32_t) written + 1, format, copy);
+ }
+
+ va_end(copy);
+
+ if (written > 0) {
+ strbuf->length += (uint32_t) written;
+ }
+}
+
+const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char until) {
+ if (value == NULL) {
+ return NULL;
+ }
+
+ const char* end = strchr(value, until);
+ if (end == NULL) {
+ ffStrbufAppendS(strbuf, value);
+ } else {
+ ffStrbufAppendNS(strbuf, (uint32_t) (end - value), value);
+ }
+ return end;
+}
+
+void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...) {
+ assert(format != NULL);
+
+ va_list arguments;
+ va_start(arguments, format);
+
+ if (strbuf->allocated == 0) {
+ ffStrbufInitVF(strbuf, format, arguments);
+ va_end(arguments);
+ return;
+ }
+
+ ffStrbufClear(strbuf);
+ ffStrbufAppendVF(strbuf, format, arguments);
+ va_end(arguments);
+}
+
+void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...) {
+ assert(format != NULL);
+
+ va_list arguments;
+ va_start(arguments, format);
+ ffStrbufAppendVF(strbuf, format, arguments);
+ va_end(arguments);
+}
+
+void ffStrbufPrependNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
+ if (value == NULL || length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, length);
+ memmove(strbuf->chars + length, strbuf->chars, strbuf->length + 1); // + 1 for the null byte
+ memcpy(strbuf->chars, value, length);
+ strbuf->length += length;
+}
+
+void ffStrbufPrependC(FFstrbuf* strbuf, char c) {
+ ffStrbufEnsureFree(strbuf, 1);
+ memmove(strbuf->chars + 1, strbuf->chars, strbuf->length + 1); // + 1 for the null byte
+ strbuf->chars[0] = c;
+ strbuf->length += 1;
+}
+
+void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value) {
+ assert(strbuf != NULL);
+
+ if (length == 0) {
+ ffStrbufClear(strbuf);
+ return;
+ }
+
+ assert(value != NULL);
+
+ if (strbuf->allocated <= length) {
+ char* newBuf = malloc(sizeof(char) * (length + 1));
+ memcpy(newBuf, value, length);
+ if (strbuf->allocated > 0) {
+ free(strbuf->chars);
+ }
+ strbuf->chars = newBuf;
+ strbuf->allocated = length + 1;
+ } else {
+ memmove(strbuf->chars, value, length);
+ }
+
+ strbuf->length = length;
+ strbuf->chars[length] = '\0';
+}
+
+void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value) {
+ assert(value && value != strbuf);
+
+ if (value->length == 0) {
+ ffStrbufClear(strbuf);
+ return;
+ }
+
+ if (value->allocated == 0) // static string
+ {
+ if (strbuf->allocated != 0) {
+ free(strbuf->chars);
+ strbuf->allocated = 0;
+ }
+ strbuf->chars = value->chars;
+ strbuf->length = value->length;
+ return;
+ }
+ ffStrbufSetNS(strbuf, value->length, value->chars);
+}
+
+void ffStrbufTrimLeft(FFstrbuf* strbuf, char c) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ uint32_t index = 0;
+ while (index < strbuf->length && strbuf->chars[index] == c) {
+ ++index;
+ }
+
+ if (index == 0) {
+ return;
+ }
+
+ if (strbuf->allocated == 0) {
+ // static string
+ strbuf->length -= index;
+ strbuf->chars += index;
+ return;
+ }
+
+ memmove(strbuf->chars, strbuf->chars + index, strbuf->length - index);
+ strbuf->length -= index;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufTrimRight(FFstrbuf* strbuf, char c) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ if (!ffStrbufEndsWithC(strbuf, c)) {
+ return;
+ }
+
+ do {
+ --strbuf->length;
+ } while (ffStrbufEndsWithC(strbuf, c));
+
+ if (strbuf->allocated == 0) {
+ // static string
+ ffStrbufInitNS(strbuf, strbuf->length, strbuf->chars);
+ return;
+ }
+
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufTrimLeftSpace(FFstrbuf* strbuf) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ uint32_t index = 0;
+ while (index < strbuf->length && isspace(strbuf->chars[index])) {
+ ++index;
+ }
+
+ if (index == 0) {
+ return;
+ }
+
+ if (strbuf->allocated == 0) {
+ // static string
+ strbuf->length -= index;
+ strbuf->chars += index;
+ return;
+ }
+
+ memmove(strbuf->chars, strbuf->chars + index, strbuf->length - index);
+ strbuf->length -= index;
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+void ffStrbufTrimRightSpace(FFstrbuf* strbuf) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ if (!ffStrbufEndsWithFn(strbuf, isspace)) {
+ return;
+ }
+
+ do {
+ --strbuf->length;
+ } while (ffStrbufEndsWithFn(strbuf, isspace));
+
+ if (strbuf->allocated == 0) {
+ // static string
+ ffStrbufInitNS(strbuf, strbuf->length, strbuf->chars);
+ return;
+ }
+
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+bool ffStrbufRemoveSubstr(FFstrbuf* strbuf, uint32_t startIndex, uint32_t endIndex) {
+ if (startIndex > strbuf->length || startIndex >= endIndex) {
+ return false;
+ }
+
+ if (endIndex > strbuf->length) {
+ ffStrbufSubstrBefore(strbuf, startIndex);
+ return true;
+ }
+
+ ffStrbufEnsureFree(strbuf, 0);
+ memmove(strbuf->chars + startIndex, strbuf->chars + endIndex, strbuf->length - endIndex);
+ strbuf->length -= (endIndex - startIndex);
+ strbuf->chars[strbuf->length] = '\0';
+ return true;
+}
+
+void ffStrbufRemoveS(FFstrbuf* strbuf, const char* str) {
+ uint32_t stringLength = (uint32_t) strlen(str);
+
+ for (uint32_t i = ffStrbufNextIndexS(strbuf, 0, str); i < strbuf->length; i = ffStrbufNextIndexS(strbuf, i, str)) {
+ ffStrbufRemoveSubstr(strbuf, i, i + stringLength);
+ }
+}
+
+void ffStrbufRemoveStrings(FFstrbuf* strbuf, uint32_t numStrings, const char* strings[]) {
+ for (uint32_t i = 0; i < numStrings; i++) {
+ ffStrbufRemoveS(strbuf, strings[i]);
+ }
+}
+
+void ffStrbufReplaceAllC(FFstrbuf* strbuf, char find, char replace) {
+ if (strbuf->length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, 0);
+ for (
+ char* current_pos = memchr(strbuf->chars, find, strbuf->length);
+ current_pos;
+ current_pos = memchr(
+ current_pos + 1,
+ find,
+ strbuf->length - (uint32_t) (current_pos + 1 - strbuf->chars))) {
+ *current_pos = replace;
+ }
+}
+
+bool ffStrbufSubstrBefore(FFstrbuf* strbuf, uint32_t index) {
+ if (strbuf->length <= index) {
+ return false;
+ }
+
+ if (strbuf->allocated == 0) {
+ // static string
+ if (index < strbuf->length) {
+ ffStrbufInitNS(strbuf, index, strbuf->chars);
+ }
+ return true;
+ }
+
+ strbuf->length = index;
+ strbuf->chars[strbuf->length] = '\0';
+ return true;
+}
+
+bool ffStrbufSubstrAfter(FFstrbuf* strbuf, uint32_t index) {
+ if (index >= strbuf->length) {
+ ffStrbufClear(strbuf);
+ return true;
+ }
+
+ if (strbuf->allocated == 0) {
+ // static string
+ strbuf->length -= index + 1;
+ strbuf->chars += index + 1;
+ return true;
+ }
+
+ memmove(strbuf->chars, strbuf->chars + index + 1, strbuf->length - index - 1);
+ strbuf->length -= (index + 1);
+ strbuf->chars[strbuf->length] = '\0';
+ return true;
+}
+
+bool ffStrbufSubstrAfterFirstC(FFstrbuf* strbuf, char c) {
+ uint32_t index = ffStrbufFirstIndexC(strbuf, c);
+ if (index >= strbuf->length) {
+ return false;
+ }
+ ffStrbufSubstrAfter(strbuf, index);
+ return true;
+}
+
+bool ffStrbufSubstrAfterFirstS(FFstrbuf* strbuf, const char* str) {
+ if (*str == '\0') {
+ return false;
+ }
+
+ uint32_t index = ffStrbufFirstIndexS(strbuf, str) + (uint32_t) strlen(str) - 1; // -1, because firstIndexS is already pointing to str[0], we want to add only the remaining length
+ if (index >= strbuf->length) {
+ return false;
+ }
+
+ ffStrbufSubstrAfter(strbuf, index);
+ return true;
+}
+
+bool ffStrbufSubstrAfterLastC(FFstrbuf* strbuf, char c) {
+ uint32_t index = ffStrbufLastIndexC(strbuf, c);
+ if (index >= strbuf->length) {
+ return false;
+ }
+
+ ffStrbufSubstrAfter(strbuf, index);
+ return true;
+}
+
+bool ffStrbufSubstr(FFstrbuf* strbuf, uint32_t start, uint32_t end) {
+ if (__builtin_expect(start >= end, false)) {
+ ffStrbufClear(strbuf);
+ return false;
+ }
+
+ if (__builtin_expect(start == 0, false)) {
+ return ffStrbufSubstrBefore(strbuf, end);
+ }
+ if (__builtin_expect(end >= strbuf->length, false)) {
+ return ffStrbufSubstrAfter(strbuf, start - 1);
+ }
+
+ uint32_t len = end - start;
+ ffStrbufEnsureFixedLengthFree(strbuf, len); // In case of static string
+ memmove(strbuf->chars, strbuf->chars + start, len);
+
+ strbuf->length = len;
+ strbuf->chars[len] = '\0';
+ return true;
+}
+
+uint32_t ffStrbufCountC(const FFstrbuf* strbuf, char c) {
+ uint32_t result = 0;
+ for (uint32_t i = 0; i < strbuf->length; i++) {
+ if (strbuf->chars[i] == c) {
+ result++;
+ }
+ }
+
+ return result;
+}
+
+bool ffStrbufRemoveIgnCaseEndS(FFstrbuf* strbuf, const char* end) {
+ uint32_t endLength = (uint32_t) strlen(end);
+ if (ffStrbufEndsWithIgnCaseNS(strbuf, endLength, end)) {
+ ffStrbufSubstrBefore(strbuf, strbuf->length - endLength);
+ return true;
+ }
+
+ return false;
+}
+
+bool ffStrbufEnsureEndsWithC(FFstrbuf* strbuf, char c) {
+ if (ffStrbufEndsWithC(strbuf, c)) {
+ return false;
+ }
+
+ ffStrbufAppendC(strbuf, c);
+ return true;
+}
+
+void ffStrbufWriteTo(const FFstrbuf* strbuf, FILE* file) {
+ fwrite(strbuf->chars, sizeof(*strbuf->chars), strbuf->length, file);
+}
+
+void ffStrbufPutTo(const FFstrbuf* strbuf, FILE* file) {
+ ffStrbufWriteTo(strbuf, file);
+ fputc('\n', file);
+}
+
+double ffStrbufToDouble(const FFstrbuf* strbuf, double defaultValue) {
+ char* str_end;
+ double result = strtod(strbuf->chars, &str_end);
+ return str_end == strbuf->chars ? defaultValue : result;
+}
+
+uint64_t ffStrbufToUInt(const FFstrbuf* strbuf, uint64_t defaultValue) {
+ char* str_end;
+ unsigned long long result = strtoull(strbuf->chars, &str_end, 10);
+ return str_end == strbuf->chars ? defaultValue : (uint64_t) result;
+}
+
+int64_t ffStrbufToSInt(const FFstrbuf* strbuf, int64_t defaultValue) {
+ char* str_end;
+ long long result = strtoll(strbuf->chars, &str_end, 10);
+ return str_end == strbuf->chars ? defaultValue : (int64_t) result;
+}
+
+void ffStrbufAppendSInt(FFstrbuf* strbuf, int64_t value) {
+ ffStrbufEnsureFree(strbuf, 21); // Required by yyjson_write_number
+ char* start = strbuf->chars + strbuf->length;
+
+ yyjson_val val = {};
+ unsafe_yyjson_set_sint(&val, value);
+ char* end = yyjson_write_number(&val, start);
+
+ assert(end != NULL);
+
+ strbuf->length += (uint32_t) (end - start);
+}
+
+void ffStrbufAppendUInt(FFstrbuf* strbuf, uint64_t value) {
+ ffStrbufEnsureFree(strbuf, 21); // Required by yyjson_write_number
+ char* start = strbuf->chars + strbuf->length;
+
+ yyjson_val val = {};
+ unsafe_yyjson_set_uint(&val, value);
+ char* end = yyjson_write_number(&val, start);
+
+ assert(end != NULL);
+
+ strbuf->length += (uint32_t) (end - start);
+}
+
+void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool trailingZeros) {
+ assert(precision <= 15); // yyjson_write_number supports up to 15 digits after the decimal point
+
+ ffStrbufEnsureFree(strbuf, 40); // Required by yyjson_write_number
+ char* start = strbuf->chars + strbuf->length;
+
+ if (precision == 0) {
+ value = round(value);
+ }
+ yyjson_val val = {};
+ unsafe_yyjson_set_double(&val, value);
+ if (precision > 0) {
+ unsafe_yyjson_set_fp_to_fixed(&val, precision);
+ }
+
+ // Write at most <precision> digits after the decimal point; doesn't append trailing zeros
+ char* end = yyjson_write_number(&val, start);
+
+ assert(end > start);
+
+ strbuf->length += (uint32_t) (end - start);
+
+ if (__builtin_expect(value > 1e21 || value < -1e21, false)) {
+ // If the value is too large, yyjson_write_number will write it in scientific notation
+ return;
+ }
+
+ if (trailingZeros) {
+ if (precision > 1) {
+ for (char* p = end - 1; *p != '.' && p > start; --p) {
+ --precision;
+ }
+ if (precision > 0) {
+ ffStrbufAppendNC(strbuf, (uint32_t) precision, '0');
+ }
+ } else if (precision == 0 || (precision < 0 && end[-1] == '0')) {
+ goto removeDecimalPoint;
+ }
+ } else {
+ if (end[-1] == '0') {
+ removeDecimalPoint:
+ // yyjson always appends ".0" to make it a float point number. We need to remove it
+ strbuf->length -= 2;
+ strbuf->chars[strbuf->length] = '\0';
+ }
+ }
+}
+
+void ffStrbufUpperCase(FFstrbuf* strbuf) {
+ for (uint32_t i = 0; i < strbuf->length; ++i) {
+ strbuf->chars[i] = (char) toupper(strbuf->chars[i]);
+ }
+}
+
+void ffStrbufLowerCase(FFstrbuf* strbuf) {
+ for (uint32_t i = 0; i < strbuf->length; ++i) {
+ strbuf->chars[i] = (char) tolower(strbuf->chars[i]);
+ }
+}
+
+void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c) {
+ if (num == 0) {
+ return;
+ }
+ if (index >= strbuf->length) {
+ index = strbuf->length;
+ }
+
+ ffStrbufEnsureFree(strbuf, num);
+ memmove(strbuf->chars + index + num, strbuf->chars + index, strbuf->length - index + 1);
+ memset(&strbuf->chars[index], c, num);
+ strbuf->length += num;
+}
+
+bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) {
+ assert(lineptr && n && buffer);
+ assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0));
+ assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length));
+
+ const char* pBufferEnd = buffer->chars + buffer->length;
+ if (!*lineptr) {
+ *lineptr = buffer->chars;
+ } else {
+ *lineptr += *n;
+ if (*lineptr >= pBufferEnd) { // non-empty last line
+ return false;
+ }
+ **lineptr = delimiter;
+ ++*lineptr;
+ }
+ if (*lineptr >= pBufferEnd) { // empty last line
+ return false;
+ }
+
+ size_t remaining = (size_t) (pBufferEnd - *lineptr);
+ char* ending = memchr(*lineptr, delimiter, remaining);
+ if (ending) {
+ *n = (size_t) (ending - *lineptr);
+ *ending = '\0';
+ } else {
+ *n = remaining;
+ }
+ return true;
+}
+
+void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) {
+ assert(buffer && lineptr && n);
+ assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0));
+ assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length));
+
+ if (!*lineptr) {
+ return;
+ }
+
+ *lineptr += *n;
+ if (*lineptr < buffer->chars + buffer->length) {
+ **lineptr = delimiter;
+ }
+}
+
+bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf) {
+ if (strbuf->allocated == 0) {
+ return false; // Doesn't work with static strings
+ }
+
+ bool changed = false;
+ for (uint32_t i = 0; i < strbuf->length; i++) {
+ if (strbuf->chars[i] != ' ') {
+ continue;
+ }
+
+ i++;
+ uint32_t j = i;
+ for (; j < strbuf->length && strbuf->chars[j] == ' '; j++);
+
+ if (j == i) {
+ continue;
+ }
+ memmove(&strbuf->chars[i], &strbuf->chars[j], strbuf->length - j + 1);
+ strbuf->length -= j - i;
+ changed = true;
+ }
+
+ return changed;
+}
+
+/// @brief Check if a separated string (comp) contains a substring (strbuf).
+/// @param strbuf The substring to check.
+/// @param compLength The length of the separated string to check.
+/// @param comp The separated string to check.
+/// @param separator The separator character.
+bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
+ if (strbuf->length == 0) {
+ return true;
+ }
+
+ if (compLength == 0) {
+ return false;
+ }
+
+ for (const char* p = comp; p < comp + compLength;) {
+ const char* colon = memchr(p, separator, (size_t) (comp + compLength - p));
+ if (colon == NULL) {
+ uint32_t remainingLen = (uint32_t) (comp + compLength - p);
+ return strbuf->length == remainingLen && memcmp(strbuf->chars, p, remainingLen) == 0;
+ }
+
+ uint32_t substrLength = (uint32_t) (colon - p);
+ if (strbuf->length == substrLength && memcmp(strbuf->chars, p, substrLength) == 0) {
+ return true;
+ }
+
+ p = colon + 1;
+ }
+
+ return false;
+}
+
+/// @brief Case insensitive version of ffStrbufMatchSeparatedNS.
+bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
+ if (strbuf->length == 0) {
+ return true;
+ }
+
+ if (compLength == 0) {
+ return false;
+ }
+
+ for (const char* p = comp; p < comp + compLength;) {
+ const char* colon = memchr(p, separator, (size_t) (comp + compLength - p));
+ if (colon == NULL) {
+ uint32_t remainingLen = (uint32_t) (comp + compLength - p);
+ return strbuf->length == remainingLen && strncasecmp(strbuf->chars, p, remainingLen) == 0;
+ }
+
+ uint32_t substrLength = (uint32_t) (colon - p);
+ if (strbuf->length == substrLength && strncasecmp(strbuf->chars, p, substrLength) == 0) {
+ return true;
+ }
+
+ p = colon + 1;
+ }
+
+ return false;
+}
+
+int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint) {
+ if (codepoint <= 0x7F) {
+ ffStrbufAppendC(strbuf, (char) codepoint);
+ return 1;
+ } else if (codepoint <= 0x7FF) {
+ ffStrbufAppendNS(strbuf, 2, (char[]) { (char) (0xC0 | (codepoint >> 6)), (char) (0x80 | (codepoint & 0x3F)) });
+ return 2;
+ } else if (codepoint <= 0xFFFF) {
+ ffStrbufAppendNS(strbuf, 3, (char[]) { (char) (0xE0 | (codepoint >> 12)), (char) (0x80 | ((codepoint >> 6) & 0x3F)), (char) (0x80 | (codepoint & 0x3F)) });
+ return 3;
+ } else if (codepoint <= 0x10FFFF) {
+ ffStrbufAppendNS(strbuf, 4, (char[]) { (char) (0xF0 | (codepoint >> 18)), (char) (0x80 | ((codepoint >> 12) & 0x3F)), (char) (0x80 | ((codepoint >> 6) & 0x3F)), (char) (0x80 | (codepoint & 0x3F)) });
+ return 4;
+ }
+
+ ffStrbufAppendS(strbuf, "�"); // U+FFFD REPLACEMENT CHARACTER
+ return 1;
+}
+
+/// @brief Check if a separated string (strbuf) contains a substring (comp).
+/// @param strbuf The separated to check.
+/// @param compLength The length of the separated string to check.
+/// @param comp The substring to check.
+/// @param separator The separator character.
+bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
+ uint32_t startIndex = 0;
+ while (startIndex < strbuf->length) {
+ uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator);
+
+ uint32_t folderLength = colonIndex - startIndex;
+ if (folderLength == compLength && memcmp(strbuf->chars + startIndex, comp, compLength) == 0) {
+ return true;
+ }
+
+ startIndex = colonIndex + 1;
+ }
+
+ return false;
+}
+
+bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) {
+ uint32_t startIndex = 0;
+ while (startIndex < strbuf->length) {
+ uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator);
+
+ uint32_t folderLength = colonIndex - startIndex;
+ if (folderLength == compLength && strncasecmp(strbuf->chars + startIndex, comp, compLength) == 0) {
+ return true;
+ }
+
+ startIndex = colonIndex + 1;
+ }
+
+ return false;
+}
+
+bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf) {
+ assert(strbuf);
+
+ if (strbuf->length < 4) {
+ return false;
+ }
+
+ // Static string must be converted first.
+ assert(strbuf->allocated > 0);
+
+ bool changed = false;
+ uint32_t read = 0;
+ uint32_t write = 0;
+
+ while (read < strbuf->length) {
+ if (
+ read + 3 < strbuf->length &&
+ strbuf->chars[read] == '\\' &&
+ strbuf->chars[read + 1] == 'x') {
+ int8_t hi = ffHexCharToInt(strbuf->chars[read + 2]);
+ int8_t lo = ffHexCharToInt(strbuf->chars[read + 3]);
+ if (hi >= 0 && lo >= 0) {
+ strbuf->chars[write++] = (char) ((hi << 4) | lo);
+ read += 4;
+ changed = true;
+ continue;
+ }
+ }
+
+ strbuf->chars[write++] = strbuf->chars[read++];
+ }
+
+ strbuf->length = write;
+ strbuf->chars[write] = '\0';
+ return changed;
+}
diff --git a/src/common/impl/base64.c b/src/common/impl/base64.c
new file mode 100644
index 0000000..98a415f
--- /dev/null
+++ b/src/common/impl/base64.c
@@ -0,0 +1,110 @@
+#include "common/base64.h"
+
+// https://github.com/kostya/benchmarks/blob/master/base64/test-nolib.c#L145
+void ffBase64EncodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output) {
+ static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ char* out = output;
+ const char* ends = str + (size - size % 3);
+ while (str != ends) {
+ uint32_t n = __builtin_bswap32(*(uint32_t*) str);
+ *out++ = chars[(n >> 26) & 63];
+ *out++ = chars[(n >> 20) & 63];
+ *out++ = chars[(n >> 14) & 63];
+ *out++ = chars[(n >> 8) & 63];
+ str += 3;
+ }
+
+ if (size % 3 == 1) {
+ uint64_t n = (uint64_t) *str << 16;
+ *out++ = chars[(n >> 18) & 63];
+ *out++ = chars[(n >> 12) & 63];
+ *out++ = '=';
+ *out++ = '=';
+ } else if (size % 3 == 2) {
+ uint64_t n = (uint64_t) *str++ << 16;
+ n |= (uint64_t) *str << 8;
+ *out++ = chars[(n >> 18) & 63];
+ *out++ = chars[(n >> 12) & 63];
+ *out++ = chars[(n >> 6) & 63];
+ *out++ = '=';
+ }
+ *out = '\0';
+ *out_size = (uint32_t) (out - output);
+}
+
+static uint8_t decode_table[256];
+
+static void init_decode_table() {
+ uint8_t ch = 0;
+ do {
+ int32_t code = -1;
+ if (ch >= 'A' && ch <= 'Z') {
+ code = ch - 0x41;
+ }
+ if (ch >= 'a' && ch <= 'z') {
+ code = ch - 0x47;
+ }
+ if (ch >= '0' && ch <= '9') {
+ code = ch + 0x04;
+ }
+ if (ch == '+' || ch == '-') {
+ code = 0x3E;
+ }
+ if (ch == '/' || ch == '_') {
+ code = 0x3F;
+ }
+ decode_table[ch] = (uint8_t) code;
+ } while (ch++ < 0xFF);
+}
+
+#define next_char(x) uint8_t x = decode_table[(uint8_t) *str++];
+
+bool ffBase64DecodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output) {
+ if (*(uint64_t*) decode_table == 0) {
+ init_decode_table();
+ }
+
+ char* out = output;
+ while (size > 0 && (str[size - 1] == '\n' || str[size - 1] == '\r' || str[size - 1] == '=')) {
+ size--;
+ }
+
+ const char* ends = str + size - 4;
+ while (true) {
+ if (str > ends) {
+ break;
+ }
+ while (*str == '\n' || *str == '\r') {
+ str++;
+ }
+
+ if (str > ends) {
+ break;
+ }
+ next_char(a);
+ next_char(b);
+ next_char(c);
+ next_char(d);
+
+ *out++ = (char) (a << 2 | b >> 4);
+ *out++ = (char) (b << 4 | c >> 2);
+ *out++ = (char) (c << 6 | d >> 0);
+ }
+
+ uint8_t mod = (uint8_t) (ends - str + 4) % 4;
+ if (mod == 2) {
+ next_char(a);
+ next_char(b);
+ *out++ = (char) (a << 2 | b >> 4);
+ } else if (mod == 3) {
+ next_char(a);
+ next_char(b);
+ next_char(c);
+ *out++ = (char) (a << 2 | b >> 4);
+ *out++ = (char) (b << 4 | c >> 2);
+ }
+
+ *out = '\0';
+ *out_size = (uint32_t) (out - output);
+ return true;
+}
diff --git a/src/common/impl/binary_apple.c b/src/common/impl/binary_apple.c
new file mode 100644
index 0000000..8478169
--- /dev/null
+++ b/src/common/impl/binary_apple.c
@@ -0,0 +1,303 @@
+#include "common/binary.h"
+#include "common/io.h"
+#include "common/strutil.h"
+
+#include <string.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <mach-o/loader.h>
+#include <mach-o/swap.h>
+#include <mach-o/fat.h>
+
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // swap_fat_arch
+
+// Ref: https://github.com/AlexDenisov/segment_dumper/blob/master/main.c
+
+typedef struct {
+ const uint8_t* data;
+ size_t length;
+} FFMemoryMapping;
+
+static inline void wrapMunmap(FFMemoryMapping* mapping) {
+ assert(mapping);
+ if (mapping->data == NULL || mapping->data == MAP_FAILED) {
+ return;
+ }
+ munmap((void*) mapping->data, mapping->length);
+}
+
+/**
+ * Helper function to access data from a memory-mapped file at a specific offset
+ */
+static inline const void* readData(const FFMemoryMapping* mapping, size_t size, off_t offset) {
+ if (offset < 0) {
+ return NULL;
+ }
+
+ size_t start = (size_t) offset;
+ if (start > mapping->length || size > mapping->length - start) {
+ return NULL;
+ }
+
+ return mapping->data + start;
+}
+
+/**
+ * Handles a Mach-O section by extracting strings from the __cstring section
+ *
+ * @param mapping Memory mapping of the Mach-O object file
+ * @param name Section name to check
+ * @param offset Offset of the section in the file
+ * @param size Size of the section
+ * @param cb Callback function to process strings
+ * @param userdata User data for the callback
+ * @param minLength Minimum string length to extract
+ *
+ * @return true to continue processing, false to stop
+ */
+static bool handleMachSection(const FFMemoryMapping* mapping, const char* name, off_t offset, size_t size, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ if (!ffStrEquals(name, "__cstring")) {
+ return true;
+ }
+
+ const char* data = readData(mapping, size, offset);
+ if (!data) {
+ return true;
+ }
+
+ for (size_t off = 0; off < size; ++off) {
+ const char* p = data + off;
+ if (*p == '\0') {
+ continue;
+ }
+ uint32_t len = (uint32_t) strnlen(p, size - off);
+ if (len < minLength) {
+ off += len; // Skip short strings
+ continue;
+ }
+ if (*p >= ' ' && *p <= '~') { // Ignore control characters
+ if (!cb(p, len, userdata)) {
+ return false;
+ }
+ }
+ off += len;
+ }
+ return true;
+}
+
+/**
+ * Processes a Mach-O header (32-bit or 64-bit)
+ *
+ * This function parses the load commands in a Mach-O header, looking for
+ * LC_SEGMENT or LC_SEGMENT_64 commands that contain the __TEXT segment.
+ * It then processes the sections within that segment to extract strings.
+ *
+ * @param mapping Memory mapping of the Mach-O object file
+ * @param offset Offset of the Mach header in the file
+ * @param is_64 Whether this is a 64-bit Mach-O header
+ * @param cb Callback function to process strings
+ * @param userdata User data for the callback
+ * @param minLength Minimum string length to extract
+ *
+ * @return NULL on success, error message on failure
+ */
+static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset, bool is_64, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ uint32_t ncmds;
+ off_t loadCommandsOffset = offset;
+
+ if (is_64) {
+ const struct mach_header_64* header = readData(mapping, sizeof(struct mach_header_64), offset);
+ if (!header) {
+ return "read mach header failed";
+ }
+
+ ncmds = header->ncmds;
+ loadCommandsOffset += sizeof(*header);
+ } else {
+ const struct mach_header* header = readData(mapping, sizeof(struct mach_header), offset);
+ if (!header) {
+ return "read mach header failed";
+ }
+
+ ncmds = header->ncmds;
+ loadCommandsOffset += sizeof(*header);
+ }
+
+ off_t commandOffset = loadCommandsOffset;
+ const struct load_command* cmd = NULL;
+ for (uint32_t i = 0U; i < ncmds; i++, commandOffset += cmd->cmdsize) {
+ cmd = readData(mapping, sizeof(*cmd), commandOffset);
+ if (!cmd) {
+ break;
+ }
+
+ if (cmd->cmdsize < sizeof(*cmd)) {
+ break;
+ }
+
+ if (cmd->cmd == LC_SEGMENT_64) {
+ const struct segment_command_64* segment = readData(mapping, sizeof(struct segment_command_64), commandOffset);
+ if (!segment) {
+ continue;
+ }
+
+ if (!ffStrEquals(segment->segname, "__TEXT")) {
+ continue;
+ }
+
+ for (uint32_t j = 0U; j < segment->nsects; j++) {
+ off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section_64));
+ const struct section_64* section = readData(mapping, sizeof(struct section_64), sectionOffset);
+ if (!section) {
+ continue;
+ }
+
+ if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) {
+ return NULL;
+ }
+ }
+ } else if (cmd->cmd == LC_SEGMENT) {
+ const struct segment_command* segment = readData(mapping, sizeof(struct segment_command), commandOffset);
+ if (!segment) {
+ continue;
+ }
+
+ if (!ffStrEquals(segment->segname, "__TEXT")) {
+ continue;
+ }
+
+ for (uint32_t j = 0; j < segment->nsects; j++) {
+ off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section));
+ const struct section* section = readData(mapping, sizeof(struct section), sectionOffset);
+ if (!section) {
+ continue;
+ }
+
+ if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) {
+ return NULL;
+ }
+ }
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ * Processes a Fat binary header (Universal binary)
+ *
+ * This function handles the fat header of a universal binary, which can contain
+ * multiple Mach-O binaries for different architectures. It extracts and processes
+ * each embedded Mach-O file.
+ *
+ * @param mapping Memory mapping of the universal binary
+ * @param cb Callback function to process strings
+ * @param userdata User data for the callback
+ * @param minLength Minimum string length to extract
+ *
+ * @return NULL on success, error message on failure
+ */
+static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ const struct fat_header* headerRaw = readData(mapping, sizeof(struct fat_header), 0);
+ if (!headerRaw) {
+ return "read fat header failed";
+ }
+
+ struct fat_header header = *headerRaw;
+
+ bool needSwap = header.magic == FAT_CIGAM || header.magic == FAT_CIGAM_64;
+
+ if (needSwap) {
+ swap_fat_header(&header, NX_UnknownByteOrder);
+ }
+
+ for (uint32_t i = 0U; i < header.nfat_arch; i++) {
+ off_t machHeaderOffset = 0;
+ if (header.magic == FAT_MAGIC) {
+ off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch));
+ const struct fat_arch* archRaw = readData(mapping, sizeof(struct fat_arch), archOffset);
+ if (!archRaw) {
+ continue;
+ }
+
+ struct fat_arch arch = *archRaw;
+
+ if (needSwap) {
+ swap_fat_arch(&arch, 1, NX_UnknownByteOrder);
+ }
+ machHeaderOffset = (off_t) arch.offset;
+ } else {
+ off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch_64));
+ const struct fat_arch_64* archRaw = readData(mapping, sizeof(struct fat_arch_64), archOffset);
+ if (!archRaw) {
+ continue;
+ }
+
+ struct fat_arch_64 arch = *archRaw;
+
+ if (needSwap) {
+ swap_fat_arch_64(&arch, 1, NX_UnknownByteOrder);
+ }
+
+ machHeaderOffset = (off_t) arch.offset;
+ }
+
+ const uint32_t* magic = readData(mapping, sizeof(uint32_t), machHeaderOffset);
+ if (!magic) {
+ continue;
+ }
+
+ if (*magic == MH_MAGIC_64 || *magic == MH_MAGIC) {
+ return dumpMachHeader(mapping, machHeaderOffset, *magic == MH_MAGIC_64, cb, userdata, minLength);
+ }
+ }
+ return "Unsupported fat header";
+}
+
+/**
+ * Extracts string literals from a Mach-O (Apple) binary file
+ *
+ * This function supports both single-architecture Mach-O files and
+ * universal binaries (fat binaries) containing multiple architectures.
+ * It locates the __cstring section in the __TEXT segment which contains
+ * the string literals used in the program.
+ */
+const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ FF_AUTO_CLOSE_FD int fd = open(machoFile, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ return "File could not be opened";
+ }
+
+ struct stat st;
+ if (fstat(fd, &st) != 0 || st.st_size <= 0) {
+ return "Failed to stat file";
+ }
+
+ FF_A_CLEANUP(wrapMunmap) FFMemoryMapping mapping = {
+ .data = mmap(NULL, (size_t) st.st_size, PROT_READ, MAP_PRIVATE, fd, 0),
+ .length = (size_t) st.st_size,
+ };
+ if (mapping.data == MAP_FAILED) {
+ return "mmap failed";
+ }
+
+ // Read the magic number to determine the type of binary
+ const uint32_t* magic = readData(&mapping, sizeof(uint32_t), 0);
+ if (!magic) {
+ return "read magic number failed";
+ }
+
+ // Check for supported formats
+ // MH_CIGAM and MH_CIGAM_64 seem to be no longer used, as `swap_mach_header` is marked as deprecated.
+ // However FAT_CIGAM and FAT_CIGAM_64 are still used (/usr/bin/vim).
+ if (*magic != MH_MAGIC && *magic != MH_MAGIC_64 && *magic != FAT_CIGAM && *magic != FAT_CIGAM_64 && *magic != FAT_MAGIC && *magic != FAT_MAGIC_64) {
+ return "Unsupported format or big endian mach-o file";
+ }
+
+ // Process either a fat binary or a regular Mach-O binary
+ if (*magic == FAT_MAGIC || *magic == FAT_MAGIC_64 || *magic == FAT_CIGAM || *magic == FAT_CIGAM_64) {
+ return dumpFatHeader(&mapping, cb, userdata, minLength);
+ } else {
+ return dumpMachHeader(&mapping, 0, *magic == MH_MAGIC_64, cb, userdata, minLength);
+ }
+}
diff --git a/src/common/impl/binary_linux.c b/src/common/impl/binary_linux.c
new file mode 100644
index 0000000..9a24217
--- /dev/null
+++ b/src/common/impl/binary_linux.c
@@ -0,0 +1,147 @@
+#include "common/binary.h"
+
+#if defined(FF_HAVE_ELF) || defined(__sun) || (defined(__FreeBSD__) && !defined(__DragonFly__)) || defined(__OpenBSD__) || defined(__NetBSD__)
+
+ #include "common/io.h"
+ #include "common/library.h"
+ #include "common/strutil.h"
+
+ #include <libelf.h> // #1254
+ #include <fcntl.h>
+
+/**
+ * Structure to hold dynamically loaded libelf function pointers
+ */
+struct FFElfData {
+ FF_LIBRARY_SYMBOL(elf_version)
+ FF_LIBRARY_SYMBOL(elf_begin)
+ FF_LIBRARY_SYMBOL(elf_getshdrstrndx)
+ FF_LIBRARY_SYMBOL(elf_nextscn)
+ FF_LIBRARY_SYMBOL(elf64_getshdr)
+ FF_LIBRARY_SYMBOL(elf32_getshdr)
+ FF_LIBRARY_SYMBOL(elf_getdata)
+ FF_LIBRARY_SYMBOL(elf_strptr)
+ FF_LIBRARY_SYMBOL(elf_end)
+
+ bool inited;
+} elfData;
+
+/**
+ * Extracts string literals from an ELF (Linux/Unix) binary file
+ *
+ * This function loads the libelf library dynamically, opens the ELF file,
+ * locates the .rodata section (which contains string literals), and
+ * scans it for valid strings. Each string found is passed to the
+ * callback function for processing.
+ *
+ * The function supports both 32-bit and 64-bit ELF formats.
+ */
+const char* ffBinaryExtractStrings(const char* elfFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ // Initialize libelf if not already done
+ if (!elfData.inited) {
+ elfData.inited = true;
+ FF_LIBRARY_LOAD_MESSAGE(libelf, "libelf" FF_LIBRARY_EXTENSION, 1);
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_version)
+ if (elfData.ffelf_version(EV_CURRENT) == EV_NONE) {
+ return "elf_version() failed";
+ }
+
+ // Load all required libelf functions
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_begin)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_getshdrstrndx)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_nextscn)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf64_getshdr)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf32_getshdr)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_getdata)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_strptr)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_end)
+
+ libelf = NULL;
+ }
+
+ if (elfData.ffelf_end == NULL) {
+ return "load libelf failed";
+ }
+
+ // Open the ELF file
+ FF_AUTO_CLOSE_FD int fd = open(elfFile, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ return "open() failed";
+ }
+
+ Elf* elf = elfData.ffelf_begin(fd, ELF_C_READ, NULL);
+ if (elf == NULL) {
+ return "elf_begin() failed";
+ }
+
+ // Get the section header string table index
+ size_t shstrndx = 0;
+ if (elfData.ffelf_getshdrstrndx(elf, &shstrndx) < 0) {
+ elfData.ffelf_end(elf);
+ return "elf_getshdrstrndx() failed";
+ }
+
+ // Iterate through all sections, looking for .rodata which contains string literals
+ Elf_Scn* scn = NULL;
+ while ((scn = elfData.ffelf_nextscn(elf, scn)) != NULL) {
+ // Try 64-bit section header first, then 32-bit if that fails
+ Elf64_Shdr* shdr64 = elfData.ffelf64_getshdr(scn);
+ Elf32_Shdr* shdr32 = NULL;
+ if (shdr64 == NULL) {
+ shdr32 = elfData.ffelf32_getshdr(scn);
+ if (shdr32 == NULL) {
+ continue;
+ }
+ }
+
+ // Get the section name and check if it's .rodata
+ const char* name = elfData.ffelf_strptr(elf, shstrndx, shdr64 ? shdr64->sh_name : shdr32->sh_name);
+ if (name == NULL || !ffStrEquals(name, ".rodata")) {
+ continue;
+ }
+
+ // Get the section data
+ Elf_Data* data = elfData.ffelf_getdata(scn, NULL);
+ if (data == NULL) {
+ continue;
+ }
+
+ // Scan the section for string literals
+ for (size_t off = 0; off < data->d_size; ++off) {
+ const char* p = (const char*) data->d_buf + off;
+ if (*p == '\0') {
+ continue;
+ }
+ uint32_t len = (uint32_t) strnlen(p, data->d_size - off);
+ if (len < minLength) {
+ off += len;
+ continue;
+ }
+ // Only process printable ASCII characters
+ if (*p >= ' ' && *p <= '~') // Ignore control characters
+ {
+ if (!cb(p, len, userdata)) {
+ break;
+ }
+ }
+ off += len;
+ }
+
+ break;
+ }
+
+ elfData.ffelf_end(elf);
+ return NULL;
+}
+
+#else
+
+/**
+ * Fallback implementation when libelf is not available
+ */
+const char* ffBinaryExtractStrings(const char* file, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ FF_UNUSED(file, cb, userdata, minLength);
+ return "Fastfetch was built without libelf support";
+}
+
+#endif
diff --git a/src/common/impl/binary_windows.c b/src/common/impl/binary_windows.c
new file mode 100644
index 0000000..918cad5
--- /dev/null
+++ b/src/common/impl/binary_windows.c
@@ -0,0 +1,71 @@
+#include "common/binary.h"
+#include "common/io.h"
+#include "common/strutil.h"
+#include "common/windows/nt.h"
+
+#include <windows.h>
+#include <stdlib.h>
+#include <string.h>
+
+/**
+ * Extracts string literals from a PE (Windows) executable
+ *
+ * This function maps the PE file into memory, locates the .rdata section
+ * (which typically contains string literals), and scans it for valid strings.
+ * Each string found is passed to the callback function for processing.
+ */
+const char* ffBinaryExtractStrings(const char* peFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) {
+ FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileA(peFile, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (hFile == INVALID_HANDLE_VALUE) {
+ return "CreateFileA() failed";
+ }
+
+ FF_AUTO_CLOSE_FD HANDLE hSection = NULL;
+ if (!NT_SUCCESS(NtCreateSection(&hSection, SECTION_MAP_READ, NULL, NULL, PAGE_READONLY, SEC_COMMIT, hFile))) {
+ return "NtCreateSection() failed";
+ }
+
+ PVOID base = NULL;
+ SIZE_T viewSize = 0;
+ if (!NT_SUCCESS(NtMapViewOfSection(hSection, NtCurrentProcess(), &base, 0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_READONLY))) {
+ return "NtMapViewOfSection() failed";
+ }
+
+ PIMAGE_NT_HEADERS ntHeaders = RtlImageNtHeader(base);
+ if (!ntHeaders) {
+ NtUnmapViewOfSection(NtCurrentProcess(), base);
+ return "RtlImageNtHeader() failed";
+ }
+
+ PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders);
+ for (WORD i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i, ++section) {
+ // Look for initialized data sections with the name ".rdata" which typically contains string literals
+ if ((section->Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) && ffStrEquals((const char*) section->Name, ".rdata")) {
+ uint8_t* data = (uint8_t*) base + section->PointerToRawData;
+
+ // Scan the section for string literals
+ for (size_t off = 0; off < section->SizeOfRawData; ++off) {
+ const char* p = (const char*) data + off;
+ if (*p == '\0') {
+ continue;
+ }
+ uint32_t len = (uint32_t) strnlen(p, section->SizeOfRawData - off);
+ if (len < minLength) {
+ off += len;
+ continue;
+ }
+ // Only process printable ASCII characters
+ if (*p >= ' ' && *p <= '~') // Ignore control characters
+ {
+ if (!cb(p, len, userdata)) {
+ break;
+ }
+ }
+ off += len;
+ }
+ }
+ }
+
+ NtUnmapViewOfSection(NtCurrentProcess(), base);
+ return NULL;
+}
diff --git a/src/common/impl/commandoption.c b/src/common/impl/commandoption.c
new file mode 100644
index 0000000..2f548df
--- /dev/null
+++ b/src/common/impl/commandoption.c
@@ -0,0 +1,261 @@
+#include "common/commandoption.h"
+#include "common/color.h"
+#include "common/printing.h"
+#include "common/time.h"
+#include "common/jsonconfig.h"
+#include "common/strutil.h"
+#include "fastfetch_datatext.h"
+#include "modules/modules.h"
+
+#include <ctype.h>
+#include <inttypes.h>
+
+bool ffParseModuleOptions(const char* key, const char* value) {
+ if (!ffStrStartsWith(key, "--") || !ffCharIsEnglishAlphabet(key[2])) {
+ return false;
+ }
+ if (value && !*value) {
+ value = NULL;
+ }
+ for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(key[2]) - 'A']; *modules; ++modules) {
+ FFModuleBaseInfo* baseInfo = *modules;
+ const char* subKey = ffOptionTestPrefix(key, baseInfo->name);
+ if (subKey != NULL) {
+ if (subKey[0] == '\0' || subKey[0] == '-') // Key is exactly the module name or has a leading '-'
+ {
+ fprintf(stderr, "Error: unknown module key %s\n", key);
+ exit(477);
+ }
+
+ FF_STRBUF_AUTO_DESTROY moduleName = ffStrbufCreateS(baseInfo->name);
+ ffStrbufLowerCase(&moduleName);
+
+ FF_STRBUF_AUTO_DESTROY jsonKey = ffStrbufCreate();
+ bool flag = false;
+ for (const char* p = subKey; *p; ++p) {
+ if (*p == '-') {
+ if (flag) {
+ fprintf(stderr, "Error: invalid double `-` in module key %s\n", key);
+ exit(477);
+ }
+ flag = true;
+ } else {
+ if (!isalpha((unsigned char) *p) && !isdigit((unsigned char) *p)) {
+ fprintf(stderr, "Error: invalid character `%c` in module key %s\n", *p, key);
+ exit(477);
+ }
+
+ if (flag) {
+ flag = false;
+ ffStrbufAppendC(&jsonKey, (char) toupper((unsigned char) *p));
+ } else {
+ ffStrbufAppendC(&jsonKey, *p);
+ }
+ }
+ }
+ fprintf(stderr, "Error: Unsupported module option: %s\n", key);
+ fputs(" Support of module options has been removed. Please add the flag to the JSON config instead.\n", stderr);
+ fprintf(stderr, " Example (demonstration only): `{ \"modules\": [ { \"type\": \"%s\", \"%s\": %s%s%s } ] }`\n", moduleName.chars, jsonKey.chars, value ? "\"" : "", value ?: "true", value ? "\"" : "");
+ fputs(" See <https://github.com/fastfetch-cli/fastfetch/wiki/Configuration> for more information.\n", stderr);
+ exit(477);
+ }
+ }
+ return false;
+}
+
+void ffPrepareCommandOption(FFdata* data) {
+ char* moduleType = NULL;
+ size_t moduleLen = 0;
+ while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) {
+#define FF_IF_MODULE_MATCH(moduleNameConstant) if (moduleLen == strlen(moduleNameConstant) && ffStrEqualsIgnCase(moduleType, moduleNameConstant) && !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleNameConstant, ':'))
+
+ switch (moduleType[0]) {
+ #if !FF_MODULE_DISABLE_CPUUSAGE
+ case 'C':
+ case 'c':
+ FF_IF_MODULE_MATCH(FF_CPUUSAGE_MODULE_NAME)
+ ffPrepareCPUUsage();
+ break;
+ #endif
+
+ #if !FF_MODULE_DISABLE_DISKIO
+ case 'D':
+ case 'd':
+ FF_IF_MODULE_MATCH(FF_DISKIO_MODULE_NAME) {
+ FF_A_CLEANUP(ffDestroyDiskIOOptions) FFDiskIOOptions options;
+ ffInitDiskIOOptions(&options);
+ ffPrepareDiskIO(&options);
+ }
+ break;
+ #endif
+
+ #if !FF_MODULE_DISABLE_NETIO
+ case 'N':
+ case 'n':
+ FF_IF_MODULE_MATCH(FF_NETIO_MODULE_NAME) {
+ FF_A_CLEANUP(ffDestroyNetIOOptions) FFNetIOOptions options;
+ ffInitNetIOOptions(&options);
+ ffPrepareNetIO(&options);
+ }
+ break;
+ #endif
+
+ #if !FF_MODULE_DISABLE_PUBLICIP
+ case 'P':
+ case 'p':
+ FF_IF_MODULE_MATCH(FF_PUBLICIP_MODULE_NAME) {
+ FF_A_CLEANUP(ffDestroyPublicIpOptions) FFPublicIPOptions options;
+ ffInitPublicIpOptions(&options);
+ ffPreparePublicIp(&options);
+ }
+ break;
+ #endif
+
+ #if !FF_MODULE_DISABLE_WEATHER
+ case 'W':
+ case 'w':
+ FF_IF_MODULE_MATCH(FF_WEATHER_MODULE_NAME) {
+ FF_A_CLEANUP(ffDestroyWeatherOptions) FFWeatherOptions options;
+ ffInitWeatherOptions(&options);
+ ffPrepareWeather(&options);
+ }
+ break;
+ #endif
+ }
+
+#undef FF_IF_MODULE_MATCH
+ }
+}
+
+static void genJsonConfig(FFdata* data, FFModuleBaseInfo* baseInfo, void* options) {
+ yyjson_mut_doc* doc = data->resultDoc;
+
+ yyjson_mut_val* modules = yyjson_mut_obj_get(doc->root, "modules");
+ if (!modules) {
+ modules = yyjson_mut_obj_add_arr(doc, doc->root, "modules");
+ }
+
+ FF_STRBUF_AUTO_DESTROY type = ffStrbufCreateS(baseInfo->name);
+ ffStrbufLowerCase(&type);
+
+ if (data->docType == FF_RESULT_DOC_TYPE_CONFIG_FULL) {
+ yyjson_mut_val* module = yyjson_mut_obj(doc);
+ yyjson_mut_obj_add_strbuf(doc, module, "type", &type);
+
+ if (baseInfo->generateJsonConfig) {
+ baseInfo->generateJsonConfig(options, doc, module);
+ }
+
+ if (yyjson_mut_obj_size(module) > 1) {
+ yyjson_mut_arr_add_val(modules, module);
+ } else {
+ yyjson_mut_arr_add_strbuf(doc, modules, &type);
+ }
+ } else {
+ yyjson_mut_arr_add_strbuf(doc, modules, &type);
+ }
+}
+
+static void genJsonResult(FFdata* data, FFModuleBaseInfo* baseInfo, void* options) {
+ yyjson_mut_doc* doc = data->resultDoc;
+ yyjson_mut_val* module = yyjson_mut_arr_add_obj(doc, doc->root);
+ yyjson_mut_obj_add_str(doc, module, "type", baseInfo->name);
+ if (baseInfo->generateJsonResult) {
+ baseInfo->generateJsonResult(options, doc, module);
+ } else {
+ yyjson_mut_obj_add_str(doc, module, "error", "Unsupported for JSON format");
+ }
+}
+
+static bool parseStructureCommand(
+ FFdata* data,
+ const char* line,
+ void (*fn)(FFdata*, FFModuleBaseInfo* baseInfo, void* options)) {
+ if (ffCharIsEnglishAlphabet(line[0])) {
+ for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(line[0]) - 'A']; *modules; ++modules) {
+ FFModuleBaseInfo* baseInfo = *modules;
+ if (ffStrEqualsIgnCase(line, baseInfo->name)) {
+ uint8_t optionBuf[FF_OPTION_MAX_SIZE];
+ baseInfo->initOptions(optionBuf);
+ if (data->resultDoc != NULL) {
+ fn(data, baseInfo, optionBuf);
+ } else {
+ baseInfo->printModule(optionBuf);
+ }
+ baseInfo->destroyOptions(optionBuf);
+ return true;
+ }
+ }
+ }
+
+ if (data->resultDoc) {
+ yyjson_mut_doc* doc = data->resultDoc;
+ yyjson_mut_val* module = yyjson_mut_arr_add_obj(doc, doc->root);
+ yyjson_mut_obj_add_str(doc, module, "type", line);
+ yyjson_mut_obj_add_str(doc, module, "error", "Unknown module type");
+ } else {
+ ffPrintError(line, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "<no implementation provided>");
+ }
+ return false;
+}
+
+void ffPrintCommandOption(FFdata* data) {
+ // Parse the structure and call the modules
+ int32_t thres = instance.config.display.stat;
+
+ char* moduleType = NULL;
+ size_t moduleLen = 0;
+ while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) {
+ if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleType, ':')) {
+ continue;
+ }
+
+ double ms = 0;
+ if (thres >= 0) {
+ ms = ffTimeGetTick();
+ }
+
+ parseStructureCommand(data, moduleType, genJsonResult);
+
+ if (thres >= 0) {
+ ms = ffTimeGetTick() - ms;
+
+ if (data->resultDoc) {
+ yyjson_mut_val* moduleJson = yyjson_mut_arr_get_last(data->resultDoc->root);
+ yyjson_mut_obj_add_real(data->resultDoc, moduleJson, "stat", ms);
+ } else {
+ char str[64];
+ int len = snprintf(str, sizeof str, "%.3fms", ms);
+ if (thres > 0) {
+ snprintf(str, sizeof str, "\e[%sm%.3fms\e[m", (ms <= thres ? FF_COLOR_FG_GREEN : ms <= 2 * thres ? FF_COLOR_FG_YELLOW
+ : FF_COLOR_FG_RED),
+ ms);
+ }
+ printf("\e7\e[1A\e[9999999C\e[%dD%s\e8", len - 1, str); // Save; Up 1; Right 9999999; Left <len - 1>; Print <str>; Load
+ }
+ }
+
+#if defined(_WIN32)
+ if (!data->resultDoc && !instance.config.display.noBuffer) {
+ fflush(stdout);
+ }
+#endif
+ }
+}
+
+void ffMigrateCommandOptionToJsonc(FFdata* data) {
+ // If we don't have a custom structure, use the default one
+ if (data->structure.length == 0) {
+ ffStrbufAppendS(&data->structure, FASTFETCH_DATATEXT_STRUCTURE); // Cannot use `ffStrbufSetStatic` here because we will modify the string
+ }
+
+ char* moduleType = NULL;
+ size_t moduleLen = 0;
+ while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) {
+ if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleType, ':')) {
+ continue;
+ }
+
+ parseStructureCommand(data, moduleType, genJsonConfig);
+ }
+}
diff --git a/src/common/impl/dbus.c b/src/common/impl/dbus.c
new file mode 100644
index 0000000..473af2d
--- /dev/null
+++ b/src/common/impl/dbus.c
@@ -0,0 +1,334 @@
+#include "common/dbus.h"
+
+#ifdef FF_HAVE_DBUS
+
+ #include "common/thread.h"
+ #include "common/strutil.h"
+
+static bool loadLibSymbols(FFDBusLibrary* lib) {
+ FF_LIBRARY_LOAD(dbus, false, "libdbus-1" FF_LIBRARY_EXTENSION, 4);
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_bus_get, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_new_method_call, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_append_args, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_init, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_get_arg_type, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_get_basic, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_recurse, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_has_next, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_next, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_unref, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_connection_send_with_reply_and_block, false)
+ FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_connection_unref, false)
+ dbus = NULL; // don't auto dlclose
+ return true;
+}
+
+static const FFDBusLibrary* loadLib(void) {
+ static FFDBusLibrary lib;
+ static bool loaded = false;
+ static bool loadSuccess = false;
+
+ if (!loaded) {
+ loaded = true;
+ loadSuccess = loadLibSymbols(&lib);
+ }
+
+ return loadSuccess ? &lib : NULL;
+}
+
+const char* ffDBusLoadData(DBusBusType busType, FFDBusData* data) {
+ data->lib = loadLib();
+ if (data->lib == NULL) {
+ return "Failed to load DBus library";
+ }
+
+ data->connection = data->lib->ffdbus_bus_get(busType, NULL);
+ if (data->connection == NULL) {
+ return "Failed to connect to DBus";
+ }
+
+ return NULL;
+}
+
+void ffDBusDestroyData(FFDBusData* data) {
+ if (data->connection != NULL) {
+ data->lib->ffdbus_connection_unref(data->connection);
+ data->connection = NULL;
+ }
+}
+
+bool ffDBusGetString(FFDBusData* dbus, DBusMessageIter* iter, FFstrbuf* result) {
+ int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter);
+
+ if (argType == DBUS_TYPE_STRING || argType == DBUS_TYPE_OBJECT_PATH) {
+ const char* value = NULL;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+
+ if (!ffStrSet(value)) {
+ return false;
+ }
+
+ ffStrbufAppendS(result, value);
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_BYTE) {
+ uint8_t value;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ ffStrbufAppendC(result, (char) value);
+ return true;
+ }
+
+ if (argType != DBUS_TYPE_VARIANT && argType != DBUS_TYPE_ARRAY) {
+ return false;
+ }
+
+ DBusMessageIter subIter;
+ dbus->lib->ffdbus_message_iter_recurse(iter, &subIter);
+
+ if (argType == DBUS_TYPE_VARIANT) {
+ return ffDBusGetString(dbus, &subIter, result);
+ }
+
+ // At this point we have an array
+
+ int subArgType = dbus->lib->ffdbus_message_iter_get_arg_type(&subIter);
+ if (subArgType == DBUS_TYPE_INVALID) {
+ return false;
+ }
+
+ if (subArgType == DBUS_TYPE_BYTE) {
+ while (true) {
+ uint8_t value;
+ dbus->lib->ffdbus_message_iter_get_basic(&subIter, &value);
+ ffStrbufAppendC(result, (char) value);
+
+ if (!dbus->lib->ffdbus_message_iter_next(&subIter)) {
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ bool foundAValue = false;
+
+ while (true) {
+ if (ffDBusGetString(dbus, &subIter, result)) {
+ foundAValue = true;
+ ffStrbufAppendS(result, ", ");
+ }
+
+ if (!dbus->lib->ffdbus_message_iter_next(&subIter)) {
+ break;
+ } else {
+ continue;
+ }
+ }
+
+ if (foundAValue) {
+ ffStrbufSubstrBefore(result, result->length - 2);
+ }
+
+ return foundAValue;
+}
+
+bool ffDBusGetBool(FFDBusData* dbus, DBusMessageIter* iter, bool* result) {
+ int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter);
+
+ if (argType == DBUS_TYPE_BOOLEAN) {
+ dbus_bool_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = value != 0;
+ return true;
+ }
+
+ if (argType != DBUS_TYPE_VARIANT) {
+ return false;
+ }
+
+ DBusMessageIter subIter;
+ dbus->lib->ffdbus_message_iter_recurse(iter, &subIter);
+ return ffDBusGetBool(dbus, &subIter, result);
+}
+
+bool ffDBusGetUint(FFDBusData* dbus, DBusMessageIter* iter, uint64_t* result) {
+ int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter);
+
+ if (argType == DBUS_TYPE_BYTE) {
+ uint8_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = value;
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_UINT16) {
+ uint16_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = value;
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_UINT32) {
+ uint32_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = value;
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_UINT64) {
+ dbus->lib->ffdbus_message_iter_get_basic(iter, result);
+ return true;
+ }
+
+ if (argType != DBUS_TYPE_VARIANT) {
+ return false;
+ }
+
+ DBusMessageIter subIter;
+ dbus->lib->ffdbus_message_iter_recurse(iter, &subIter);
+ return ffDBusGetUint(dbus, &subIter, result);
+}
+
+bool ffDBusGetInt(FFDBusData* dbus, DBusMessageIter* iter, int64_t* result) {
+ int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter);
+
+ if (argType == DBUS_TYPE_INT16) {
+ int16_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = value;
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_INT32) {
+ int32_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = value;
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_INT64) {
+ dbus->lib->ffdbus_message_iter_get_basic(iter, result);
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_BYTE) {
+ uint8_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = value;
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_UINT16) {
+ uint16_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = (int32_t) value;
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_UINT32) {
+ uint32_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ *result = (int32_t) value;
+ return true;
+ }
+
+ if (argType == DBUS_TYPE_UINT64) {
+ uint64_t value = 0;
+ dbus->lib->ffdbus_message_iter_get_basic(iter, &value);
+ if (value > INT64_MAX) {
+ return false;
+ }
+ *result = (int64_t) value;
+ return true;
+ }
+
+ if (argType != DBUS_TYPE_VARIANT) {
+ return false;
+ }
+
+ DBusMessageIter subIter;
+ dbus->lib->ffdbus_message_iter_recurse(iter, &subIter);
+ return ffDBusGetInt(dbus, &subIter, result);
+}
+
+DBusMessage* ffDBusGetMethodReply(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* method, const char* arg1, const char* arg2) {
+ DBusMessage* message = dbus->lib->ffdbus_message_new_method_call(busName, objectPath, interface, method);
+ if (message == NULL) {
+ return NULL;
+ }
+
+ if (arg1) {
+ if (arg2) {
+ dbus->lib->ffdbus_message_append_args(message, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_STRING, &arg2, DBUS_TYPE_INVALID);
+ } else {
+ dbus->lib->ffdbus_message_append_args(message, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_INVALID);
+ }
+ }
+
+ DBusMessage* reply = dbus->lib->ffdbus_connection_send_with_reply_and_block(dbus->connection, message, instance.config.general.processingTimeout, NULL);
+
+ dbus->lib->ffdbus_message_unref(message);
+
+ return reply;
+}
+
+DBusMessage* ffDBusGetProperty(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property) {
+ DBusMessage* message = dbus->lib->ffdbus_message_new_method_call(busName, objectPath, "org.freedesktop.DBus.Properties", "Get");
+ if (message == NULL) {
+ return NULL;
+ }
+
+ dbus->lib->ffdbus_message_append_args(message,
+ DBUS_TYPE_STRING,
+ &interface,
+ DBUS_TYPE_STRING,
+ &property,
+ DBUS_TYPE_INVALID);
+
+ DBusMessage* reply = dbus->lib->ffdbus_connection_send_with_reply_and_block(dbus->connection, message, instance.config.general.processingTimeout, NULL);
+
+ dbus->lib->ffdbus_message_unref(message);
+
+ return reply;
+}
+
+bool ffDBusGetPropertyString(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property, FFstrbuf* result) {
+ DBusMessage* reply = ffDBusGetProperty(dbus, busName, objectPath, interface, property);
+ if (reply == NULL) {
+ return false;
+ }
+
+ DBusMessageIter rootIterator;
+ if (!dbus->lib->ffdbus_message_iter_init(reply, &rootIterator)) {
+ dbus->lib->ffdbus_message_unref(reply);
+ return false;
+ }
+
+ bool ret = ffDBusGetString(dbus, &rootIterator, result);
+
+ dbus->lib->ffdbus_message_unref(reply);
+
+ return ret;
+}
+
+bool ffDBusGetPropertyUint(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property, uint64_t* result) {
+ DBusMessage* reply = ffDBusGetProperty(dbus, busName, objectPath, interface, property);
+ if (reply == NULL) {
+ return false;
+ }
+
+ DBusMessageIter rootIterator;
+ if (!dbus->lib->ffdbus_message_iter_init(reply, &rootIterator)) {
+ dbus->lib->ffdbus_message_unref(reply);
+ return false;
+ }
+
+ bool ret = ffDBusGetUint(dbus, &rootIterator, result);
+
+ dbus->lib->ffdbus_message_unref(reply);
+
+ return ret;
+}
+
+#endif // FF_HAVE_DBUS
diff --git a/src/common/impl/debug_windows.c b/src/common/impl/debug_windows.c
new file mode 100644
index 0000000..fb76a65
--- /dev/null
+++ b/src/common/impl/debug_windows.c
@@ -0,0 +1,55 @@
+#include "common/debug.h"
+#include "common/windows/nt.h"
+
+#include <windows.h>
+
+const char* ffDebugWin32Error(DWORD errorCode) {
+ static char buffer[512];
+
+ wchar_t bufferW[256];
+ ULONG len = FormatMessageW(
+ FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL,
+ (DWORD) errorCode,
+ 0,
+ bufferW,
+ ARRAY_SIZE(bufferW),
+ NULL);
+
+ if (len == 0) {
+ snprintf(buffer, sizeof(buffer), "Unknown error code (%lu)", errorCode);
+ } else {
+ // Remove trailing newline
+ while (len > 0 && (bufferW[len - 1] == '\r' || bufferW[len - 1] == '\n')) {
+ --len;
+ }
+
+ if (NT_SUCCESS(RtlUnicodeToUTF8N(buffer, sizeof(buffer), &len, bufferW, len * sizeof(wchar_t)))) {
+ snprintf(buffer + len, sizeof(buffer) - len, " (%lu)", errorCode);
+ } else {
+ snprintf(buffer, sizeof(buffer), "Unknown error (%lu)", errorCode);
+ }
+ }
+
+ return buffer;
+}
+
+const char* ffDebugNtStatus(NTSTATUS status) {
+ return ffDebugWin32Error(RtlNtStatusToDosError(status));
+}
+
+static inline DWORD HRESULTToWin32Error(HRESULT hr) {
+ if (SUCCEEDED(hr)) {
+ return ERROR_SUCCESS;
+ }
+
+ if (HRESULT_FACILITY(hr) == FACILITY_WIN32) {
+ return HRESULT_CODE(hr);
+ }
+
+ return ERROR_INTERNAL_ERROR;
+}
+
+const char* ffDebugHResult(HRESULT hr) {
+ return ffDebugWin32Error(HRESULTToWin32Error(hr));
+}
diff --git a/src/common/impl/duration.c b/src/common/impl/duration.c
new file mode 100644
index 0000000..a6a4965
--- /dev/null
+++ b/src/common/impl/duration.c
@@ -0,0 +1,87 @@
+#include "common/duration.h"
+
+void ffDurationAppendNum(uint64_t totalSeconds, FFstrbuf* result) {
+ const FFOptionsDisplay* options = &instance.config.display;
+
+ bool spaceBeforeUnit = options->durationSpaceBeforeUnit != FF_SPACE_BEFORE_UNIT_NEVER;
+
+ if (totalSeconds < 60) {
+ ffStrbufAppendUInt(result, totalSeconds);
+ if (spaceBeforeUnit) {
+ ffStrbufAppendC(result, ' ');
+ }
+ ffStrbufAppendS(result, options->durationAbbreviation ? "sec" : "second");
+ if (totalSeconds != 1) {
+ ffStrbufAppendC(result, 's');
+ }
+ return;
+ }
+
+ uint32_t seconds = (uint32_t) (totalSeconds % 60);
+ totalSeconds /= 60;
+ if (seconds >= 30) {
+ totalSeconds++;
+ }
+
+ uint32_t minutes = (uint32_t) (totalSeconds % 60);
+ totalSeconds /= 60;
+ uint32_t hours = (uint32_t) (totalSeconds % 24);
+ totalSeconds /= 24;
+ uint32_t days = (uint32_t) totalSeconds;
+
+ if (days > 0) {
+ ffStrbufAppendUInt(result, days);
+ if (spaceBeforeUnit) {
+ ffStrbufAppendC(result, ' ');
+ }
+ if (options->durationAbbreviation) {
+ ffStrbufAppendC(result, 'd');
+
+ if (hours > 0 || minutes > 0) {
+ ffStrbufAppendC(result, ' ');
+ }
+ } else {
+ ffStrbufAppendS(result, days == 1 ? "day" : "days");
+
+ if (days >= 100) {
+ ffStrbufAppendS(result, "(!)");
+ }
+
+ if (hours > 0 || minutes > 0) {
+ ffStrbufAppendS(result, ", ");
+ }
+ }
+ }
+
+ if (hours > 0) {
+ ffStrbufAppendUInt(result, hours);
+ if (spaceBeforeUnit) {
+ ffStrbufAppendC(result, ' ');
+ }
+ if (options->durationAbbreviation) {
+ ffStrbufAppendC(result, 'h');
+
+ if (minutes > 0) {
+ ffStrbufAppendC(result, ' ');
+ }
+ } else {
+ ffStrbufAppendS(result, hours == 1 ? "hour" : "hours");
+
+ if (minutes > 0) {
+ ffStrbufAppendS(result, ", ");
+ }
+ }
+ }
+
+ if (minutes > 0) {
+ ffStrbufAppendUInt(result, minutes);
+ if (spaceBeforeUnit) {
+ ffStrbufAppendC(result, ' ');
+ }
+ if (options->durationAbbreviation) {
+ ffStrbufAppendC(result, 'm');
+ } else {
+ ffStrbufAppendS(result, minutes == 1 ? "min" : "mins");
+ }
+ }
+}
diff --git a/src/common/impl/edidHelper.c b/src/common/impl/edidHelper.c
new file mode 100644
index 0000000..1e3310e
--- /dev/null
+++ b/src/common/impl/edidHelper.c
@@ -0,0 +1,132 @@
+#include "common/edidHelper.h"
+
+void ffEdidGetPhysicalResolution(const uint8_t edid[128], uint32_t* width, uint32_t* height) {
+ const int dtd = 54;
+ *width = (((uint32_t) edid[dtd + 4] >> 4) << 8) | edid[dtd + 2];
+ *height = (((uint32_t) edid[dtd + 7] >> 4) << 8) | edid[dtd + 5];
+}
+
+void ffEdidGetPreferredResolutionAndRefreshRate(const uint8_t edid[128], uint32_t* width, uint32_t* height, double* refreshRate) {
+ for (uint32_t i = 0x36; i < 0x7E; i += 0x12) { // read through descriptor blocks...
+ if (edid[i] != 0x00 && edid[i + 1] != 0x00) { // a dtd
+ uint32_t hactive = edid[i + 2] + (uint32_t) ((edid[i + 4] & 0xf0) << 4);
+ uint32_t hblank = edid[i + 3] + (uint32_t) ((edid[i + 4] & 0x0f) << 8);
+ uint32_t vactive = edid[i + 5] + (uint32_t) ((edid[i + 7] & 0xf0) << 4);
+ uint32_t vblank = edid[i + 6] + (uint32_t) ((edid[i + 7] & 0x0f) << 8);
+ uint32_t pixclk = ((uint32_t) edid[i + 1] << 8) | (edid[i]);
+ *width = hactive;
+ *height = vactive;
+ *refreshRate = (double) pixclk * 10000 / (double) (hactive + hblank) / (double) (vactive + vblank);
+ return;
+ }
+ }
+}
+
+void ffEdidGetVendorAndModel(const uint8_t edid[128], FFstrbuf* result) {
+ // https://github.com/jinksong/read_edid/blob/master/parse-edid/parse-edid.c
+ ffStrbufAppendF(result, "%c%c%c%04X", (char) (((uint32_t) edid[8] >> 2 & 0x1f) + 'A' - 1), (char) (((((uint32_t) edid[8] & 0x3) << 3) | (((uint32_t) edid[9] & 0xe0) >> 5)) + 'A' - 1), (char) (((uint32_t) edid[9] & 0x1f) + 'A' - 1), (uint32_t) (edid[10] + (uint32_t) (edid[11] << 8)));
+}
+
+bool ffEdidGetName(const uint8_t edid[128], FFstrbuf* name) {
+ // https://github.com/jinksong/read_edid/blob/master/parse-edid/parse-edid.c
+ for (uint32_t i = 0x36; i < 0x7E; i += 0x12) { // read through descriptor blocks...
+ if (edid[i] == 0x00) { // not a timing descriptor
+ if (edid[i + 3] == 0xfc) { // Model Name tag
+ for (uint32_t j = 0; j < 13; j++) {
+ if (edid[i + 5 + j] == 0x0a) {
+ ffStrbufAppendNS(name, j, (const char*) &edid[i + 5]);
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ // use manufacturer + model number as monitor name
+ ffEdidGetVendorAndModel(edid, name);
+ return false;
+}
+
+void ffEdidGetPhysicalSize(const uint8_t edid[128], uint32_t* width, uint32_t* height) {
+ // Detailed Timing Descriptors
+ uint32_t dw = (((uint32_t) edid[68] & 0xF0) << 4) + edid[66];
+ uint32_t dh = (((uint32_t) edid[68] & 0x0F) << 8) + edid[67];
+
+ // Basic Display Parameters
+ uint32_t bw = edid[21] * 10;
+ uint32_t bh = edid[22] * 10;
+
+ // Some monitors report invalid data in DTD. See #1406
+ if (abs((int) dw - (int) bw) < 10 && abs((int) dh - (int) bh) < 10) {
+ *width = dw;
+ *height = dh;
+ } else {
+ *width = bw;
+ *height = bh;
+ }
+}
+
+void ffEdidGetSerialAndManufactureDate(const uint8_t edid[128], uint32_t* serial, uint16_t* year, uint16_t* week) {
+ if (edid[17] > 0 && edid[17] < 0xFF) {
+ *year = (uint16_t) (edid[17] + 1990);
+ *week = (uint16_t) edid[16];
+ if (*week == 0xFF) {
+ *week = 0;
+ }
+ } else {
+ *year = *week = 0;
+ }
+
+ *serial = *(uint32_t*) &edid[12];
+}
+
+bool ffEdidGetHdrCompatible(const uint8_t* edid, uint32_t length) {
+ if (length <= 128) {
+ return false;
+ }
+ for (const uint8_t* cta = &edid[128]; cta < &edid[length]; cta += 128) {
+ // https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#CTA_EDID_Timing_Extension_Block
+ if (cta[0] != 0x02 /* CTA EDID */) {
+ continue;
+ }
+ if (cta[1] < 0x03 /* Version 3 */) {
+ continue;
+ }
+ const uint8_t offset = cta[2];
+ if (offset <= 4) {
+ continue;
+ }
+ for (uint8_t i = 4; i < offset;) {
+ uint8_t blkLen = cta[i] & 0x1f;
+ if (blkLen > 0) {
+ uint8_t blkTag = (cta[i] & 0xe0) >> 5;
+ if (blkTag == 0x07 /* Extended Block Type Tag */) {
+ uint8_t extendedTag = cta[i + 1];
+ if (extendedTag == 6 /* HDR SMDB */ || extendedTag == 7 /* HDR DMDB */) {
+ return true;
+ }
+ }
+ }
+ i += (uint8_t) (blkLen + 1);
+ }
+ }
+ return false;
+}
+
+bool ffEdidIsValid(const uint8_t edid[128], uint32_t length) {
+ if (length < 128 || length % 128 != 0) {
+ return false;
+ }
+
+ static const uint8_t edidHeader[] = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 };
+ if (memcmp(edid, edidHeader, sizeof(edidHeader)) != 0) {
+ return false;
+ }
+
+ uint8_t sum = 0;
+ for (uint32_t i = 0; i < 128; i++) {
+ sum += edid[i];
+ }
+
+ return sum == 0;
+}
diff --git a/src/common/impl/font.c b/src/common/impl/font.c
new file mode 100644
index 0000000..93feff1
--- /dev/null
+++ b/src/common/impl/font.c
@@ -0,0 +1,513 @@
+#include "fastfetch.h"
+#include "common/FFlist.h"
+#include "common/FFstrbuf.h"
+#include "common/strutil.h"
+#include "common/font.h"
+
+#include <string.h>
+#include <ctype.h>
+
+void ffFontInit(FFfont* font) {
+ // Ensure no memory allocates
+ ffStrbufInit(&font->pretty);
+ ffStrbufInit(&font->name);
+ ffStrbufInit(&font->size);
+ ffListInit(&font->styles);
+}
+
+static void strbufAppendNSExcludingC(FFstrbuf* strbuf, uint32_t length, const char* value, char exclude) {
+ if (value == NULL || length == 0) {
+ return;
+ }
+
+ ffStrbufEnsureFree(strbuf, length);
+
+ for (uint32_t i = 0; i < length; i++) {
+ if (value[i] != exclude) {
+ strbuf->chars[strbuf->length++] = value[i];
+ }
+ }
+
+ strbuf->chars[strbuf->length] = '\0';
+}
+
+static void fontInitPretty(FFfont* font) {
+ ffStrbufAppend(&font->pretty, &font->name);
+
+ if (font->size.length == 0 && font->styles.length == 0) {
+ return;
+ } else if (font->pretty.length == 0) {
+ ffStrbufAppendS(&font->pretty, "default");
+ }
+
+ ffStrbufAppendS(&font->pretty, " (");
+
+ if (font->size.length > 0) {
+ ffStrbufAppend(&font->pretty, &font->size);
+ if (!ffStrbufEndsWithS(&font->size, "pt") && !ffStrbufEndsWithS(&font->size, "px")) {
+ ffStrbufAppendS(&font->pretty, "pt");
+ }
+
+ if (font->styles.length > 0) {
+ ffStrbufAppendS(&font->pretty, ", ");
+ }
+ }
+
+ for (uint32_t i = 0; i < font->styles.length; i++) {
+ ffStrbufAppend(&font->pretty, FF_LIST_GET(FFstrbuf, font->styles, i));
+
+ if (i < font->styles.length - 1) {
+ ffStrbufAppendS(&font->pretty, ", ");
+ }
+ }
+
+ ffStrbufAppendC(&font->pretty, ')');
+}
+
+void ffFontInitQt(FFfont* font, const char* data) {
+ ffFontInit(font);
+
+ // See https://doc.qt.io/qt-5/qfont.html#toString
+
+ // Family
+ data = ffStrbufAppendSUntilC(&font->name, data, ',');
+ ffStrbufTrim(&font->name, ' ');
+ if (!data) {
+ goto exit;
+ }
+ data++;
+
+ // Size
+ data = ffStrbufAppendSUntilC(&font->size, data, ',');
+ ffStrbufTrim(&font->size, ' ');
+ if (!data) {
+ goto exit;
+ }
+ data++;
+
+ // Style
+ data = strrchr(data, ',');
+ if (!data) {
+ goto exit;
+ }
+ data++;
+ if (isalpha(*data)) {
+ do {
+ FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles);
+ ffStrbufInit(style);
+ data = ffStrbufAppendSUntilC(style, data, ' ');
+ if (data) {
+ data++;
+ }
+ } while (data);
+ }
+
+exit:
+ fontInitPretty(font);
+}
+
+static void fontPangoParseWord(const char** data, FFfont* font, FFstrbuf* alternativeBuffer) {
+ while (**data == ' ' || **data == '\t' || **data == ',') {
+ ++(*data);
+ }
+
+ const char* wordStart = *data;
+
+ while (**data != ' ' && **data != '\t' && **data != ',' && **data != '\0' && **data != '`' && **data != '\\') {
+ ++(*data);
+ }
+
+ uint32_t wordLength = (uint32_t) (*data - wordStart);
+ if (wordLength == 0) {
+ return;
+ }
+
+ if (**data == '\0' || **data == '`' || **data == '\\') {
+ ffStrbufAppendNS(&font->size, wordLength, wordStart);
+ if (ffStrbufEndsWithS(&font->size, "px")) {
+ ffStrbufSubstrBefore(&font->size, font->size.length - 2);
+ }
+
+ double dummy;
+ if (sscanf(font->size.chars, "%lf", &dummy) == 1) {
+ return;
+ }
+
+ ffStrbufClear(&font->size);
+ }
+
+ if (
+ ffStrStartsWithIgnCase(wordStart, "Ultra") ||
+ ffStrStartsWithIgnCase(wordStart, "Extra") ||
+ ffStrStartsWithIgnCase(wordStart, "Semi") ||
+ ffStrStartsWithIgnCase(wordStart, "Demi") ||
+ ffStrStartsWithIgnCase(wordStart, "Normal") ||
+ ffStrStartsWithIgnCase(wordStart, "Roman") ||
+ ffStrStartsWithIgnCase(wordStart, "Oblique") ||
+ ffStrStartsWithIgnCase(wordStart, "Italic") ||
+ ffStrStartsWithIgnCase(wordStart, "Thin") ||
+ ffStrStartsWithIgnCase(wordStart, "Light") ||
+ ffStrStartsWithIgnCase(wordStart, "Bold") ||
+ ffStrStartsWithIgnCase(wordStart, "Black") ||
+ ffStrStartsWithIgnCase(wordStart, "Condensed") ||
+ ffStrStartsWithIgnCase(wordStart, "Expanded")) {
+ if (alternativeBuffer == NULL) {
+ alternativeBuffer = FF_LIST_ADD(FFstrbuf, font->styles);
+ ffStrbufInit(alternativeBuffer);
+ }
+
+ strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-');
+
+ if (
+ ffStrStartsWithIgnCase(wordStart, "Ultra ") ||
+ ffStrStartsWithIgnCase(wordStart, "Extra ") ||
+ ffStrStartsWithIgnCase(wordStart, "Semi ") ||
+ ffStrStartsWithIgnCase(wordStart, "Demi ")) {
+ fontPangoParseWord(data, font, alternativeBuffer);
+ }
+
+ return;
+ }
+
+ if (alternativeBuffer != NULL) {
+ strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-');
+ return;
+ }
+
+ if (font->name.length > 0) {
+ ffStrbufAppendC(&font->name, ' ');
+ }
+ ffStrbufAppendNS(&font->name, wordLength, wordStart);
+}
+
+void ffFontInitPango(FFfont* font, const char* data) {
+ ffFontInit(font);
+
+ while (*data != '\0' && *data != '`' && *data != '\\') {
+ fontPangoParseWord(&data, font, NULL);
+ }
+
+ fontInitPretty(font);
+}
+
+void ffFontInitValues(FFfont* font, const char* name, const char* size) {
+ ffFontInit(font);
+
+ ffStrbufAppendS(&font->name, name);
+ ffStrbufTrim(&font->name, '"');
+ ffStrbufAppendS(&font->size, size);
+
+ fontInitPretty(font);
+}
+
+void ffFontInitXlfd(FFfont* font, const char* xlfd) {
+ assert(xlfd && *xlfd);
+
+ // https://en.wikipedia.org/wiki/X_logical_font_description
+ ffFontInit(font);
+
+ // XLFD: -foundry-family-weight-slant-setwidth-addstyle-pixelsize-pointsize-xres-yres-spacing-averagewidth-charsetregistry-charsetencoding
+ // It often starts with '-', which would create an empty first field. Skip it to align indexes.
+ if (*xlfd == '-') {
+ xlfd++;
+ }
+
+ const char* pstart = xlfd;
+
+ for (int field = 0; field < 14; field++) {
+ const char* pend = strchr(pstart, '-');
+ uint32_t length = pend ? (uint32_t) (pend - pstart) : (uint32_t) strlen(pstart);
+
+ if (length > 0) {
+ if (field == 1) // family
+ {
+ ffStrbufAppendNS(&font->name, length, pstart);
+ } else if (field == 7) // pointsize (decipoints, preferred)
+ {
+ // parse positive integer from substring
+ long deciPt = 0;
+ bool ok = true;
+ for (uint32_t i = 0; i < length; i++) {
+ char c = pstart[i];
+ if (c < '0' || c > '9') {
+ ok = false;
+ break;
+ }
+ deciPt = deciPt * 10 + (c - '0');
+ }
+
+ if (ok && deciPt > 0) {
+ ffStrbufClear(&font->size);
+
+ char tmp[32];
+ if (deciPt % 10 == 0) {
+ snprintf(tmp, sizeof(tmp), "%ldpt", deciPt / 10);
+ } else {
+ snprintf(tmp, sizeof(tmp), "%ld.%ldpt", deciPt / 10, deciPt % 10);
+ }
+
+ ffStrbufAppendS(&font->size, tmp);
+ }
+ } else if (field == 6) // pixelsize (fallback if pointsize missing/invalid)
+ {
+ if (font->size.length == 0) {
+ long px = 0;
+ bool ok = true;
+ for (uint32_t i = 0; i < length; i++) {
+ char c = pstart[i];
+ if (c < '0' || c > '9') {
+ ok = false;
+ break;
+ }
+ px = px * 10 + (c - '0');
+ }
+
+ if (ok && px > 0) {
+ ffStrbufAppendNS(&font->size, length, pstart);
+ ffStrbufAppendS(&font->size, "px");
+ }
+ }
+ } else if (field >= 2 && field <= 5) // weight/slant/setwidth/addstyle
+ {
+ // ignore "normal" (case-insensitive)
+ if (!(length == 6 && ffStrStartsWithIgnCase(pstart, "normal"))) {
+ FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles);
+ ffStrbufInitNS(style, length, pstart);
+ }
+ }
+ }
+
+ if (!pend) {
+ break;
+ }
+
+ pstart = pend + 1;
+ }
+
+ fontInitPretty(font);
+}
+
+void ffFontInitXft(FFfont* font, const char* xft) {
+ assert(xft);
+
+ // https://en.wikipedia.org/wiki/Xft
+ // Xft/Fontconfig pattern examples:
+ // "DejaVu Sans Mono-10"
+ // "monospace:size=10:weight=bold:slant=italic"
+ // "Fira Code-12:style=Regular"
+ // Goal: extract family(name), size, and some common styles.
+
+ ffFontInit(font);
+
+ // 1) Parse "head" part before first ':' => usually "family[-size]" (may include commas)
+ const char* p = xft;
+
+ while (*p == ' ' || *p == '\t') {
+ ++p;
+ }
+
+ const char* headStart = p;
+ while (*p != '\0' && *p != ':') {
+ ++p;
+ }
+ const char* headEnd = p;
+
+ // trim tail spaces
+ while (headEnd > headStart && (headEnd[-1] == ' ' || headEnd[-1] == '\t')) {
+ --headEnd;
+ }
+
+ // If multiple families are listed, take the first one (up to comma)
+ for (const char* q = headStart; q < headEnd; ++q) {
+ if (*q == ',') {
+ headEnd = q;
+ while (headEnd > headStart && (headEnd[-1] == ' ' || headEnd[-1] == '\t')) {
+ --headEnd;
+ }
+ break;
+ }
+ }
+
+ // Try parse trailing "-<number>" as size, otherwise entire head is name
+ const char* dashPos = NULL;
+ const char* sizeStart = NULL;
+
+ for (const char* q = headEnd; q > headStart;) {
+ --q;
+ if (*q == '-' && (q + 1) < headEnd && ffCharIsDigit(q[1])) {
+ dashPos = q;
+ sizeStart = q + 1;
+ break;
+ }
+ }
+
+ if (dashPos) {
+ bool ok = true;
+ bool seenDigit = false;
+ for (const char* q = sizeStart; q < headEnd; ++q) {
+ if (ffCharIsDigit(*q)) {
+ seenDigit = true;
+ } else if (*q == '.') {
+ continue;
+ } else {
+ ok = false;
+ break;
+ }
+ }
+
+ if (ok && seenDigit) {
+ const char* nameEnd = dashPos;
+ while (nameEnd > headStart && (nameEnd[-1] == ' ' || nameEnd[-1] == '\t')) {
+ --nameEnd;
+ }
+
+ if (nameEnd > headStart) {
+ ffStrbufAppendNS(&font->name, (uint32_t) (nameEnd - headStart), headStart);
+ }
+
+ if (headEnd > sizeStart) {
+ ffStrbufAppendNS(&font->size, (uint32_t) (headEnd - sizeStart), sizeStart);
+ }
+ } else {
+ if (headEnd > headStart) {
+ ffStrbufAppendNS(&font->name, (uint32_t) (headEnd - headStart), headStart);
+ }
+ }
+ } else {
+ if (headEnd > headStart) {
+ ffStrbufAppendNS(&font->name, (uint32_t) (headEnd - headStart), headStart);
+ }
+ }
+
+ ffStrbufTrim(&font->name, ' ');
+ ffStrbufTrim(&font->name, '"');
+
+ // 2) Parse key=value fields after ':' (Fontconfig-like). Fields separated by ':'.
+ // Common keys: size, pixelsize, pointsize, style, weight, slant, width
+ while (*p == ':') {
+ ++p;
+
+ // key
+ const char* keyStart = p;
+ while (*p != '\0' && *p != '=' && *p != ':') {
+ ++p;
+ }
+ const char* keyEnd = p;
+
+ if (*p != '=') {
+ continue; // skip tokens without '='
+ }
+
+ ++p; // skip '='
+
+ // value (until next ':', allow backslash-escaping)
+ FF_STRBUF_AUTO_DESTROY value = ffStrbufCreate();
+
+ while (*p != '\0' && *p != ':') {
+ if (*p == '\\' && p[1] != '\0') {
+ ++p;
+ ffStrbufAppendC(&value, *p);
+ ++p;
+ continue;
+ }
+
+ ffStrbufAppendC(&value, *p);
+ ++p;
+ }
+
+ ffStrbufTrim(&value, ' ');
+ ffStrbufTrim(&value, '"');
+
+ uint32_t keyLen = (uint32_t) (keyEnd - keyStart);
+
+ // helper: set numeric size if not set yet
+ const bool sizeEmpty = (font->size.length == 0);
+ if (value.length > 0) {
+ if (
+ (keyLen == 4 && ffStrStartsWithIgnCase(keyStart, "size")) ||
+ (keyLen == 9 && (ffStrStartsWithIgnCase(keyStart, "pixelsize") || ffStrStartsWithIgnCase(keyStart, "pointsize")))) {
+ if (sizeEmpty && ffCharIsDigit(value.chars[0])) {
+ ffStrbufAppend(&font->size, &value);
+ ffStrbufAppendS(&font->size,
+ (keyLen == 9 && ffStrStartsWithIgnCase(keyStart, "pixelsize")) ? "px" : "pt");
+ }
+ } else if (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "style")) {
+ // style may contain multiple words: "Bold Italic"
+ const char* s = value.chars;
+ while (*s != '\0') {
+ while (*s == ' ' || *s == '\t' || *s == ',') {
+ ++s;
+ }
+
+ const char* w = s;
+ while (*s != '\0' && *s != ' ' && *s != '\t' && *s != ',') {
+ ++s;
+ }
+
+ if (s > w) {
+ FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles);
+ ffStrbufInitNS(style, (uint32_t) (s - w), w);
+ }
+ }
+ } else if (
+ (keyLen == 6 && ffStrStartsWithIgnCase(keyStart, "weight")) ||
+ (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "slant")) ||
+ (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "width"))) {
+ // normalize: remove '-' to align with other parsers ("Semi-Bold" -> "SemiBold")
+ FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles);
+ ffStrbufInit(style);
+ strbufAppendNSExcludingC(style, value.length, value.chars, '-');
+ ffStrbufTrim(style, ' ');
+ if (style->length == 0) {
+ ffStrbufDestroy(style);
+ --font->styles.length;
+ }
+ }
+ }
+ }
+
+ fontInitPretty(font);
+}
+
+void ffFontInitMoveValues(FFfont* font, FFstrbuf* name, FFstrbuf* size, FFstrbuf* style) {
+ ffFontInit(font);
+
+ if (name) {
+ ffStrbufInitMove(&font->name, name);
+ }
+ if (size) {
+ ffStrbufInitMove(&font->size, size);
+ }
+ if (style) {
+ FFstrbuf* styleBuf = FF_LIST_ADD(FFstrbuf, font->styles);
+ ffStrbufInitMove(styleBuf, style);
+ }
+
+ fontInitPretty(font);
+}
+
+void ffFontInitWithSpace(FFfont* font, const char* rawName) {
+ const char* pspace = strrchr(rawName, ' ');
+ if (pspace == NULL) {
+ ffFontInitCopy(font, rawName);
+ return;
+ }
+
+ ffFontInit(font);
+
+ ffStrbufAppendNS(&font->name, (uint32_t) (pspace - rawName), rawName);
+ ffStrbufAppendS(&font->size, pspace + 1);
+
+ fontInitPretty(font);
+}
+
+void ffFontDestroy(FFfont* font) {
+ ffStrbufDestroy(&font->pretty);
+ ffStrbufDestroy(&font->name);
+ ffStrbufDestroy(&font->size);
+
+ FF_LIST_FOR_EACH (FFstrbuf, str, font->styles) {
+ ffStrbufDestroy(str);
+ }
+ ffListDestroy(&font->styles);
+}
diff --git a/src/common/impl/format.c b/src/common/impl/format.c
new file mode 100644
index 0000000..db414bc
--- /dev/null
+++ b/src/common/impl/format.c
@@ -0,0 +1,761 @@
+#include "fastfetch.h"
+#include "common/format.h"
+#include "common/parsing.h"
+#include "common/textModifier.h"
+#include "common/strutil.h"
+#include "common/library.h"
+
+#include <inttypes.h>
+
+void ffFormatAppendFormatArg(FFstrbuf* buffer, const FFformatarg* formatarg) {
+ switch (formatarg->type) {
+ case FF_ARG_TYPE_INT:
+ ffStrbufAppendSInt(buffer, *(int32_t*) formatarg->value);
+ break;
+ case FF_ARG_TYPE_UINT:
+ ffStrbufAppendUInt(buffer, *(uint32_t*) formatarg->value);
+ break;
+ case FF_ARG_TYPE_UINT64:
+ ffStrbufAppendUInt(buffer, *(uint64_t*) formatarg->value);
+ break;
+ case FF_ARG_TYPE_UINT16:
+ ffStrbufAppendUInt(buffer, *(uint16_t*) formatarg->value);
+ break;
+ case FF_ARG_TYPE_UINT8:
+ ffStrbufAppendUInt(buffer, *(uint8_t*) formatarg->value);
+ break;
+ case FF_ARG_TYPE_STRING:
+ ffStrbufAppendS(buffer, (const char*) formatarg->value);
+ break;
+ case FF_ARG_TYPE_STRBUF:
+ ffStrbufAppend(buffer, (const FFstrbuf*) formatarg->value);
+ break;
+ case FF_ARG_TYPE_FLOAT:
+ ffStrbufAppendDouble(buffer, *(float*) formatarg->value, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros != FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER);
+ break;
+ case FF_ARG_TYPE_DOUBLE:
+ ffStrbufAppendDouble(buffer, *(double*) formatarg->value, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros != FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER);
+ break;
+ case FF_ARG_TYPE_BOOL:
+ ffStrbufAppendS(buffer, *(bool*) formatarg->value ? "true" : "false");
+ break;
+ case FF_ARG_TYPE_LIST: {
+ const FFlist* list = (const FFlist*) formatarg->value;
+ for (uint32_t i = 0; i < list->length; i++) {
+ ffStrbufAppend(buffer, FF_LIST_GET(FFstrbuf, *list, i));
+ if (i < list->length - 1) {
+ ffStrbufAppendS(buffer, ", ");
+ }
+ }
+ break;
+ }
+ case FF_ARG_TYPE_BUFFER: {
+ // Placeholder for binary data, just print the size for now
+ const FFArgBuffer* argBuffer = (const FFArgBuffer*) formatarg->value;
+ ffStrbufAppendF(buffer, "buffer(%u bytes)", argBuffer->length);
+ break;
+ }
+ default:
+ if (formatarg->type != FF_ARG_TYPE_NULL) {
+ fprintf(stderr, "Error: format string \"%s\": argument is not implemented: %i\n", buffer->chars, formatarg->type);
+ }
+ break;
+ }
+}
+
+/**
+ * @brief parses a string to a uint32_t
+ *
+ * If the string can't be parsed, or is < 1, uint32_t max is returned.
+ *
+ * @param placeholderValue the string to parse
+ * @return uint32_t the parsed value
+ */
+static uint32_t getArgumentIndex(const char* placeholderValue, uint32_t numArgs, const FFformatarg* arguments) {
+ char firstChar = placeholderValue[0];
+ if (firstChar == '\0') {
+ return 0; // use arg counter
+ }
+
+ if (firstChar >= '0' && firstChar <= '9') {
+ char* pEnd = NULL;
+ uint32_t result = (uint32_t) strtoul(placeholderValue, &pEnd, 10);
+ if (result > numArgs) {
+ return UINT32_MAX;
+ }
+ if (*pEnd != '\0') {
+ return UINT32_MAX;
+ }
+ return result;
+ } else if (ffCharIsEnglishAlphabet(firstChar)) {
+ for (uint32_t i = 0; i < numArgs; ++i) {
+ const FFformatarg* arg = &arguments[i];
+ if (arg->name && ffStrEqualsIgnCase(placeholderValue, arg->name)) {
+ return i + 1;
+ }
+ }
+ }
+
+ return UINT32_MAX;
+}
+
+static inline void appendInvalidPlaceholder(FFstrbuf* buffer, const char* start, const FFstrbuf* placeholderValue, uint32_t index, uint32_t formatStringLength) {
+ ffStrbufAppendS(buffer, start);
+ ffStrbufAppend(buffer, placeholderValue);
+
+ if (index < formatStringLength) {
+ ffStrbufAppendC(buffer, '}');
+ }
+}
+
+static inline bool formatArgSet(const FFformatarg* arg) {
+ return arg->value != NULL && ((arg->type == FF_ARG_TYPE_DOUBLE && *(double*) arg->value > 0.0) || (arg->type == FF_ARG_TYPE_FLOAT && *(float*) arg->value > 0.0) || (arg->type == FF_ARG_TYPE_INT && *(int32_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_STRBUF && ((FFstrbuf*) arg->value)->length > 0) || (arg->type == FF_ARG_TYPE_STRING && ffStrSet((char*) arg->value)) || (arg->type == FF_ARG_TYPE_UINT8 && *(uint8_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT16 && *(uint16_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT && *(uint32_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT64 && *(uint64_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_BOOL && *(bool*) arg->value) || (arg->type == FF_ARG_TYPE_LIST && ((FFlist*) arg->value)->length > 0));
+}
+
+FF_A_UNUSED static inline void normalizeArgName(FFstrbuf* dst, const char* src) {
+ ffStrbufClear(dst);
+ bool flag = false;
+ for (const char* p = src; *p; ++p) {
+ if (*p == '-') {
+ flag = true;
+ } else if (flag) {
+ ffStrbufAppendC(dst, (char) toupper((unsigned char) *p));
+ flag = false;
+ } else {
+ ffStrbufAppendC(dst, *p);
+ }
+ }
+}
+
+#if FF_HAVE_LUA
+ #include "common/lua.h"
+
+static void appendLuaError(FFstrbuf* buffer, const char* prefix, lua_State* L) {
+ const char* err = lua_tolstring(L, -1, NULL);
+ if (err) {
+ const char* tmp = strchr(err, ':');
+ if (tmp) {
+ err = tmp + 1;
+ while (*err == ' ') {
+ ++err;
+ }
+ }
+ }
+ ffStrbufAppendF(buffer, "%s: %s", prefix, err ? err : "unknown");
+}
+
+static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t scriptLen, uint32_t numArgs, const FFformatarg* arguments) {
+ const char* err = ffLuaLoadState();
+ if (err) {
+ ffStrbufAppendF(buffer, "Lua init error: %s", err);
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY argNameBuf = ffStrbufCreate();
+ bool ret = false;
+
+ lua_State* L = luaData.L;
+ // Clear stack and load chunk
+ lua_settop(L, 0);
+ if (luaL_loadbuffer(L, script, scriptLen, "") != LUA_OK) {
+ appendLuaError(buffer, "Lua load error", L);
+ } else {
+ // Build args table for name lookup only.
+ lua_createtable(L, 0, 0);
+
+ for (uint32_t i = 0; i < numArgs; ++i) {
+ const FFformatarg* arg = &arguments[i];
+ switch (arg->type) {
+ case FF_ARG_TYPE_INT:
+ lua_pushinteger(L, (lua_Integer) * (int32_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_UINT:
+ lua_pushinteger(L, (lua_Integer) * (uint32_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_UINT64:
+ lua_pushinteger(L, (lua_Integer) * (uint64_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_UINT16:
+ lua_pushinteger(L, (lua_Integer) * (uint16_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_UINT8:
+ lua_pushinteger(L, (lua_Integer) * (uint8_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_FLOAT:
+ lua_pushnumber(L, (lua_Number) * (float*) arg->value);
+ break;
+ case FF_ARG_TYPE_DOUBLE:
+ lua_pushnumber(L, (lua_Number) * (double*) arg->value);
+ break;
+ case FF_ARG_TYPE_BOOL:
+ lua_pushboolean(L, *(bool*) arg->value);
+ break;
+ case FF_ARG_TYPE_STRING:
+ lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value));
+ break;
+ case FF_ARG_TYPE_STRBUF: {
+ const FFstrbuf* sb = (const FFstrbuf*) arg->value;
+ lua_pushlstring(L, sb->chars, sb->length);
+ break;
+ }
+ case FF_ARG_TYPE_LIST: {
+ const FFlist* list = (const FFlist*) arg->value;
+ lua_createtable(L, 0, 0);
+ for (uint32_t li = 0; li < list->length; ++li) {
+ const FFstrbuf* item = FF_LIST_GET(FFstrbuf, *list, li);
+ lua_pushlstring(L, item->chars, item->length);
+ lua_seti(L, -2, (lua_Integer) (li + 1));
+ }
+ break;
+ }
+ default:
+ lua_pushnil(L);
+ break;
+ }
+ if (arg->name && arg->name[0]) {
+ normalizeArgName(&argNameBuf, arg->name);
+ } else {
+ ffStrbufSetF(&argNameBuf, "arg%" PRIu32, i + 1);
+ }
+ lua_setfield(L, -2, argNameBuf.chars);
+ }
+
+ if (lua_pcall(L, 1, LUA_MULTRET, 0) != LUA_OK) {
+ appendLuaError(buffer, "Lua runtime error", L);
+ } else {
+ int nresults = lua_gettop(L);
+ if (nresults == 0) {
+ ffStrbufAppendS(buffer, "Lua result error: no result");
+ } else {
+ // Convert first result to string
+ const char* res = lua_tolstring(L, 1, NULL);
+ if (res) {
+ ffStrbufAppendS(buffer, res);
+ } else {
+ luaL_tolstring(L, 1, NULL);
+ const char* sval = lua_tolstring(L, -1, NULL);
+ if (sval) {
+ ffStrbufAppendS(buffer, sval);
+ }
+ }
+ ret = true;
+ }
+ }
+ }
+ lua_settop(L, 0);
+ return ret;
+}
+#endif
+
+#if FF_HAVE_QUICKJS
+ #include <quickjs.h>
+
+struct FFQuickJSData {
+ FF_LIBRARY_SYMBOL(JS_NewRuntime)
+ FF_LIBRARY_SYMBOL(JS_NewContext)
+ FF_LIBRARY_SYMBOL(JS_FreeRuntime)
+ FF_LIBRARY_SYMBOL(JS_EvalThis)
+ FF_LIBRARY_SYMBOL(JS_GetException)
+ FF_LIBRARY_SYMBOL(JS_ToCStringLen2)
+ FF_LIBRARY_SYMBOL(JS_FreeCString)
+ FF_LIBRARY_SYMBOL(JS_NewStringLen)
+ FF_LIBRARY_SYMBOL(JS_NewArray)
+ FF_LIBRARY_SYMBOL(JS_NewBigUint64)
+ FF_LIBRARY_SYMBOL(JS_SetPropertyUint32)
+ FF_LIBRARY_SYMBOL(JS_NewObject)
+ FF_LIBRARY_SYMBOL(JS_SetPropertyStr)
+ FF_LIBRARY_SYMBOL(JS_FreeValue)
+
+ JSRuntime* rt;
+ JSContext* ctx;
+ bool inited;
+} qjsData;
+
+static const char* loadQuickJSState(void) {
+ if (qjsData.inited) {
+ if (qjsData.ctx == NULL) {
+ return "QuickJS is not available";
+ }
+ return NULL;
+ }
+
+ qjsData.inited = true;
+ #ifdef _WIN32
+ FF_LIBRARY_LOAD_MESSAGE(libqjs, "libqjs-0" FF_LIBRARY_EXTENSION, 0)
+ #else
+ FF_LIBRARY_LOAD_MESSAGE(libqjs, "libqjs" FF_LIBRARY_EXTENSION, 0)
+ #endif
+
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewRuntime)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewContext)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeRuntime)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_EvalThis)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_GetException)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_ToCStringLen2)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeCString)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewStringLen)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewArray)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_SetPropertyUint32)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewObject)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_SetPropertyStr)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeValue)
+
+ qjsData.rt = qjsData.ffJS_NewRuntime();
+ if (qjsData.rt == NULL) {
+ return "JS_NewRuntime() failed";
+ }
+
+ qjsData.ctx = qjsData.ffJS_NewContext(qjsData.rt);
+ if (qjsData.ctx == NULL) {
+ qjsData.ffJS_FreeRuntime(qjsData.rt);
+ qjsData.rt = NULL;
+ return "JS_NewContext() failed";
+ }
+
+ libqjs = NULL; // don't close quickjs
+
+ return NULL;
+}
+
+static bool parseQuickJSString(FFstrbuf* buffer, const char* script, uint32_t scriptLen, uint32_t numArgs, const FFformatarg* arguments) {
+ const char* err = loadQuickJSState();
+ if (err) {
+ ffStrbufAppendF(buffer, "Qjs init error: %s", err);
+ return false;
+ }
+ JSContext* ctx = qjsData.ctx;
+ JSValue argsObj = qjsData.ffJS_NewObject(ctx);
+ FF_STRBUF_AUTO_DESTROY argNameBuf = ffStrbufCreate();
+
+ for (uint32_t i = 0; i < numArgs; ++i) {
+ const FFformatarg* arg = &arguments[i];
+
+ JSValue value;
+ switch (arg->type) {
+ case FF_ARG_TYPE_INT:
+ value = JS_NewInt32(ctx, *(int32_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_UINT:
+ value = JS_NewUint32(ctx, *(uint32_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_UINT64: {
+ uint64_t val = *(uint64_t*) arg->value;
+ if (val <= INT32_MAX) {
+ value = JS_NewInt32(ctx, (int32_t) val);
+ } else {
+ value = JS_NewFloat64(ctx, (double) val);
+ }
+ break;
+ }
+ case FF_ARG_TYPE_UINT16:
+ value = JS_NewUint32(ctx, *(uint16_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_UINT8:
+ value = JS_NewUint32(ctx, *(uint8_t*) arg->value);
+ break;
+ case FF_ARG_TYPE_FLOAT:
+ value = JS_NewFloat64(ctx, *(float*) arg->value);
+ break;
+ case FF_ARG_TYPE_DOUBLE:
+ value = JS_NewFloat64(ctx, *(double*) arg->value);
+ break;
+ case FF_ARG_TYPE_BOOL:
+ value = JS_NewBool(ctx, *(bool*) arg->value);
+ break;
+ case FF_ARG_TYPE_STRING:
+ value = qjsData.ffJS_NewStringLen(ctx, (const char*) arg->value, strlen((const char*) arg->value));
+ break;
+ case FF_ARG_TYPE_STRBUF: {
+ const FFstrbuf* sb = (const FFstrbuf*) arg->value;
+ value = qjsData.ffJS_NewStringLen(ctx, sb->chars, sb->length);
+ break;
+ }
+ case FF_ARG_TYPE_LIST: {
+ const FFlist* list = (const FFlist*) arg->value;
+ JSValue arr = qjsData.ffJS_NewArray(ctx);
+ for (uint32_t li = 0; li < list->length; ++li) {
+ const FFstrbuf* item = FF_LIST_GET(FFstrbuf, *list, li);
+ JSValue itemValue = qjsData.ffJS_NewStringLen(ctx, item->chars, item->length);
+ qjsData.ffJS_SetPropertyUint32(ctx, arr, li, itemValue);
+ }
+
+ value = arr;
+ break;
+ }
+ default:
+ value = JS_UNDEFINED;
+ break;
+ }
+
+ if (arg->name && arg->name[0]) {
+ normalizeArgName(&argNameBuf, arg->name);
+ } else {
+ ffStrbufSetF(&argNameBuf, "arg%" PRIu32, i + 1);
+ }
+ qjsData.ffJS_SetPropertyStr(ctx, argsObj, argNameBuf.chars, value);
+ }
+ JSValue result = qjsData.ffJS_EvalThis(ctx, argsObj, script, scriptLen, "", JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_STRICT);
+
+ qjsData.ffJS_FreeValue(ctx, argsObj);
+
+ bool ret = false;
+ if (JS_IsException(result)) {
+ JSValue exc = qjsData.ffJS_GetException(ctx);
+ const char* message = qjsData.ffJS_ToCStringLen2(ctx, NULL, exc, false);
+ qjsData.ffJS_FreeValue(ctx, exc);
+ ffStrbufAppendF(buffer, "Qjs runtime error: %s", message ?: "unknown");
+ if (message) {
+ qjsData.ffJS_FreeCString(ctx, message);
+ }
+ } else if (JS_IsUndefined(result)) {
+ ffStrbufAppendS(buffer, "Qjs result error: undefined result");
+ } else {
+ size_t len;
+ const char* res = qjsData.ffJS_ToCStringLen2(ctx, &len, result, false);
+ if (res) {
+ ffStrbufAppendNS(buffer, (uint32_t) len, res);
+ qjsData.ffJS_FreeCString(ctx, res);
+ }
+ ret = true;
+ }
+
+ qjsData.ffJS_FreeValue(ctx, result);
+ return ret;
+}
+#endif
+
+static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) {
+ if (__builtin_expect(in->chars[0] == '\e' && in->chars[1] == '[', false)) {
+ // skip ANSI escape codes at the start of the string for truncation
+ const char* p = in->chars + 2;
+ while (*p && (ffCharIsDigit(*p) || *p == ';')) {
+ ++p;
+ }
+ if (*p && isascii(*p)) {
+ ++p;
+ }
+ uint32_t prefixLen = (uint32_t) (p - in->chars);
+ ffStrbufAppendNS(out, prefixLen, in->chars);
+ ffStrbufSubstrAfter(in, prefixLen - 1);
+
+ if (trailingEscape) {
+ // likely have a `CSI m` reset at the end of the string
+ uint32_t iLastEscape = ffStrbufLastIndexC(in, '\e');
+ if (iLastEscape != in->length) {
+ ffStrbufSetNS(trailingEscape, in->length - iLastEscape, in->chars + iLastEscape);
+ ffStrbufSubstrBefore(in, iLastEscape);
+ }
+ }
+ return true;
+ }
+ return false;
+}
+
+static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
+ uint32_t argCounter = 0;
+
+ uint32_t numOpenIfs = 0;
+ uint32_t numOpenNotIfs = 0;
+
+ FF_STRBUF_AUTO_DESTROY placeholderValue = ffStrbufCreate();
+
+ for (uint32_t i = 0; i < formatstr->length; ++i) {
+ // if we don't have a placeholder start just copy the chars over to output buffer
+ if (formatstr->chars[i] != '{') {
+ ffStrbufAppendC(buffer, formatstr->chars[i]);
+ continue;
+ }
+
+ // jump to next char, the start of the placeholder value
+ ++i;
+
+ // unmatched trailing '{'
+ if (i >= formatstr->length) {
+ ffStrbufAppendC(buffer, '{');
+ break;
+ }
+
+ // double {{ elvaluates to a single { and doesn't count as start
+ if (formatstr->chars[i] == '{') {
+ ffStrbufAppendC(buffer, '{');
+ continue;
+ }
+
+ ffStrbufClear(&placeholderValue);
+
+ {
+ uint32_t iEnd = ffStrbufNextIndexC(formatstr, i, '}');
+ ffStrbufAppendNS(&placeholderValue, iEnd - i, &formatstr->chars[i]);
+ i = iEnd;
+ }
+
+ char firstChar = placeholderValue.chars[0];
+
+ if (placeholderValue.length == 1) {
+ // test if for stop, if so break the loop
+ if (firstChar == '-') {
+ break;
+ }
+
+ // test for end of an if, if so do nothing
+ if (firstChar == '?') {
+ if (numOpenIfs == 0) {
+ appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
+ } else {
+ --numOpenIfs;
+ }
+
+ continue;
+ }
+
+ // test for end of a not if, if so do nothing
+ if (firstChar == '/') {
+ if (numOpenNotIfs == 0) {
+ appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
+ } else {
+ --numOpenNotIfs;
+ }
+
+ continue;
+ }
+
+ // test for end of a color, if so do nothing
+ if (firstChar == '#') {
+ if (!instance.config.display.pipe) {
+ ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
+ }
+
+ continue;
+ }
+ }
+
+ // test for if, if so evaluate it
+ if (firstChar == '?') {
+ ffStrbufSubstrAfter(&placeholderValue, 0);
+
+ uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments);
+
+ // testing for an invalid index
+ if (index > numArgs || index < 1) {
+ appendInvalidPlaceholder(buffer, "{?", &placeholderValue, i, formatstr->length);
+ continue;
+ }
+
+ // continue normally if an format arg is set and the value is > 0
+ if (formatArgSet(&arguments[index - 1])) {
+ ++numOpenIfs;
+ continue;
+ }
+
+ // fastforward to the end of the if without printing the in between
+ i = ffStrbufNextIndexS(formatstr, i, "{?}") + 2; // 2 is the length of "{?}" - 1 because the loop will increment it again directly after continue
+ continue;
+ }
+
+ // test for not if, if so evaluate it
+ if (firstChar == '/') {
+ ffStrbufSubstrAfter(&placeholderValue, 0);
+
+ uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments);
+
+ // testing for an invalid index
+ if (index > numArgs || index < 1) {
+ appendInvalidPlaceholder(buffer, "{/", &placeholderValue, i, formatstr->length);
+ continue;
+ }
+
+ // continue normally if an format arg is not set or the value is 0
+ if (!formatArgSet(&arguments[index - 1])) {
+ ++numOpenNotIfs;
+ continue;
+ }
+
+ // fastforward to the end of the if without printing the in between
+ i = ffStrbufNextIndexS(formatstr, i, "{/}") + 2; // 2 is the length of "{/}" - 1 because the loop will increment it again directly after continue
+ continue;
+ }
+
+ // test for color, if so evaluate it
+ if (firstChar == '#') {
+ if (!instance.config.display.pipe) {
+ ffStrbufAppendS(buffer, "\e[");
+ ffOptionParseColorNoClear(placeholderValue.chars + 1, buffer);
+ ffStrbufAppendC(buffer, 'm');
+ }
+ continue;
+ }
+
+ // test for constant or env var, if so evaluate it
+ if (firstChar == '$') {
+ char* pend = NULL;
+ int32_t indexSigned = (int32_t) strtol(placeholderValue.chars + 1, &pend, 10);
+ if (pend == placeholderValue.chars + 1) {
+ // treat placeholder as an environment variable
+ char* envValue = getenv(placeholderValue.chars + 1);
+ if (envValue) {
+ ffStrbufAppendS(buffer, envValue);
+ } else {
+ appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
+ }
+ } else {
+ // treat placeholder as a constant
+ uint32_t index = (uint32_t) (indexSigned < 0 ? (int32_t) instance.config.display.constants.length + indexSigned : indexSigned - 1);
+
+ if (*pend != '\0' || instance.config.display.constants.length <= index) {
+ appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
+ } else {
+ FFstrbuf* item = FF_LIST_GET(FFstrbuf, instance.config.display.constants, index);
+ ffStrbufAppend(buffer, item);
+ }
+ }
+ continue;
+ }
+
+ char* pSep = placeholderValue.chars;
+ char cSep = '\0';
+ while (*pSep && *pSep != ':' && *pSep != '<' && *pSep != '>' && *pSep != '|' && *pSep != '~') {
+ ++pSep;
+ }
+ if (*pSep) {
+ cSep = *pSep;
+ *pSep = '\0';
+ } else {
+ pSep = NULL;
+ }
+
+ uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments);
+
+ // test for invalid index
+ if (index == 0) {
+ index = ++argCounter;
+ }
+
+ if (index > numArgs) {
+ if (pSep) {
+ *pSep = cSep;
+ }
+ appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
+ continue;
+ }
+
+ if (!cSep) {
+ ffFormatAppendFormatArg(buffer, &arguments[index - 1]);
+ } else if (cSep == '~') {
+ FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate();
+ ffFormatAppendFormatArg(&tempString, &arguments[index - 1]);
+ FF_STRBUF_AUTO_DESTROY trailingEscape = ffStrbufCreate();
+ skipAnsiEscape(&tempString, buffer, &trailingEscape);
+
+ char* pEnd = NULL;
+ int32_t start = (int32_t) strtol(pSep + 1, &pEnd, 10);
+ if (start < 0) {
+ start = (int32_t) tempString.length + start;
+ }
+ if (start >= 0 && (uint32_t) start < tempString.length) {
+ if (*pEnd == '\0') {
+ ffStrbufAppendNS(buffer, tempString.length - (uint32_t) start, &tempString.chars[start]);
+ } else if (*pEnd == ',') {
+ int32_t end = (int32_t) strtol(pEnd + 1, &pEnd, 10);
+ if (!*pEnd) {
+ if (end < 0) {
+ end = (int32_t) tempString.length + end;
+ }
+ if ((uint32_t) end > tempString.length) {
+ end = (int32_t) tempString.length;
+ }
+ if (end > start) {
+ ffStrbufAppendNS(buffer, (uint32_t) (end - start), &tempString.chars[start]);
+ }
+ }
+ }
+ }
+
+ if (trailingEscape.length > 0) {
+ ffStrbufAppend(buffer, &trailingEscape);
+ }
+
+ if (*pEnd) {
+ *pSep = cSep;
+ appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
+ continue;
+ }
+ } else {
+ char* pEnd = NULL;
+ int32_t truncLength = (int32_t) strtol(pSep + 1, &pEnd, 10);
+ if (*pEnd != '\0') {
+ *pSep = cSep;
+ appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length);
+ continue;
+ }
+
+ bool ellipsis = false;
+ if (truncLength < 0) {
+ ellipsis = true;
+ truncLength = -truncLength;
+ }
+
+ FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate();
+ ffFormatAppendFormatArg(&tempString, &arguments[index - 1]);
+ FF_STRBUF_AUTO_DESTROY trailingEscape = ffStrbufCreate();
+ skipAnsiEscape(&tempString, buffer, &trailingEscape);
+
+ if (tempString.length == (uint32_t) truncLength) {
+ ffStrbufAppend(buffer, &tempString);
+ } else if (tempString.length > (uint32_t) truncLength) {
+ if (cSep == ':') {
+ ffStrbufSubstrBefore(&tempString, (uint32_t) truncLength);
+ ffStrbufTrimRightSpace(&tempString);
+ } else {
+ ffStrbufSubstrBefore(&tempString, (uint32_t) (!ellipsis ? truncLength : truncLength - 1));
+ }
+ ffStrbufAppend(buffer, &tempString);
+
+ if (ellipsis) {
+ ffStrbufAppendS(buffer, "…");
+ }
+ } else if (cSep == ':') {
+ ffStrbufAppend(buffer, &tempString);
+ } else {
+ if (cSep == '<') { // left align
+ ffStrbufAppend(buffer, &tempString);
+ ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length, ' ');
+ } else if (cSep == '>') { // right align
+ ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length, ' ');
+ ffStrbufAppend(buffer, &tempString);
+ } else if (cSep == '|') { // center align
+ uint32_t padding = ((uint32_t) truncLength - tempString.length) / 2;
+ ffStrbufAppendNC(buffer, padding, ' ');
+ ffStrbufAppend(buffer, &tempString);
+ ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length - padding, ' ');
+ }
+ }
+
+ if (trailingEscape.length > 0) {
+ ffStrbufAppend(buffer, &trailingEscape);
+ }
+ }
+ }
+
+ if (!instance.config.display.pipe) {
+ ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
+ }
+
+ return true;
+}
+
+bool ffParseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {
+#if FF_HAVE_QUICKJS
+ if (ffStrbufStartsWithS(formatstr, "qjs:")) {
+ return parseQuickJSString(buffer, formatstr->chars + 4, formatstr->length - 4, numArgs, arguments);
+ }
+#endif
+
+#if FF_HAVE_LUA
+ if (ffStrbufStartsWithS(formatstr, "lua:")) {
+ // If outputFormat starts with "lua:", treat the rest as a Lua script
+ return parseLuaString(buffer, formatstr->chars + 4, formatstr->length - 4, numArgs, arguments);
+ }
+#endif
+
+ return parseFormatString(buffer, formatstr, numArgs, arguments);
+}
diff --git a/src/common/impl/frequency.c b/src/common/impl/frequency.c
new file mode 100644
index 0000000..4838df8
--- /dev/null
+++ b/src/common/impl/frequency.c
@@ -0,0 +1,26 @@
+#include "common/frequency.h"
+
+bool ffFreqAppendNum(uint32_t mhz, FFstrbuf* result) {
+ if (mhz == 0) {
+ return false;
+ }
+
+ const FFOptionsDisplay* options = &instance.config.display;
+ bool spaceBeforeUnit = options->freqSpaceBeforeUnit != FF_SPACE_BEFORE_UNIT_NEVER;
+ int8_t ndigits = options->freqNdigits;
+
+ if (ndigits >= 0) {
+ ffStrbufAppendDouble(result, mhz / 1000., ndigits, true);
+ if (spaceBeforeUnit) {
+ ffStrbufAppendC(result, ' ');
+ }
+ ffStrbufAppendS(result, "GHz");
+ } else {
+ ffStrbufAppendUInt(result, mhz);
+ if (spaceBeforeUnit) {
+ ffStrbufAppendC(result, ' ');
+ }
+ ffStrbufAppendS(result, "MHz");
+ }
+ return true;
+}
diff --git a/src/common/impl/init.c b/src/common/impl/init.c
new file mode 100644
index 0000000..f0fc0a2
--- /dev/null
+++ b/src/common/impl/init.c
@@ -0,0 +1,300 @@
+#include "fastfetch.h"
+#include "common/init.h"
+#include "common/parsing.h"
+#include "common/thread.h"
+#include "common/textModifier.h"
+#include "detection/displayserver/displayserver.h"
+#include "detection/terminaltheme/terminaltheme.h"
+#include "logo/logo.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <locale.h>
+#ifdef _WIN32
+ #include <windows.h>
+ #include "common/windows/unicode.h"
+#else
+ #include <signal.h>
+#endif
+
+FFinstance instance; // Global singleton
+
+static void initState(FFstate* state) {
+ state->logoWidth = 0;
+ state->logoHeight = 0;
+ state->keysHeight = 0;
+ state->terminalLightTheme = false;
+ state->titleFqdn = false;
+
+ ffPlatformInit(&state->platform);
+ state->dynamicInterval = 0;
+
+ #if !FF_MODULE_DISABLE_TERMINALTHEME
+ {
+ // don't enable bright color if the terminal is in light mode
+ FFTerminalThemeResult result;
+ if (ffDetectTerminalTheme(&result, true /* forceEnv for performance */) && !result.bg.dark) {
+ state->terminalLightTheme = true;
+ }
+ }
+ #endif
+}
+
+static void defaultConfig(void) {
+ ffOptionsInitLogo(&instance.config.logo);
+ ffOptionsInitGeneral(&instance.config.general);
+ ffOptionsInitDisplay(&instance.config.display);
+}
+
+void ffInitInstance(void) {
+#ifdef _WIN32
+ // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale?source=recommendat>
+ setlocale(LC_ALL, ".UTF8");
+#else
+ // Never use `setlocale(LC_ALL, "")`
+ setlocale(LC_TIME, "");
+#endif
+
+ defaultConfig();
+ initState(&instance.state);
+}
+
+static volatile bool ffDisableLinewrap = false;
+static volatile bool ffHideCursor = false;
+#ifdef _WIN32
+static volatile UINT oldCp = CP_UTF8;
+#endif
+
+static void resetConsole(void) {
+ if (ffDisableLinewrap) {
+ fputs("\033[?7h", stdout);
+ }
+
+ if (ffHideCursor) {
+ fputs("\033[?25h", stdout);
+ }
+
+ if (instance.state.dynamicInterval > 0) {
+ fputs("\033[?1049l", stdout); // Disable alternate buffer
+ }
+
+#if defined(_WIN32)
+ fflush(stdout);
+
+ if (oldCp != CP_UTF8) {
+ SetConsoleOutputCP(oldCp);
+ }
+#endif
+}
+
+#ifdef _WIN32
+BOOL WINAPI consoleHandler(FF_A_UNUSED DWORD signal) {
+ resetConsole();
+ exit(0);
+}
+#else
+static void exitSignalHandler(FF_A_UNUSED int signal) {
+ resetConsole();
+ exit(0);
+}
+#endif
+
+void ffStart(void) {
+ ffDisableLinewrap = instance.config.display.disableLinewrap && !instance.config.display.pipe;
+ ffHideCursor = instance.config.display.hideCursor && !instance.config.display.pipe;
+
+#ifdef _WIN32
+ SetErrorMode(SEM_FAILCRITICALERRORS);
+ if (instance.config.display.noBuffer) {
+ setvbuf(stdout, NULL, _IONBF, 0);
+ } else {
+ setvbuf(stdout, NULL, _IOFBF, 4096);
+ }
+ SetConsoleCtrlHandler(consoleHandler, TRUE);
+ HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
+ DWORD mode = 0;
+ if (GetConsoleMode(hStdout, &mode)) {
+ SetConsoleMode(hStdout, mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
+ oldCp = GetConsoleOutputCP();
+ if (oldCp != CP_UTF8) {
+ SetConsoleOutputCP(CP_UTF8);
+ }
+ }
+#else
+ if (instance.config.display.noBuffer) {
+ setvbuf(stdout, NULL, _IONBF, 0);
+ }
+ struct sigaction action;
+ sigemptyset(&action.sa_mask);
+ action.sa_flags = 0;
+ action.sa_handler = exitSignalHandler;
+ sigaction(SIGINT, &action, NULL);
+ sigaction(SIGTERM, &action, NULL);
+ sigaction(SIGQUIT, &action, NULL);
+ sigset_t newmask;
+ sigemptyset(&newmask);
+ sigaddset(&newmask, SIGCHLD);
+ sigprocmask(SIG_BLOCK, &newmask, NULL);
+#endif
+
+ // reset everything to default before we start printing
+ if (!instance.config.display.pipe) {
+ fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout);
+ }
+
+ if (ffHideCursor) {
+ fputs("\033[?25l", stdout);
+ }
+
+ if (ffDisableLinewrap) {
+ fputs("\033[?7l", stdout);
+ }
+
+ if (instance.state.dynamicInterval > 0) {
+ fputs("\033[?1049h\033[H", stdout); // Enable alternate buffer
+ fflush(stdout);
+ }
+}
+
+void ffFinish(void) {
+ resetConsole();
+}
+
+static void destroyConfig(void) {
+ ffOptionsDestroyLogo(&instance.config.logo);
+ ffOptionsDestroyGeneral(&instance.config.general);
+ ffOptionsDestroyDisplay(&instance.config.display);
+}
+
+static void destroyState(void) {
+ ffPlatformDestroy(&instance.state.platform);
+}
+
+void ffDestroyInstance(void) {
+ destroyConfig();
+ destroyState();
+}
+
+#if FF_HAVE_LUA
+ #include <lua.h>
+#endif
+#if FF_HAVE_QUICKJS
+ #include <quickjs.h>
+ #define FF_STR_INDIR(x) #x
+ #define FF_STR(x) FF_STR_INDIR(x)
+#endif
+
+// Must be in a file compiled with the libfastfetch target, because the FF_HAVE* macros are not defined for the executable targets
+void ffListFeatures(void) {
+ fputs(
+#if FF_HAVE_THREADS
+ "threads\n"
+#endif
+#if FF_HAVE_VULKAN
+ "vulkan\n"
+#endif
+#if FF_HAVE_WAYLAND
+ "wayland\n"
+#endif
+#if FF_HAVE_XCB_RANDR
+ "xcb-randr\n"
+#endif
+#if FF_HAVE_XRANDR
+ "xrandr\n"
+#endif
+#if FF_HAVE_DRM
+ "drm\n"
+#endif
+#if FF_HAVE_DRM_AMDGPU
+ "drm_amdgpu\n"
+#endif
+#if FF_HAVE_GIO
+ "gio\n"
+#endif
+#if FF_HAVE_DCONF
+ "dconf\n"
+#endif
+#if FF_HAVE_EET
+ "eet\n"
+#endif
+#if FF_HAVE_DBUS
+ "dbus\n"
+#endif
+#if FF_HAVE_IMAGEMAGICK7
+ "imagemagick7\n"
+#endif
+#if FF_HAVE_IMAGEMAGICK6
+ "imagemagick6\n"
+#endif
+#if FF_HAVE_CHAFA
+ "chafa\n"
+#endif
+#if FF_HAVE_ZLIB
+ "zlib\n"
+#endif
+#if FF_HAVE_SQLITE3
+ "sqlite3\n"
+#endif
+#if FF_HAVE_RPM
+ "rpm\n"
+#endif
+#if FF_HAVE_EGL
+ "egl\n"
+#endif
+#if FF_HAVE_GLX
+ "glx\n"
+#endif
+#if FF_HAVE_OPENCL
+ "opencl\n"
+#endif
+#if FF_HAVE_FREETYPE
+ "freetype\n"
+#endif
+#if FF_HAVE_PULSE
+ "libpulse\n"
+#endif
+#if FF_HAVE_DDCUTIL
+ "libddcutil\n"
+#endif
+#if FF_HAVE_ELF || __sun || (__FreeBSD__ && !__DragonFly__) || __OpenBSD__ || __NetBSD__
+ "libelf\n"
+#endif
+#if FF_HAVE_LIBZFS
+ "libzfs\n"
+#endif
+#if FF_HAVE_VA
+ "va\n"
+#endif
+#if FF_HAVE_VDPAU
+ "vdpau\n"
+#endif
+#if FF_USE_SYSTEM_YYJSON
+ "System yyjson\n"
+#endif
+#if FF_HAVE_LINUX_VIDEODEV2
+ "linux/videodev2\n"
+#endif
+#if FF_HAVE_EMBEDDED_PCIIDS
+ "Embedded pciids\n"
+#endif
+#if FF_ENABLE_WCWIDTH
+ "Embedded wcwidth\n"
+#endif
+#if FF_HAVE_WINRT
+ "WinRT headers\n"
+#endif
+#if FF_WIN81_COMPAT
+ "Windows 8.1 Compatibility\n"
+#endif
+#if FF_APPLE_MEMSIZE_USABLE
+ "Apple memsize_usable\n"
+#endif
+#if FF_HAVE_LUA
+ LUA_VERSION "\n"
+#endif
+#if FF_HAVE_QUICKJS
+ "QuickJS " FF_STR(QJS_VERSION_MAJOR) "." FF_STR(QJS_VERSION_MINOR) "." FF_STR(QJS_VERSION_PATCH) QJS_VERSION_SUFFIX "\n"
+#endif
+ "",
+ stdout);
+}
diff --git a/src/common/impl/io_unix.c b/src/common/impl/io_unix.c
new file mode 100644
index 0000000..e603186
--- /dev/null
+++ b/src/common/impl/io_unix.c
@@ -0,0 +1,333 @@
+#include "common/io.h"
+#include "fastfetch.h"
+#include "common/strutil.h"
+#include "common/time.h"
+
+#include <fcntl.h>
+#include <termios.h>
+#include <dirent.h>
+#include <errno.h>
+#ifndef __APPLE__
+ #include <poll.h>
+#else
+ #include <sys/select.h>
+#endif
+
+#if FF_HAVE_WORDEXP
+ #include <wordexp.h>
+#else
+ #include <glob.h>
+#endif
+
+static void createSubfolders(const char* fileName) {
+ FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate();
+
+ const char* token = NULL;
+ while ((token = strchr(fileName, '/')) != NULL) {
+ ffStrbufAppendNS(&path, (uint32_t) (token - fileName + 1), fileName);
+ mkdir(path.chars, S_IRWXU | S_IRGRP | S_IROTH);
+ fileName = token + 1;
+ }
+}
+
+bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
+ int openFlagsModes = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC;
+ mode_t openFlagsRights = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
+
+ int FF_AUTO_CLOSE_FD fd = open(fileName, openFlagsModes, openFlagsRights);
+ if (fd == -1) {
+ if (errno == ENOENT) {
+ createSubfolders(fileName);
+ fd = open(fileName, openFlagsModes, openFlagsRights);
+ if (fd == -1) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ return write(fd, data, dataSize) > 0;
+}
+
+static inline void readWithLength(int fd, FFstrbuf* buffer, uint32_t length) {
+ ffStrbufEnsureFixedLengthFree(buffer, length);
+ ssize_t bytesRead = 0;
+ while (
+ length > 0 && (bytesRead = read(fd, buffer->chars + buffer->length, length)) > 0) {
+ buffer->length += (uint32_t) bytesRead;
+ length -= (uint32_t) bytesRead;
+ }
+}
+
+static inline void readUntilEOF(int fd, FFstrbuf* buffer) {
+ ffStrbufEnsureFree(buffer, 31);
+ uint32_t available = ffStrbufGetFree(buffer);
+ ssize_t bytesRead = 0;
+ while (
+ (bytesRead = read(fd, buffer->chars + buffer->length, available)) > 0) {
+ buffer->length += (uint32_t) bytesRead;
+ if ((uint32_t) bytesRead == available) {
+ ffStrbufEnsureFree(buffer, buffer->allocated - 1); // Doubles capacity every round. -1 for the null byte.
+ }
+ available = ffStrbufGetFree(buffer);
+ }
+}
+
+bool ffAppendFDBuffer(int fd, FFstrbuf* buffer) {
+ struct stat fileInfo;
+ if (fstat(fd, &fileInfo) != 0) {
+ return false;
+ }
+
+ if (fileInfo.st_size > 0) {
+ readWithLength(fd, buffer, (uint32_t) fileInfo.st_size);
+ } else {
+ readUntilEOF(fd, buffer);
+ }
+
+ buffer->chars[buffer->length] = '\0';
+
+ return buffer->length > 0;
+}
+
+bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
+ bool result = false;
+
+#if FF_HAVE_WORDEXP
+
+ wordexp_t exp;
+ if (wordexp(in, &exp, 0) != 0) { // WARN: 0 = no safety flags; command substitution allowed
+ return false;
+ }
+
+ if (exp.we_wordc >= 1) {
+ result = true;
+ ffStrbufSetS(out, exp.we_wordv[exp.we_wordc > 1 ? ffTimeGetNow() % exp.we_wordc : 0]);
+ }
+
+ wordfree(&exp);
+
+#else
+
+ glob_t gb;
+ if (glob(in, GLOB_NOSORT
+ #ifdef GLOB_TILDE
+ | GLOB_TILDE
+ #endif
+ #ifdef GLOB_BRACE
+ | GLOB_BRACE
+ #endif
+ ,
+ NULL,
+ &gb) != 0)
+ return false;
+
+ if (gb.gl_pathc >= 1) {
+ result = true;
+ ffStrbufSetS(out, gb.gl_pathv[gb.gl_pathc > 1 ? ffTimeGetNow() % (unsigned) gb.gl_pathc : 0]);
+ }
+
+ globfree(&gb);
+
+#endif
+
+ return result;
+}
+
+static int ftty = -1;
+static struct termios oldTerm;
+void restoreTerm(void) {
+ tcsetattr(ftty, TCSAFLUSH, &oldTerm);
+}
+
+const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) {
+ if (ftty < 0) {
+ ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC);
+ if (ftty < 0) {
+ return "open(\"/dev/tty\", O_RDWR | O_NOCTTY | O_CLOEXEC) failed";
+ }
+
+ if (tcgetattr(ftty, &oldTerm) == -1) {
+ return "tcgetattr(STDIN_FILENO, &oldTerm) failed";
+ }
+
+ struct termios newTerm = oldTerm;
+ newTerm.c_lflag &= (tcflag_t) ~(ICANON | ECHO);
+ if (tcsetattr(ftty, TCSAFLUSH, &newTerm) == -1) {
+ return "tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm)";
+ }
+ atexit(restoreTerm);
+ }
+
+ ffWriteFDData(ftty, strlen(request), request);
+
+// Give the terminal some time to respond
+#ifndef __APPLE__
+ if (poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS) <= 0) {
+ return "poll(/dev/tty) timeout or failed";
+ }
+#else
+ {
+ // On macOS, poll(/dev/tty) always returns immediately
+ // See also https://nathancraddock.com/blog/macos-dev-tty-polling/
+ fd_set rd;
+ FD_ZERO(&rd);
+ FD_SET(ftty, &rd);
+ if (select(ftty + 1, &rd, NULL, NULL, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }) <= 0) {
+ return "select(/dev/tty) timeout or failed";
+ }
+ }
+#endif
+
+ char buffer[1024];
+ size_t bytesRead = 0;
+
+ va_list args;
+ va_start(args, format);
+
+ while (true) {
+ ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1);
+
+ if (nRead <= 0) {
+ va_end(args);
+ return "read(STDIN_FILENO, buffer, sizeof(buffer) - 1) failed";
+ }
+
+ bytesRead += (size_t) nRead;
+ buffer[bytesRead] = '\0';
+
+ va_list cargs;
+ va_copy(cargs, args);
+ int ret = vsscanf(buffer, format, cargs);
+ va_end(cargs);
+
+ if (ret <= 0) {
+ va_end(args);
+ return "vsscanf(buffer, format, args) failed";
+ }
+ if (ret >= nParams) {
+ break;
+ }
+ }
+
+ va_end(args);
+
+ return NULL;
+}
+
+bool ffSuppressIO(bool suppress) {
+#ifndef NDEBUG
+ if (instance.config.display.debugMode) {
+ return false;
+ }
+#endif
+
+ static bool init = false;
+ static int origOut = -1;
+ static int origErr = -1;
+ static int nullFile = -1;
+
+ if (!init) {
+ if (!suppress) {
+ return true;
+ }
+
+ origOut = dup(STDOUT_FILENO);
+ origErr = dup(STDERR_FILENO);
+ nullFile = open("/dev/null", O_WRONLY | O_CLOEXEC);
+ init = true;
+ }
+
+ if (nullFile == -1) {
+ return false;
+ }
+
+ fflush(stdout);
+ fflush(stderr);
+
+ dup2(suppress ? nullFile : origOut, STDOUT_FILENO);
+ dup2(suppress ? nullFile : origErr, STDERR_FILENO);
+ return true;
+}
+
+void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indentation, const char* folderName, bool pretty) {
+ int dfd = open(folder->chars, O_RDONLY | O_CLOEXEC | O_DIRECTORY); // Ownership of dfd will be transformed to dir
+ if (dfd < 0) {
+ return;
+ }
+
+ FF_AUTO_CLOSE_DIR DIR* dir = fdopendir(dfd);
+ if (dir == NULL) {
+ close(dfd);
+ return; // Should not happen
+ }
+
+ uint32_t folderLength = folder->length;
+
+ if (pretty && folderName != NULL) {
+ for (uint8_t i = 0; i < indentation - 1; i++) {
+ fputs(" | ", stdout);
+ }
+ printf("%s/\n", folderName);
+ }
+
+ struct dirent* entry;
+
+ while ((entry = readdir(dir)) != NULL) {
+ if (entry->d_name[0] == '.') { // skip hidden files
+ continue;
+ }
+
+ bool isDir = false;
+#if !defined(__sun) && !defined(__HAIKU__)
+ if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK) {
+ isDir = entry->d_type == DT_DIR;
+ } else
+#endif
+ {
+ struct stat stbuf;
+ if (fstatat(dfd, entry->d_name, &stbuf, 0) < 0) {
+ isDir = false;
+ } else {
+ isDir = S_ISDIR(stbuf.st_mode);
+ }
+ }
+ if (isDir) {
+ ffStrbufAppendS(folder, entry->d_name);
+ ffStrbufAppendC(folder, '/');
+ listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry->d_name, pretty);
+ ffStrbufSubstrBefore(folder, folderLength);
+ continue;
+ }
+
+ if (pretty) {
+ for (uint8_t i = 0; i < indentation; i++) {
+ fputs(" | ", stdout);
+ }
+ } else {
+ fputs(folder->chars + baseLength, stdout);
+ }
+
+ puts(entry->d_name);
+ }
+}
+
+void ffListFilesRecursively(const char* path, bool pretty) {
+ FF_STRBUF_AUTO_DESTROY folder = ffStrbufCreateS(path);
+ ffStrbufEnsureEndsWithC(&folder, '/');
+ listFilesRecursively(folder.length, &folder, 0, NULL, pretty);
+}
+
+FFNativeFD ffGetNullFD(void) {
+ static FFNativeFD hNullFile = -1;
+ if (hNullFile != -1) {
+ return hNullFile;
+ }
+ hNullFile = open("/dev/null", O_WRONLY | O_CLOEXEC);
+ return hNullFile;
+}
+
+bool ffRemoveFile(const char* fileName) {
+ return unlink(fileName) == 0;
+}
diff --git a/src/common/impl/io_windows.c b/src/common/impl/io_windows.c
new file mode 100644
index 0000000..24b8d05
--- /dev/null
+++ b/src/common/impl/io_windows.c
@@ -0,0 +1,501 @@
+#include "fastfetch.h"
+#include "common/io.h"
+#include "common/strutil.h"
+#include "common/windows/nt.h"
+#include "common/windows/unicode.h"
+
+#include <windows.h>
+
+static bool createSubfolders(wchar_t* fileName) {
+ HANDLE hRoot = ffGetPeb()->ProcessParameters->CurrentDirectory.Handle;
+ bool closeRoot = false;
+ wchar_t* ptr = fileName;
+
+ // Absolute drive path: C:\...
+ if (ffCharIsEnglishAlphabet((char) ptr[0]) && ptr[1] == L':' && ptr[2] == L'\\') {
+ wchar_t saved = ptr[3];
+ ptr[3] = L'\0';
+
+ hRoot = CreateFileW(
+ fileName,
+ FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ NULL,
+ OPEN_EXISTING,
+ FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS,
+ NULL);
+
+ ptr[3] = saved;
+ if (hRoot == INVALID_HANDLE_VALUE) {
+ return false;
+ }
+
+ closeRoot = true;
+ ptr += 3; // skip "C:\"
+ }
+ // UNC path: \\server\share\...
+ else if (ptr[0] == L'\\' && ptr[1] == L'\\') {
+ wchar_t* serverEnd = wcschr(ptr + 2, L'\\');
+ if (serverEnd == NULL) {
+ return false;
+ }
+
+ wchar_t* shareEnd = wcschr(serverEnd + 1, L'\\');
+ if (shareEnd == NULL) {
+ return true; // no parent subfolder exists before file name
+ }
+
+ wchar_t saved = *shareEnd;
+ *shareEnd = L'\0';
+
+ hRoot = CreateFileW(
+ fileName,
+ FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ NULL,
+ OPEN_EXISTING,
+ FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS,
+ NULL);
+
+ *shareEnd = saved;
+ if (hRoot == INVALID_HANDLE_VALUE) {
+ return false;
+ }
+
+ closeRoot = true;
+ ptr = shareEnd + 1; // first component under share
+ }
+ // Rooted path on current drive: \foo\bar
+ else if (ptr[0] == L'\\') {
+ UNICODE_STRING* dosPath = &ffGetPeb()->ProcessParameters->CurrentDirectory.DosPath;
+ wchar_t driveRoot[] = { dosPath->Buffer[0], L':', L'\\', L'\0' };
+ hRoot = CreateFileW(
+ driveRoot,
+ FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ NULL,
+ OPEN_EXISTING,
+ FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS,
+ NULL);
+ if (hRoot == INVALID_HANDLE_VALUE) {
+ return false;
+ }
+ closeRoot = true;
+ ptr++; // skip leading '\'
+ }
+
+ while (true) {
+ wchar_t* token = wcschr(ptr, L'\\');
+ if (token == NULL) {
+ break;
+ }
+
+ // Skip empty path segments caused by duplicated '\'
+ if (token == ptr) {
+ ptr = token + 1;
+ continue;
+ }
+
+ HANDLE hNew = INVALID_HANDLE_VALUE;
+ IO_STATUS_BLOCK iosb = {};
+
+ NTSTATUS status = NtCreateFile(
+ &hNew,
+ FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE,
+ &(OBJECT_ATTRIBUTES) {
+ .Length = sizeof(OBJECT_ATTRIBUTES),
+ .RootDirectory = hRoot,
+ .ObjectName = &(UNICODE_STRING) {
+ .Buffer = ptr,
+ .Length = (USHORT) ((USHORT) (token - ptr) * sizeof(wchar_t)),
+ .MaximumLength = (USHORT) ((USHORT) (token - ptr) * sizeof(wchar_t)),
+ },
+ .Attributes = OBJ_CASE_INSENSITIVE,
+ },
+ &iosb,
+ NULL,
+ FILE_ATTRIBUTE_NORMAL,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ FILE_OPEN_IF,
+ FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
+ NULL,
+ 0);
+
+ if (!NT_SUCCESS(status)) {
+ if (closeRoot && hRoot != INVALID_HANDLE_VALUE) {
+ NtClose(hRoot);
+ }
+ return false;
+ }
+
+ if (closeRoot && hRoot != INVALID_HANDLE_VALUE) {
+ NtClose(hRoot);
+ }
+ hRoot = hNew;
+ closeRoot = true;
+
+ ptr = token + 1;
+ }
+
+ if (closeRoot && hRoot != INVALID_HANDLE_VALUE) {
+ NtClose(hRoot);
+ }
+
+ return true;
+}
+
+bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) {
+ wchar_t fileNameW[MAX_PATH];
+ ULONG len = 0;
+ if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) {
+ return false;
+ }
+
+ for (ULONG i = 0; i < len / sizeof(wchar_t); ++i) {
+ if (fileNameW[i] == L'/') {
+ fileNameW[i] = L'\\';
+ }
+ }
+
+ HANDLE FF_AUTO_CLOSE_FD handle = CreateFileW(fileNameW, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (handle == INVALID_HANDLE_VALUE) {
+ if (GetLastError() == ERROR_PATH_NOT_FOUND) {
+ if (!createSubfolders(fileNameW)) {
+ return false;
+ }
+ handle = CreateFileW(fileNameW, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (handle == INVALID_HANDLE_VALUE) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ DWORD written;
+ return !!WriteFile(handle, data, (DWORD) dataSize, &written, NULL);
+}
+
+static inline void readWithLength(HANDLE handle, FFstrbuf* buffer, uint32_t length) {
+ ffStrbufEnsureFree(buffer, length);
+ DWORD bytesRead = 0;
+ while (
+ length > 0 &&
+ ReadFile(handle, buffer->chars + buffer->length, length, &bytesRead, NULL) != FALSE &&
+ bytesRead > 0) {
+ buffer->length += (uint32_t) bytesRead;
+ length -= (uint32_t) bytesRead;
+ }
+}
+
+static inline void readUntilEOF(HANDLE handle, FFstrbuf* buffer) {
+ ffStrbufEnsureFree(buffer, 31);
+ uint32_t available = ffStrbufGetFree(buffer);
+ DWORD bytesRead = 0;
+ while (
+ ReadFile(handle, buffer->chars + buffer->length, available, &bytesRead, NULL) != FALSE &&
+ bytesRead > 0) {
+ buffer->length += (uint32_t) bytesRead;
+ if ((uint32_t) bytesRead == available) {
+ ffStrbufEnsureFree(buffer, buffer->allocated - 1); // Doubles capacity every round. -1 for the null byte.
+ }
+ available = ffStrbufGetFree(buffer);
+ }
+}
+
+bool ffAppendFDBuffer(HANDLE handle, FFstrbuf* buffer) {
+ FILE_STANDARD_INFORMATION fileInfo;
+ IO_STATUS_BLOCK iosb;
+ if (!NT_SUCCESS(NtQueryInformationFile(handle, &iosb, &fileInfo, sizeof(fileInfo), FileStandardInformation))) {
+ fileInfo.EndOfFile.QuadPart = 0;
+ }
+
+ if (fileInfo.EndOfFile.QuadPart > 0) {
+ readWithLength(handle, buffer, (uint32_t) fileInfo.EndOfFile.QuadPart);
+ } else {
+ readUntilEOF(handle, buffer);
+ }
+
+ buffer->chars[buffer->length] = '\0';
+
+ return buffer->length > 0;
+}
+
+HANDLE openatW(HANDLE dfd, const wchar_t* fileName, uint16_t fileNameLen, bool directory) {
+ assert(fileNameLen <= 0x7FFF);
+
+ HANDLE hFile;
+ IO_STATUS_BLOCK iosb = {};
+ if (!NT_SUCCESS(NtOpenFile(&hFile,
+ (directory ? FILE_LIST_DIRECTORY | FILE_TRAVERSE : FILE_READ_DATA | FILE_READ_EA) | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
+ &(OBJECT_ATTRIBUTES) {
+ .Length = sizeof(OBJECT_ATTRIBUTES),
+ .RootDirectory = dfd,
+ .ObjectName = &(UNICODE_STRING) {
+ .Buffer = (PWSTR) fileName,
+ .Length = fileNameLen * (USHORT) sizeof(wchar_t),
+ .MaximumLength = (fileNameLen + 1) * (USHORT) sizeof(wchar_t),
+ },
+ .Attributes = OBJ_CASE_INSENSITIVE,
+ },
+ &iosb,
+ FILE_SHARE_READ | (directory ? FILE_SHARE_WRITE | FILE_SHARE_DELETE : 0),
+ FILE_SYNCHRONOUS_IO_NONALERT | (directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE)))) {
+ return INVALID_HANDLE_VALUE;
+ }
+
+ return hFile;
+}
+
+HANDLE openat(HANDLE dfd, const char* fileName, int oflag) {
+ wchar_t fileNameW[MAX_PATH];
+ ULONG len;
+ if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) {
+ return INVALID_HANDLE_VALUE;
+ }
+ // Implies `fileNameW[len] = L'\0';` and `len` includes the null terminator
+ len /= sizeof(wchar_t); // convert from bytes to characters
+
+ for (uint32_t i = 0; i < len - 1; ++i) {
+ if (fileNameW[i] == L'/') {
+ fileNameW[i] = L'\\';
+ }
+ }
+
+ return openatW(dfd, fileNameW, (uint16_t) (len - 1), !!(oflag & O_DIRECTORY));
+}
+
+bool ffPathExpandEnv(const char* in, FFstrbuf* out) {
+ if (in[0] == '~') {
+ if ((in[1] == '/' || in[1] == '\\' || in[1] == '\0') && !ffStrContainsC(in, '%')) {
+ ffStrbufSet(out, &instance.state.platform.homeDir);
+ ffStrbufAppendS(out, in + 1);
+ return true;
+ }
+ }
+
+ wchar_t pathInW[MAX_PATH], pathOutW[MAX_PATH];
+ ULONG len = (ULONG) strlen(in);
+ if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathInW, (ULONG) sizeof(pathInW), &len, in, len))) {
+ return false;
+ }
+ len /= sizeof(wchar_t); // convert from bytes to characters
+
+ SIZE_T outLen; // in characters, including null terminator
+ if (!NT_SUCCESS(RtlExpandEnvironmentStrings(NULL, pathInW, len, pathOutW, ARRAY_SIZE(pathOutW), &outLen))) {
+ return false;
+ }
+
+ ffStrbufSetNWS(out, (uint32_t) outLen - 1, pathOutW);
+ return true;
+}
+
+bool ffSuppressIO(bool suppress) {
+#ifndef NDEBUG
+ if (instance.config.display.debugMode) {
+ return false;
+ }
+#endif
+
+ static bool init = false;
+ static HANDLE hOrigOut = INVALID_HANDLE_VALUE;
+ static HANDLE hOrigErr = INVALID_HANDLE_VALUE;
+ HANDLE hNullFile = ffGetNullFD();
+ static int fOrigOut = -1;
+ static int fOrigErr = -1;
+ static int fNullFile = -1;
+
+ if (!init) {
+ if (!suppress) {
+ return true;
+ }
+
+ hOrigOut = GetStdHandle(STD_OUTPUT_HANDLE);
+ hOrigErr = GetStdHandle(STD_ERROR_HANDLE);
+ fOrigOut = _dup(STDOUT_FILENO);
+ fOrigErr = _dup(STDERR_FILENO);
+ fNullFile = _open_osfhandle((intptr_t) hNullFile, 0);
+
+ init = true;
+ }
+ if (hNullFile == INVALID_HANDLE_VALUE || fNullFile == -1) {
+ return false;
+ }
+
+ fflush(stdout);
+ fflush(stderr);
+
+ SetStdHandle(STD_OUTPUT_HANDLE, suppress ? hNullFile : hOrigOut);
+ SetStdHandle(STD_ERROR_HANDLE, suppress ? hNullFile : hOrigErr);
+ _dup2(suppress ? fNullFile : fOrigOut, STDOUT_FILENO);
+ _dup2(suppress ? fNullFile : fOrigErr, STDERR_FILENO);
+
+ return true;
+}
+
+void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indentation, const char* folderName, bool pretty) {
+ uint32_t folderLength = folder->length;
+
+ if (pretty && folderName != NULL) {
+ for (uint8_t i = 0; i < indentation - 1; i++) {
+ fputs(" | ", stdout);
+ }
+ printf("%s/\n", folderName);
+ }
+
+ ffStrbufAppendC(folder, '*');
+ WIN32_FIND_DATAA entry;
+ HANDLE hFind = FindFirstFileA(folder->chars, &entry);
+ ffStrbufTrimRight(folder, '*');
+ if (hFind == INVALID_HANDLE_VALUE) {
+ return;
+ }
+
+ do {
+ if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+ if (ffStrEquals(entry.cFileName, ".") || ffStrEquals(entry.cFileName, "..")) {
+ continue;
+ }
+
+ ffStrbufSubstrBefore(folder, folderLength);
+ ffStrbufAppendS(folder, entry.cFileName);
+ ffStrbufAppendC(folder, '/');
+ listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry.cFileName, pretty);
+ ffStrbufSubstrBefore(folder, folderLength);
+ continue;
+ }
+
+ if (pretty) {
+ for (uint8_t i = 0; i < indentation; i++) {
+ fputs(" | ", stdout);
+ }
+ } else {
+ fputs(folder->chars + baseLength, stdout);
+ }
+
+ puts(entry.cFileName);
+ } while (FindNextFileA(hFind, &entry));
+ FindClose(hFind);
+}
+
+void ffListFilesRecursively(const char* path, bool pretty) {
+ FF_STRBUF_AUTO_DESTROY folder = ffStrbufCreateS(path);
+ ffStrbufEnsureEndsWithC(&folder, '/');
+ listFilesRecursively(folder.length, &folder, 0, NULL, pretty);
+}
+
+const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) {
+ HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
+ FF_AUTO_CLOSE_FD HANDLE hConin = INVALID_HANDLE_VALUE;
+ DWORD inputMode = 0;
+ bool hasInputMode = !!GetConsoleMode(hInput, &inputMode);
+ if (!hasInputMode) {
+ hConin = CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, NULL);
+ hInput = hConin;
+ hasInputMode = !!GetConsoleMode(hInput, &inputMode);
+ }
+ SetConsoleMode(hInput, 0);
+
+ FlushConsoleInputBuffer(hInput);
+
+ {
+ DWORD bytes = 0;
+ HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+ FF_AUTO_CLOSE_FD HANDLE hConout = INVALID_HANDLE_VALUE;
+ DWORD outputMode;
+ if (!GetConsoleMode(hOutput, &outputMode)) {
+ hConout = CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, NULL);
+ hOutput = hConout;
+ }
+ WriteFile(hOutput, request, (DWORD) strlen(request), &bytes, NULL);
+ }
+
+ while (true) {
+ if (NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 }) != STATUS_WAIT_0) {
+ SetConsoleMode(hInput, inputMode);
+ return "NtWaitForSingleObject() failed or timeout";
+ }
+
+ // Ignore all unexpected input events
+ INPUT_RECORD record;
+ DWORD len = 0;
+ if (!PeekConsoleInputW(hInput, &record, 1, &len)) {
+ break;
+ }
+
+ if (
+ record.EventType == KEY_EVENT &&
+ record.Event.KeyEvent.uChar.UnicodeChar != L'\r' &&
+ record.Event.KeyEvent.uChar.UnicodeChar != L'\n') {
+ break;
+ } else {
+ ReadConsoleInputW(hInput, &record, 1, &len);
+ }
+ }
+
+ va_list args;
+ va_start(args, format);
+
+ char buffer[1024];
+ uint32_t bytesRead = 0;
+
+ while (true) {
+ DWORD bytes = 0;
+ if (!ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, NULL) || bytes == 0) {
+ va_end(args);
+ return "ReadFile() failed";
+ }
+
+ bytesRead += bytes;
+ if (__builtin_expect(bytesRead >= sizeof(buffer) - 1, false)) {
+ va_end(args);
+ return "terminal response buffer overflow";
+ }
+ buffer[bytesRead] = '\0';
+
+ va_list cargs;
+ va_copy(cargs, args);
+ int ret = vsscanf(buffer, format, cargs);
+ va_end(cargs);
+
+ if (ret <= 0) {
+ va_end(args);
+ return "vsscanf(buffer, format, args) failed";
+ }
+ if (ret >= nParams) {
+ break;
+ }
+ }
+
+ if (hasInputMode) {
+ SetConsoleMode(hInput, inputMode);
+ }
+
+ va_end(args);
+
+ return NULL;
+}
+
+FFNativeFD ffGetNullFD(void) {
+ static FFNativeFD hNullFile = INVALID_HANDLE_VALUE;
+ if (hNullFile != INVALID_HANDLE_VALUE) {
+ return hNullFile;
+ }
+ hNullFile = CreateFileW(
+ L"NUL",
+ GENERIC_READ | GENERIC_WRITE,
+ FILE_SHARE_WRITE,
+ 0,
+ OPEN_EXISTING,
+ 0,
+ &(SECURITY_ATTRIBUTES) {
+ .nLength = sizeof(SECURITY_ATTRIBUTES),
+ .lpSecurityDescriptor = NULL,
+ .bInheritHandle = TRUE,
+ });
+ return hNullFile;
+}
+
+bool ffRemoveFile(const char* fileName) {
+ return DeleteFileA(fileName) != FALSE;
+}
diff --git a/src/common/impl/jsonconfig.c b/src/common/impl/jsonconfig.c
new file mode 100644
index 0000000..fccc56c
--- /dev/null
+++ b/src/common/impl/jsonconfig.c
@@ -0,0 +1,365 @@
+#include "fastfetch.h"
+#include "common/color.h"
+#include "common/jsonconfig.h"
+#include "common/printing.h"
+#include "common/io.h"
+#include "common/time.h"
+#include "common/strutil.h"
+#include "detection/version/version.h"
+#include "modules/modules.h"
+
+#include <assert.h>
+#include <ctype.h>
+#include <inttypes.h>
+
+bool ffJsonConfigParseModuleArgs(yyjson_val* key, yyjson_val* val, FFModuleArgs* moduleArgs) {
+ if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) {
+ return true;
+ }
+
+ if (unsafe_yyjson_equals_str(key, "key")) {
+ ffStrbufSetJsonVal(&moduleArgs->key, val);
+ return true;
+ } else if (unsafe_yyjson_equals_str(key, "format")) {
+ ffStrbufSetJsonVal(&moduleArgs->outputFormat, val);
+ return true;
+ } else if (unsafe_yyjson_equals_str(key, "outputColor")) {
+ ffOptionParseColor(yyjson_get_str(val), &moduleArgs->outputColor);
+ return true;
+ } else if (unsafe_yyjson_equals_str(key, "keyColor")) {
+ ffOptionParseColor(yyjson_get_str(val), &moduleArgs->keyColor);
+ return true;
+ } else if (unsafe_yyjson_equals_str(key, "keyWidth")) {
+ moduleArgs->keyWidth = (uint32_t) yyjson_get_uint(val);
+ return true;
+ } else if (unsafe_yyjson_equals_str(key, "keyIcon")) {
+ ffStrbufSetJsonVal(&moduleArgs->keyIcon, val);
+ return true;
+ }
+ return false;
+}
+
+void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFModuleArgs* moduleArgs) {
+ if (moduleArgs->key.length > 0) {
+ yyjson_mut_obj_add_strbuf(doc, module, "key", &moduleArgs->key);
+ }
+ if (moduleArgs->outputFormat.length > 0) {
+ yyjson_mut_obj_add_strbuf(doc, module, "format", &moduleArgs->outputFormat);
+ }
+ if (moduleArgs->outputColor.length > 0) {
+ yyjson_mut_obj_add_strbuf(doc, module, "outputColor", &moduleArgs->outputColor);
+ }
+ if (moduleArgs->keyColor.length > 0) {
+ yyjson_mut_obj_add_strbuf(doc, module, "keyColor", &moduleArgs->keyColor);
+ }
+ if (moduleArgs->keyWidth > 0) {
+ yyjson_mut_obj_add_uint(doc, module, "keyWidth", moduleArgs->keyWidth);
+ }
+ if (moduleArgs->keyIcon.length > 0) {
+ yyjson_mut_obj_add_strbuf(doc, module, "keyIcon", &moduleArgs->keyIcon);
+ }
+}
+
+const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]) {
+ if (yyjson_is_int(val)) {
+ int intVal = yyjson_get_int(val);
+
+ for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
+ if (intVal == pPair->value) {
+ *result = pPair->value;
+ return NULL;
+ }
+ }
+
+ return "Invalid enum integer";
+ } else if (yyjson_is_str(val)) {
+ const char* strVal = yyjson_get_str(val);
+ for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
+ if (ffStrEqualsIgnCase(strVal, pPair->key)) {
+ *result = pPair->value;
+ return NULL;
+ }
+ }
+
+ return "Invalid enum string";
+ } else {
+ return "Invalid enum value type; must be a string or integer";
+ }
+}
+
+static bool parseModuleJsonObject(const char* type, yyjson_val* jsonVal, yyjson_mut_doc* jsonDoc) {
+ if (!ffCharIsEnglishAlphabet(type[0])) {
+ return false;
+ }
+
+ for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(type[0]) - 'A']; *modules; ++modules) {
+ FFModuleBaseInfo* baseInfo = *modules;
+ if (ffStrEqualsIgnCase(type, baseInfo->name)) {
+ uint8_t optionBuf[FF_OPTION_MAX_SIZE];
+ baseInfo->initOptions(optionBuf);
+ if (jsonVal) {
+ baseInfo->parseJsonObject(optionBuf, jsonVal);
+ }
+ bool succeeded;
+ if (jsonDoc) {
+ yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root);
+ yyjson_mut_obj_add_str(jsonDoc, module, "type", baseInfo->name);
+ if (baseInfo->generateJsonResult) {
+ succeeded = baseInfo->generateJsonResult(optionBuf, jsonDoc, module);
+ } else {
+ yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unsupported for JSON format");
+ succeeded = false;
+ }
+ } else {
+ succeeded = baseInfo->printModule(optionBuf);
+ }
+ baseInfo->destroyOptions(optionBuf);
+ return succeeded;
+ }
+ }
+
+ if (jsonDoc) {
+ yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root);
+ yyjson_mut_obj_add_strcpy(jsonDoc, module, "type", type);
+ yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unknown module type");
+ } else {
+ FFModuleArgs moduleArgs;
+ ffOptionInitModuleArg(&moduleArgs, "");
+ ffPrintError(type, 0, &moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown module type");
+ ffOptionDestroyModuleArg(&moduleArgs);
+ }
+ return false;
+}
+
+static void prepareModuleJsonObject(const char* type, yyjson_val* module) {
+ switch (type[0]) {
+ #if !FF_MODULE_DISABLE_CPUUSAGE
+ case 'c':
+ case 'C': {
+ if (ffStrEqualsIgnCase(type, FF_CPUUSAGE_MODULE_NAME)) {
+ ffPrepareCPUUsage();
+ } else if (ffStrEqualsIgnCase(type, FF_COMMAND_MODULE_NAME)) {
+ FF_A_CLEANUP(ffDestroyCommandOptions) FFCommandOptions options;
+ ffInitCommandOptions(&options);
+ if (module) {
+ ffCommandModuleInfo.parseJsonObject(&options, module);
+ }
+ ffPrepareCommand(&options);
+ }
+ break;
+ }
+ #endif
+
+ #if !FF_MODULE_DISABLE_DISKIO
+ case 'd':
+ case 'D': {
+ if (ffStrEqualsIgnCase(type, FF_DISKIO_MODULE_NAME)) {
+ FF_A_CLEANUP(ffDestroyDiskIOOptions) FFDiskIOOptions options;
+ ffInitDiskIOOptions(&options);
+ if (module) {
+ ffDiskIOModuleInfo.parseJsonObject(&options, module);
+ }
+ ffPrepareDiskIO(&options);
+ }
+ break;
+ }
+ #endif
+
+ #if !FF_MODULE_DISABLE_NETIO
+ case 'n':
+ case 'N': {
+ if (ffStrEqualsIgnCase(type, FF_NETIO_MODULE_NAME)) {
+ FF_A_CLEANUP(ffDestroyNetIOOptions) FFNetIOOptions options;
+ ffInitNetIOOptions(&options);
+ if (module) {
+ ffNetIOModuleInfo.parseJsonObject(&options, module);
+ }
+ ffPrepareNetIO(&options);
+ }
+ break;
+ }
+ #endif
+
+ #if !FF_MODULE_DISABLE_PUBLICIP
+ case 'p':
+ case 'P': {
+ if (ffStrEqualsIgnCase(type, FF_PUBLICIP_MODULE_NAME)) {
+ FF_A_CLEANUP(ffDestroyPublicIpOptions) FFPublicIPOptions options;
+ ffInitPublicIpOptions(&options);
+ if (module) {
+ ffPublicIPModuleInfo.parseJsonObject(&options, module);
+ }
+ ffPreparePublicIp(&options);
+ }
+ break;
+ }
+ #endif
+
+ #if !FF_MODULE_DISABLE_WEATHER
+ case 'w':
+ case 'W': {
+ if (ffStrEqualsIgnCase(type, FF_WEATHER_MODULE_NAME)) {
+ FF_A_CLEANUP(ffDestroyWeatherOptions) FFWeatherOptions options;
+ ffInitWeatherOptions(&options);
+ if (module) {
+ ffWeatherModuleInfo.parseJsonObject(&options, module);
+ }
+ ffPrepareWeather(&options);
+ }
+ break;
+ }
+ #endif
+ }
+}
+
+static bool matchesJsonArray(const char* str, yyjson_val* val) {
+ assert(val);
+
+ if (unsafe_yyjson_is_str(val)) {
+ return ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(val));
+ }
+
+ if (!unsafe_yyjson_is_arr(val)) {
+ return false;
+ }
+
+ size_t idx, max;
+ yyjson_val* item;
+ yyjson_arr_foreach (val, idx, max, item) {
+ if (yyjson_is_str(item) && ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(item))) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static const char* printJsonConfig(FFdata* data, bool prepare) {
+ yyjson_mut_doc* jsonDoc = data->resultDoc;
+ yyjson_val* const root = yyjson_doc_get_root(data->configDoc);
+ assert(root);
+
+ if (!yyjson_is_obj(root)) {
+ return "Invalid JSON config format. Root value must be an object";
+ }
+
+ yyjson_val* modules = yyjson_obj_get(root, "modules");
+ if (!modules) {
+ return NULL;
+ }
+ if (!yyjson_is_arr(modules)) {
+ return "Property 'modules' must be an array of strings or objects";
+ }
+
+ bool succeeded = true;
+ int32_t thres = instance.config.display.stat;
+ yyjson_val* item;
+ size_t idx, max;
+ yyjson_arr_foreach (modules, idx, max, item) {
+ double ms = 0;
+ if (!prepare && thres >= 0) {
+ ms = ffTimeGetTick();
+ }
+
+ yyjson_val* module = item;
+ const char* type = yyjson_get_str(module);
+ if (type) {
+ module = NULL;
+ } else if (yyjson_is_obj(module)) {
+ yyjson_val* conditions = yyjson_obj_get(module, "condition");
+ if (conditions) {
+ if (!yyjson_is_obj(conditions)) {
+ return "Property 'condition' must be an object";
+ }
+
+ yyjson_val* system = yyjson_obj_get(conditions, "system");
+ if (system && !matchesJsonArray(ffVersionResult.sysName, system)) {
+ continue;
+ }
+
+ system = yyjson_obj_get(conditions, "!system");
+ if (system && matchesJsonArray(ffVersionResult.sysName, system)) {
+ continue;
+ }
+
+ yyjson_val* arch = yyjson_obj_get(conditions, "arch");
+ if (arch && !matchesJsonArray(ffVersionResult.architecture, arch)) {
+ continue;
+ }
+
+ arch = yyjson_obj_get(conditions, "!arch");
+ if (arch && matchesJsonArray(ffVersionResult.architecture, arch)) {
+ continue;
+ }
+
+ yyjson_val* previousSucceeded = yyjson_obj_get(conditions, "succeeded");
+ if (previousSucceeded && !unsafe_yyjson_is_null(previousSucceeded)) {
+ if (!unsafe_yyjson_is_bool(previousSucceeded)) {
+ return "Property 'succeeded' in 'condition' must be a boolean";
+ }
+ if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) {
+ continue;
+ }
+ }
+ }
+
+ type = yyjson_get_str(yyjson_obj_get(module, "type"));
+ if (!type) {
+ return "module object must contain a \"type\" key ( case sensitive )";
+ }
+ if (yyjson_obj_size(module) == 1) { // contains only Property type
+ module = NULL;
+ }
+ } else {
+ return "modules must be an array of strings or objects";
+ }
+
+ if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, type, ':')) {
+ continue;
+ }
+
+ if (prepare) {
+ prepareModuleJsonObject(type, module);
+ } else {
+ succeeded = parseModuleJsonObject(type, module, jsonDoc);
+ }
+
+ if (!prepare && thres >= 0) {
+ ms = ffTimeGetTick() - ms;
+ if (jsonDoc) {
+ yyjson_mut_val* moduleJson = yyjson_mut_arr_get_last(jsonDoc->root);
+ yyjson_mut_obj_add_real(jsonDoc, moduleJson, "stat", ms);
+ } else {
+ char str[64];
+ int len = snprintf(str, sizeof str, "%.3fms", ms);
+ if (thres > 0) {
+ snprintf(str, sizeof str, "\e[%sm%.3fms\e[m", (ms <= thres ? FF_COLOR_FG_GREEN : ms <= 2 * thres ? FF_COLOR_FG_YELLOW
+ : FF_COLOR_FG_RED),
+ ms);
+ }
+ printf("\e7\e[1A\e[9999999C\e[%dD%s\e8", len - 1, str); // Save; Up 1; Right 9999999; Left <len - 1>; Print <str>; Load
+ }
+ }
+
+#if defined(_WIN32)
+ if (!instance.config.display.noBuffer && !jsonDoc) {
+ fflush(stdout);
+ }
+#endif
+ }
+
+ return NULL;
+}
+
+void ffPrintJsonConfig(FFdata* data, bool prepare) {
+ yyjson_mut_doc* jsonDoc = data->resultDoc;
+ const char* error = printJsonConfig(data, prepare);
+ if (error) {
+ if (jsonDoc) {
+ yyjson_mut_val* obj = yyjson_mut_obj(jsonDoc);
+ yyjson_mut_obj_add_str(jsonDoc, obj, "error", error);
+ yyjson_mut_doc_set_root(jsonDoc, obj);
+ } else {
+ ffPrintError("JsonConfig", 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "%s", error);
+ }
+ }
+}
diff --git a/src/common/impl/kmod_apple.c b/src/common/impl/kmod_apple.c
new file mode 100644
index 0000000..82edc49
--- /dev/null
+++ b/src/common/impl/kmod_apple.c
@@ -0,0 +1,12 @@
+#include "common/kmod.h"
+#include "common/apple/cf_helpers.h"
+#include <IOKit/kext/KextManager.h>
+#include <CoreFoundation/CoreFoundation.h>
+
+bool ffKmodLoaded(const char* modName) {
+ FF_CFTYPE_AUTO_RELEASE CFStringRef name = CFStringCreateWithCString(kCFAllocatorDefault, modName, kCFStringEncodingUTF8);
+ FF_CFTYPE_AUTO_RELEASE CFArrayRef identifiers = CFArrayCreate(kCFAllocatorDefault, (const void**) &name, 1, &kCFTypeArrayCallBacks);
+ FF_CFTYPE_AUTO_RELEASE CFArrayRef keys = CFArrayCreate(kCFAllocatorDefault, NULL, 0, NULL);
+ FF_CFTYPE_AUTO_RELEASE CFDictionaryRef kextInfo = KextManagerCopyLoadedKextInfo(identifiers, keys);
+ return CFDictionaryContainsKey(kextInfo, name);
+}
diff --git a/src/common/impl/kmod_bsd.c b/src/common/impl/kmod_bsd.c
new file mode 100644
index 0000000..996d4ab
--- /dev/null
+++ b/src/common/impl/kmod_bsd.c
@@ -0,0 +1,7 @@
+#include "common/kmod.h"
+#include <sys/param.h>
+#include <sys/module.h>
+
+bool ffKmodLoaded(const char* modName) {
+ return modfind(modName) >= 0;
+}
diff --git a/src/common/impl/kmod_linux.c b/src/common/impl/kmod_linux.c
new file mode 100644
index 0000000..204ab35
--- /dev/null
+++ b/src/common/impl/kmod_linux.c
@@ -0,0 +1,25 @@
+#include "common/kmod.h"
+#include "common/io.h"
+
+bool ffKmodLoaded(const char* modName) {
+ static FFstrbuf modules;
+ if (modules.chars == NULL) {
+ ffStrbufInitS(&modules, "\n");
+ ffAppendFileBuffer("/proc/modules", &modules);
+ }
+
+ if (modules.length == 0) {
+ return false;
+ }
+
+ uint32_t len = (uint32_t) strlen(modName);
+ if (len > 250) {
+ return false;
+ }
+
+ char temp[256];
+ temp[0] = '\n';
+ memcpy(temp + 1, modName, len);
+ temp[1 + len] = ' ';
+ return memmem(modules.chars, modules.length, temp, len + 2) != NULL;
+}
diff --git a/src/common/impl/kmod_nbsd.c b/src/common/impl/kmod_nbsd.c
new file mode 100644
index 0000000..f1b450a
--- /dev/null
+++ b/src/common/impl/kmod_nbsd.c
@@ -0,0 +1,40 @@
+#include "common/kmod.h"
+#include "common/strutil.h"
+
+#include <sys/module.h>
+#include <sys/param.h>
+
+typedef struct FF_A_PACKED FFNbsdModList {
+ int len;
+ modstat_t mods[];
+} FFNbsdModList;
+
+bool ffKmodLoaded(const char* modName) {
+ static FFNbsdModList* list = NULL;
+
+ if (list == NULL) {
+ struct iovec iov = {};
+
+ for (size_t len = 8192;; len = iov.iov_len) {
+ iov.iov_len = len;
+ iov.iov_base = realloc(iov.iov_base, len);
+ if (modctl(MODCTL_STAT, &iov) < 0) {
+ free(iov.iov_base);
+ return true; // ignore errors
+ }
+
+ if (len >= iov.iov_len) {
+ break;
+ }
+ }
+ list = (FFNbsdModList*) iov.iov_base;
+ }
+
+ for (int i = 0; i < list->len; i++) {
+ if (ffStrEquals(list->mods[i].ms_name, modName)) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/common/impl/kmod_nosupport.c b/src/common/impl/kmod_nosupport.c
new file mode 100644
index 0000000..c254ba7
--- /dev/null
+++ b/src/common/impl/kmod_nosupport.c
@@ -0,0 +1,5 @@
+#include "common/kmod.h"
+
+bool ffKmodLoaded(FF_A_UNUSED const char* modName) {
+ return true; // Don't generate kernel module related errors
+}
diff --git a/src/common/impl/kmod_sunos.c b/src/common/impl/kmod_sunos.c
new file mode 100644
index 0000000..da392e2
--- /dev/null
+++ b/src/common/impl/kmod_sunos.c
@@ -0,0 +1,23 @@
+#include "common/kmod.h"
+#include "common/strutil.h"
+
+#include <sys/modctl.h>
+#include <errno.h>
+
+bool ffKmodLoaded(const char* modName) {
+ struct modinfo modinfo = {
+ .mi_id = -1,
+ .mi_nextid = -1,
+ .mi_info = MI_INFO_ALL,
+ };
+
+ for (int id = -1; modctl(MODINFO, id, &modinfo) == 0; id = modinfo.mi_id) {
+ modinfo.mi_name[MODMAXNAMELEN - 1] = '\0';
+
+ if (ffStrEquals(modinfo.mi_name, modName)) {
+ return true;
+ }
+ }
+
+ return !(errno == EINVAL || errno == ENOENT);
+}
diff --git a/src/common/impl/kmod_windows.c b/src/common/impl/kmod_windows.c
new file mode 100644
index 0000000..081e5a7
--- /dev/null
+++ b/src/common/impl/kmod_windows.c
@@ -0,0 +1,28 @@
+#include "common/kmod.h"
+#include "common/windows/nt.h"
+#include "common/mallocHelper.h"
+#include "common/strutil.h"
+
+bool ffKmodLoaded(const char* modName) {
+ ULONG bufferSize = 0;
+ NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &bufferSize);
+ if (bufferSize == 0) {
+ return true; // ignore errors
+ }
+
+ FF_AUTO_FREE RTL_PROCESS_MODULES* buffer = malloc(bufferSize);
+
+ if (!NT_SUCCESS(NtQuerySystemInformation(SystemModuleInformation, buffer, bufferSize, &bufferSize))) {
+ return true; // ignore errors
+ }
+
+ for (ULONG i = 0; i < buffer->NumberOfModules; i++) {
+ const char* name = (const char*) buffer->Modules[i].FullPathName + buffer->Modules[i].OffsetToFileName;
+
+ if (ffStrEqualsIgnCase(name, modName)) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/src/common/impl/library.c b/src/common/impl/library.c
new file mode 100644
index 0000000..6f96e96
--- /dev/null
+++ b/src/common/impl/library.c
@@ -0,0 +1,180 @@
+#include "fastfetch.h"
+#include "common/library.h"
+#include "common/debug.h"
+
+#if _WIN32
+ #include "common/windows/nt.h"
+ #include <errno.h>
+ #include <ntstatus.h>
+#endif
+
+#ifndef FF_DISABLE_DLOPEN
+
+ #include <stdarg.h>
+
+ // Clang doesn't define __SANITIZE_ADDRESS__ but defines __has_feature(address_sanitizer)
+ #if !defined(__SANITIZE_ADDRESS__) && defined(__has_feature)
+ #if __has_feature(address_sanitizer)
+ #define __SANITIZE_ADDRESS__
+ #endif
+ #endif
+
+ #ifndef FF_DLOPEN_FLAGS
+ #ifdef __SANITIZE_ADDRESS__
+ #define FF_DLOPEN_FLAGS RTLD_LAZY | RTLD_NODELETE
+ #else
+ #define FF_DLOPEN_FLAGS RTLD_LAZY
+ #endif
+ #endif
+
+static void* libraryLoad(const char* path, int maxVersion) {
+ void* result = dlopen(path, FF_DLOPEN_FLAGS);
+
+ #if _WIN32
+
+ // libX.dll.1 never exists on Windows, while libX-1.dll may exist
+ FF_UNUSED(maxVersion)
+
+ if (result != NULL) {
+ return result;
+ }
+
+ uint32_t pathLen = ffStrbufLastIndexC(&instance.state.platform.exePath, '/');
+ if (pathLen == instance.state.platform.exePath.length) {
+ return result;
+ }
+
+ char absPath[MAX_PATH * 2];
+ strcpy(mempcpy(absPath, instance.state.platform.exePath.chars, pathLen + 1), path);
+ return dlopen(absPath, FF_DLOPEN_FLAGS);
+
+ #else
+
+ if (result == NULL) {
+ FF_DEBUG("dlopen(\"%s\"): %s", path, dlerror());
+ }
+
+ if (result != NULL || maxVersion < 0) {
+ return result;
+ }
+
+ FF_STRBUF_AUTO_DESTROY pathbuf = ffStrbufCreateA(64);
+ ffStrbufAppendS(&pathbuf, path);
+ ffStrbufAppendC(&pathbuf, '.');
+
+ for (int i = maxVersion; i >= 0; --i) {
+ uint32_t originalLength = pathbuf.length;
+ ffStrbufAppendSInt(&pathbuf, i);
+
+ result = dlopen(pathbuf.chars, FF_DLOPEN_FLAGS);
+ if (result != NULL) {
+ break;
+ } else {
+ FF_DEBUG("dlopen(\"%s\"): %s", pathbuf.chars, dlerror());
+ }
+
+ ffStrbufSubstrBefore(&pathbuf, originalLength);
+ }
+
+ #endif
+
+ return result;
+}
+
+void* ffLibraryLoad(const char* path, int maxVersion, ...) {
+ void* result = libraryLoad(path, maxVersion);
+
+ if (!result) {
+ va_list defaultNames;
+ va_start(defaultNames, maxVersion);
+
+ do {
+ const char* pathRest = va_arg(defaultNames, const char*);
+ if (pathRest == NULL) {
+ break;
+ }
+
+ int maxVersionRest = va_arg(defaultNames, int);
+ result = libraryLoad(pathRest, maxVersionRest);
+ } while (!result);
+
+ va_end(defaultNames);
+ }
+
+ return result;
+}
+
+#endif
+
+#if _WIN32
+
+void* dlopen(const char* path, FF_A_UNUSED int mode) {
+ wchar_t pathW[MAX_PATH + 1];
+ ULONG pathWBytes = 0;
+
+ NTSTATUS status = RtlUTF8ToUnicodeN(pathW, sizeof(pathW), &pathWBytes, path, (uint32_t) strlen(path) + 1);
+ if (!NT_SUCCESS(status)) {
+ FF_DEBUG("RtlUTF8ToUnicodeN failed for path %s with status 0x%08lX: %s", path, status, ffDebugNtStatus(status));
+ return NULL;
+ }
+
+ PVOID module = NULL;
+ status = LdrLoadDll(NULL, NULL, &(UNICODE_STRING) {
+ .Length = (USHORT) (pathWBytes - sizeof(wchar_t)), // Exclude null terminator
+ .MaximumLength = (USHORT) pathWBytes,
+ .Buffer = pathW,
+ },
+ &module);
+
+ if (!NT_SUCCESS(status)) {
+ FF_DEBUG("LdrLoadDll failed for path %s with status 0x%08lX: %s", path, status, ffDebugNtStatus(status));
+ return NULL;
+ }
+
+ return module;
+}
+
+int dlclose(void* handle) {
+ NTSTATUS status = LdrUnloadDll(handle);
+ if (!NT_SUCCESS(status)) {
+ FF_DEBUG("LdrUnloadDll failed for handle %p with status 0x%08lX: %s", handle, status, ffDebugNtStatus(status));
+ return -1;
+ }
+ return 0;
+}
+
+void* dlsym(void* handle, const char* symbol) {
+ void* address;
+ USHORT symbolBytes = (USHORT) (strlen(symbol) + 1);
+ NTSTATUS status = LdrGetProcedureAddress(handle, &(ANSI_STRING) {
+ .Length = symbolBytes - sizeof(char),
+ .MaximumLength = symbolBytes,
+ .Buffer = (char*) symbol,
+ },
+ 0,
+ &address);
+ if (!NT_SUCCESS(status)) {
+ FF_DEBUG("LdrGetProcedureAddress failed for symbol %s with status 0x%08lX: %s", symbol, status, ffDebugNtStatus(status));
+ return NULL;
+ }
+ return address;
+}
+
+void* ffLibraryGetModule(const wchar_t* libraryFileName) {
+ assert(libraryFileName != NULL && "Use \"ffGetPeb()->ImageBaseAddress\" instead");
+
+ void* module = NULL;
+ USHORT libraryFileNameBytes = (USHORT) (wcslen(libraryFileName) * sizeof(wchar_t) + sizeof(wchar_t));
+ NTSTATUS status = LdrGetDllHandle(NULL, NULL, &(UNICODE_STRING) {
+ .Length = libraryFileNameBytes - sizeof(wchar_t),
+ .MaximumLength = libraryFileNameBytes,
+ .Buffer = (wchar_t*) libraryFileName,
+ },
+ &module);
+ if (!NT_SUCCESS(status)) {
+ FF_DEBUG("LdrGetDllHandle failed for library %ls with status 0x%08lX: %s", libraryFileName, status, ffDebugNtStatus(status));
+ return NULL;
+ }
+ return module;
+}
+#endif
diff --git a/src/common/impl/lua.c b/src/common/impl/lua.c
new file mode 100644
index 0000000..ffc1d83
--- /dev/null
+++ b/src/common/impl/lua.c
@@ -0,0 +1,233 @@
+#if FF_HAVE_LUA
+
+ #include "common/lua.h"
+ #include "common/mallocHelper.h"
+
+struct FFLuaData luaData;
+
+static yyjson_mut_val* lua2yyjson(lua_State* L, int idx, yyjson_mut_doc* doc, int depth) {
+ if (__builtin_expect(depth > 15, false)) {
+ yyjson_mut_doc_free(doc);
+ lua_pushlstring(
+ L, "yyjson: recursion depth exceeded; possible circular reference", strlen("yyjson: recursion depth exceeded; possible circular reference"));
+ lua_error(L); // noreturn
+ __builtin_unreachable();
+ }
+
+ if (idx < 0) {
+ idx = lua_gettop(L) + idx + 1;
+ }
+ int type = lua_type(L, idx);
+
+ switch (type) {
+ case LUA_TNIL:
+ return yyjson_mut_null(doc);
+
+ case LUA_TBOOLEAN:
+ return yyjson_mut_bool(doc, lua_toboolean(L, idx));
+
+ case LUA_TNUMBER: {
+ if (lua_isinteger(L, idx)) {
+ lua_Integer i = lua_tointeger(L, idx);
+ return yyjson_mut_sint(doc, (int64_t) i);
+ } else {
+ return yyjson_mut_real(doc, lua_tonumber(L, idx));
+ }
+ }
+
+ case LUA_TSTRING: {
+ size_t len;
+ const char* str = lua_tolstring(L, idx, &len);
+ return yyjson_mut_strncpy(doc, str, len);
+ }
+
+ case LUA_TTABLE: {
+ lua_Unsigned len = lua_rawlen(L, idx);
+ int is_array = 1;
+
+ if (len == 0) {
+ lua_pushnil(L);
+ is_array = 0;
+ if (lua_next(L, idx) != 0) {
+ lua_pop(L, 2);
+ }
+ } else {
+ lua_pushnil(L);
+ while (lua_next(L, idx) != 0) {
+ if (lua_type(L, -2) != LUA_TNUMBER) {
+ is_array = 0;
+ lua_pop(L, 2);
+ break;
+ }
+ int isnum = false;
+ lua_Integer k = lua_tointegerx(L, -2, &isnum);
+ if (!isnum || (lua_Unsigned) k > len) {
+ is_array = 0;
+ lua_pop(L, 2);
+ break;
+ }
+ lua_pop(L, 1);
+ }
+ }
+
+ if (is_array) {
+ yyjson_mut_val* arr = yyjson_mut_arr(doc);
+ for (lua_Unsigned i = 1; i <= len; i++) {
+ lua_rawgeti(L, idx, (lua_Integer) i);
+ yyjson_mut_val* val = lua2yyjson(L, -1, doc, depth + 1);
+ yyjson_mut_arr_append(arr, val);
+ lua_pop(L, 1);
+ }
+ return arr;
+ } else {
+ yyjson_mut_val* obj = yyjson_mut_obj(doc);
+ lua_pushnil(L);
+ while (lua_next(L, idx) != 0) {
+ size_t klen;
+ const char* key_str = luaL_tolstring(L, -2, &klen);
+ yyjson_mut_val* key = yyjson_mut_strncpy(doc, key_str, klen);
+ lua_pop(L, 1);
+
+ yyjson_mut_val* val = lua2yyjson(L, -1, doc, depth + 1);
+ yyjson_mut_obj_add(obj, key, val);
+ lua_pop(L, 1);
+ }
+ return obj;
+ }
+ }
+
+ default:
+ return yyjson_mut_null(doc);
+ }
+}
+
+static int yyjsonEncode(lua_State* L) {
+ luaL_checkany(L, 1);
+
+ bool pretty = false;
+ if (lua_isboolean(L, 2)) {
+ pretty = (int) lua_toboolean(L, 2);
+ }
+
+ yyjson_mut_doc* doc = yyjson_mut_doc_new(NULL);
+ if (__builtin_expect(!doc, false)) {
+ lua_pushlstring(L, "yyjson: yyjson_mut_doc_new() failed", strlen("yyjson: yyjson_mut_doc_new() failed"));
+ return lua_error(L);
+ }
+
+ yyjson_mut_val* root = lua2yyjson(L, 1, doc, 0);
+ yyjson_mut_doc_set_root(doc, root);
+
+ size_t jsonLen;
+ yyjson_write_err err = {};
+ FF_AUTO_FREE const char* jsonStr = yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INF_AND_NAN | (pretty ? YYJSON_WRITE_PRETTY_TWO_SPACES : 0), NULL, &jsonLen, &err);
+
+ if (__builtin_expect(jsonStr != NULL, true)) {
+ lua_pushlstring(L, jsonStr, jsonLen);
+ yyjson_mut_doc_free(doc);
+ return 1;
+ } else {
+ yyjson_mut_doc_free(doc);
+ {
+ FF_STRBUF_AUTO_DESTROY errBuf = ffStrbufCreateStatic("yyjson: yyjson_mut_write_opts() failed: ");
+ ffStrbufAppendS(&errBuf, err.msg);
+ lua_pushlstring(L, errBuf.chars, errBuf.length);
+ }
+
+ return lua_error(L); // longjmp
+ }
+}
+
+const char* ffLuaLoadState(void) {
+ if (luaData.inited) {
+ if (luaData.L == NULL) {
+ return "Lua library is not available";
+ }
+ return NULL;
+ }
+
+ luaData.inited = true;
+ // clang-format off
+ #ifdef _WIN32
+ #define FF_LOAD_LIBLUA(version) FF_LIBRARY_LOAD_MESSAGE(liblua, \
+ "lua5" #version FF_LIBRARY_EXTENSION, 0)
+ #else
+ #define FF_LOAD_LIBLUA(version) FF_LIBRARY_LOAD_MESSAGE(liblua, \
+ "liblua5." #version FF_LIBRARY_EXTENSION, 0, \
+ "liblua-5." #version FF_LIBRARY_EXTENSION, 0, \
+ "liblua5." #version FF_LIBRARY_EXTENSION ".5." #version, 0)
+ #endif
+ // clang-format on
+ #if LUA_VERSION_NUM == 505
+ FF_LOAD_LIBLUA(5)
+ #elif LUA_VERSION_NUM == 504
+ FF_LOAD_LIBLUA(4)
+ #elif LUA_VERSION_NUM == 503
+ FF_LOAD_LIBLUA(3)
+ #else
+ #error "Unsupported Lua version"
+ #endif
+ #undef FF_LOAD_LIBLUA
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaL_newstate)
+ #if LUA_VERSION_NUM >= 505
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaL_openselectedlibs)
+ #else
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaL_requiref)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaopen_base)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaopen_math)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaopen_string)
+ FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaopen_table)
+ #endif
+
+ #if !FF_DISABLE_DLOPEN
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, luaL_checkany)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, luaL_loadbufferx)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, luaL_tolstring)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_callk)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_createtable)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_error)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_gettop)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_isinteger)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_next)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pcallk)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushboolean)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushcclosure)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushinteger)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushlstring)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushnil)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushnumber)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushvalue)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_rawgeti)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_rawlen)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_setfield)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_setglobal)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_seti)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_settop)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_toboolean)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_tointegerx)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_tolstring)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_tonumberx)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_type)
+ #endif
+
+ lua_State* L = ffluaL_newstate();
+ if (L == NULL) {
+ return "luaL_newstate() failed";
+ }
+ #if LUA_VERSION_NUM >= 505
+ ffluaL_openselectedlibs(L, LUA_GLIBK | LUA_MATHLIBK | LUA_STRLIBK | LUA_TABLIBK, 0);
+ #else
+ ffluaL_requiref(L, LUA_GNAME, ffluaopen_base, 1);
+ ffluaL_requiref(L, LUA_MATHLIBNAME, ffluaopen_math, 1);
+ ffluaL_requiref(L, LUA_STRLIBNAME, ffluaopen_string, 1);
+ ffluaL_requiref(L, LUA_TABLIBNAME, ffluaopen_table, 1);
+ lua_settop(L, 0);
+ #endif
+ lua_pushcfunction(L, yyjsonEncode);
+ lua_setglobal(L, "json_encode");
+ luaData.L = L;
+ liblua = NULL; // don't close lua
+ return NULL;
+}
+
+#endif
diff --git a/src/common/impl/memrchr.c b/src/common/impl/memrchr.c
new file mode 100644
index 0000000..aaf97e6
--- /dev/null
+++ b/src/common/impl/memrchr.c
@@ -0,0 +1,21 @@
+#include "common/memrchr.h"
+#include <stddef.h>
+#include <stdint.h>
+
+void* memrchr(const void* s, int c, size_t n) {
+ if (n == 0) {
+ return NULL;
+ }
+
+ const uint8_t uc = (uint8_t) c;
+
+ const uint8_t* p = (const uint8_t*) s + n;
+
+ while (n--) {
+ if (*--p == uc) {
+ return (void*) p;
+ }
+ }
+
+ return NULL;
+}
diff --git a/src/common/impl/netif.c b/src/common/impl/netif.c
new file mode 100644
index 0000000..1525a18
--- /dev/null
+++ b/src/common/impl/netif.c
@@ -0,0 +1,22 @@
+#include "common/netif.h"
+
+#ifndef _WIN32
+ #include <net/if.h>
+ #include <netinet/in.h>
+#endif
+
+const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV4(void) {
+ static FFNetifDefaultRouteResult result;
+ if (result.status == FF_NETIF_UNINITIALIZED) {
+ result.status = ffNetifGetDefaultRouteImplV4(&result) ? FF_NETIF_OK : FF_NETIF_INVALID;
+ }
+ return &result;
+}
+
+const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV6(void) {
+ static FFNetifDefaultRouteResult result;
+ if (result.status == FF_NETIF_UNINITIALIZED) {
+ result.status = ffNetifGetDefaultRouteImplV6(&result) ? FF_NETIF_OK : FF_NETIF_INVALID;
+ }
+ return &result;
+}
diff --git a/src/common/impl/netif_apple.c b/src/common/impl/netif_apple.c
new file mode 100644
index 0000000..2d42735
--- /dev/null
+++ b/src/common/impl/netif_apple.c
@@ -0,0 +1,218 @@
+#include "common/netif.h"
+#include "common/io.h"
+
+#include <net/if.h>
+#include <net/if_dl.h>
+#include <net/route.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#define ROUNDUP2(a, n) ((a) > 0 ? (1 + (((a) - 1U) | ((n) - 1))) : (n))
+
+#if __APPLE__
+ // https://github.com/apple-oss-distributions/network_cmds/blob/8f38231438e6a4d16ef8015e97e12c2c05105644/rtsol.tproj/if.c#L243
+ #define ROUNDUP(a) ROUNDUP2((a), sizeof(uint32_t))
+#elif __sun
+ // https://github.com/illumos/illumos-gate/blob/95b8c88950fa7b19af46bc63230137cf96b0bff7/usr/src/cmd/cmd-inet/usr.sbin/route.c#L339
+ #define ROUNDUP(a) ROUNDUP2((a), sizeof(long))
+#else
+ #error unknown platform
+#endif
+
+static struct sockaddr*
+get_rt_address(struct rt_msghdr* rtm, int desired) {
+ struct sockaddr* sa = (struct sockaddr*) (rtm + 1);
+
+ for (int i = 0; i < RTAX_MAX; i++) {
+ if (rtm->rtm_addrs & (1 << i)) {
+ if ((1 << i) == desired) {
+ return sa;
+ }
+
+#ifndef __sun
+ uint32_t salen = sa->sa_len;
+#else
+ uint32_t salen;
+ // https://github.com/illumos/illumos-gate/blob/95b8c88950fa7b19af46bc63230137cf96b0bff7/usr/src/cmd/cmd-inet/usr.sbin/route.c#L2941
+ switch (sa->sa_family) {
+ case AF_INET:
+ salen = sizeof(struct sockaddr_in);
+ break;
+ case AF_LINK:
+ salen = sizeof(struct sockaddr_dl);
+ break;
+ case AF_INET6:
+ salen = sizeof(struct sockaddr_in6);
+ break;
+ default:
+ salen = sizeof(struct sockaddr);
+ break;
+ }
+#endif
+ sa = (struct sockaddr*) (ROUNDUP(salen) + (char*) sa);
+ }
+ }
+ return NULL;
+}
+
+bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) {
+ // https://github.com/hashPirate/copenheimer-masscan-fork/blob/36f1ed9f7b751a7dccd5ed27874e2e703db7d481/src/rawsock-getif.c#L104
+
+ FF_AUTO_CLOSE_FD int pfRoute = socket(PF_ROUTE, SOCK_RAW, AF_INET);
+ if (pfRoute < 0) {
+ return false;
+ }
+
+ {
+ struct timeval timeout = { 1, 0 };
+ setsockopt(pfRoute, SOL_SOCKET, SO_RCVTIMEO, (char*) &timeout, sizeof(timeout));
+ setsockopt(pfRoute, SOL_SOCKET, SO_SNDTIMEO, (char*) &timeout, sizeof(timeout));
+ }
+
+ uint32_t pid = instance.state.platform.pid;
+
+ struct {
+ struct rt_msghdr hdr;
+ struct sockaddr_in dst;
+ uint8_t data[512];
+ } rtmsg = {
+ .hdr = {
+ .rtm_type = RTM_GET,
+ .rtm_flags = RTF_UP | RTF_GATEWAY,
+ .rtm_version = RTM_VERSION,
+ .rtm_addrs = RTA_DST | RTA_IFP | RTA_IFA,
+ .rtm_msglen = sizeof(rtmsg.hdr) + sizeof(rtmsg.dst),
+ .rtm_pid = (pid_t) pid,
+ .rtm_seq = 1,
+ },
+ .dst = {
+ .sin_family = AF_INET,
+#ifndef __sun
+ .sin_len = sizeof(rtmsg.dst),
+#endif
+ },
+ };
+
+ if (send(pfRoute, &rtmsg, rtmsg.hdr.rtm_msglen, 0) != rtmsg.hdr.rtm_msglen) {
+ return false;
+ }
+
+ bool gotResponse = false;
+ while (recv(pfRoute, &rtmsg, sizeof(rtmsg), 0) > 0) {
+ if (rtmsg.hdr.rtm_seq == 1 && rtmsg.hdr.rtm_pid == (pid_t) pid) {
+ gotResponse = true;
+ break;
+ }
+ }
+ if (!gotResponse) {
+ return false;
+ }
+
+#ifndef __sun // On Solaris, the RTF_GATEWAY flag is not set for default routes for some reason
+ if ((rtmsg.hdr.rtm_flags & (RTF_UP | RTF_GATEWAY)) == (RTF_UP | RTF_GATEWAY))
+#endif
+ {
+ struct sockaddr_dl* sdl = (struct sockaddr_dl*) get_rt_address(&rtmsg.hdr, RTA_IFP);
+ if (sdl
+#ifndef __sun
+ && sdl->sdl_len
+#endif
+ && sdl->sdl_family == AF_LINK) {
+ if (sdl->sdl_nlen > IF_NAMESIZE) {
+ return false;
+ }
+ memcpy(result->ifName, sdl->sdl_data, sdl->sdl_nlen);
+ result->ifName[sdl->sdl_nlen] = '\0';
+ result->ifIndex = sdl->sdl_index;
+
+ // Get the preferred source address
+ struct sockaddr_in* src = (struct sockaddr_in*) get_rt_address(&rtmsg.hdr, RTA_IFA);
+ if (src && src->sin_family == AF_INET) {
+ result->preferredSourceAddrV4 = src->sin_addr.s_addr;
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ return false;
+}
+
+bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) {
+ // https://github.com/hashPirate/copenheimer-masscan-fork/blob/36f1ed9f7b751a7dccd5ed27874e2e703db7d481/src/rawsock-getif.c#L104
+
+ FF_AUTO_CLOSE_FD int pfRoute = socket(PF_ROUTE, SOCK_RAW, AF_INET6);
+ if (pfRoute < 0) {
+ return false;
+ }
+
+ {
+ struct timeval timeout = { 1, 0 };
+ setsockopt(pfRoute, SOL_SOCKET, SO_RCVTIMEO, (char*) &timeout, sizeof(timeout));
+ setsockopt(pfRoute, SOL_SOCKET, SO_SNDTIMEO, (char*) &timeout, sizeof(timeout));
+ }
+
+ uint32_t pid = instance.state.platform.pid;
+
+ struct {
+ struct rt_msghdr hdr;
+ struct sockaddr_in6 dst;
+ uint8_t data[512];
+ } rtmsg = {
+ .hdr = {
+ .rtm_type = RTM_GET,
+ .rtm_flags = RTF_UP | RTF_GATEWAY,
+ .rtm_version = RTM_VERSION,
+ .rtm_addrs = RTA_DST | RTA_IFP,
+ .rtm_msglen = sizeof(rtmsg.hdr) + sizeof(rtmsg.dst),
+ .rtm_pid = (pid_t) pid,
+ .rtm_seq = 2,
+ },
+ .dst = {
+ .sin6_family = AF_INET6,
+#ifndef __sun
+ .sin6_len = sizeof(rtmsg.dst),
+#endif
+ },
+ };
+
+ if (send(pfRoute, &rtmsg, rtmsg.hdr.rtm_msglen, 0) != rtmsg.hdr.rtm_msglen) {
+ return false;
+ }
+
+ bool gotResponse = false;
+ while (recv(pfRoute, &rtmsg, sizeof(rtmsg), 0) > 0) {
+ if (rtmsg.hdr.rtm_seq == 2 && rtmsg.hdr.rtm_pid == (pid_t) pid) {
+ gotResponse = true;
+ break;
+ }
+ }
+ if (!gotResponse) {
+ return false;
+ }
+
+#ifndef __sun // On Solaris, the RTF_GATEWAY flag is not set for default routes for some reason
+ if ((rtmsg.hdr.rtm_flags & (RTF_UP | RTF_GATEWAY)) == (RTF_UP | RTF_GATEWAY))
+#endif
+ {
+ struct sockaddr_dl* sdl = (struct sockaddr_dl*) get_rt_address(&rtmsg.hdr, RTA_IFP);
+ if (sdl
+#ifndef __sun
+ && sdl->sdl_len
+#endif
+ && sdl->sdl_family == AF_LINK) {
+ if (sdl->sdl_nlen > IF_NAMESIZE) {
+ return false;
+ }
+ memcpy(result->ifName, sdl->sdl_data, sdl->sdl_nlen);
+ result->ifName[sdl->sdl_nlen] = '\0';
+ result->ifIndex = sdl->sdl_index;
+
+ return true;
+ }
+ return false;
+ }
+
+ return false;
+}
diff --git a/src/common/impl/netif_bsd.c b/src/common/impl/netif_bsd.c
new file mode 100644
index 0000000..2b59c6d
--- /dev/null
+++ b/src/common/impl/netif_bsd.c
@@ -0,0 +1,124 @@
+#include "common/netif.h"
+#include "common/io.h"
+#include "common/mallocHelper.h"
+
+#include <net/if.h>
+#include <net/if_dl.h>
+#include <net/route.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/sysctl.h>
+
+#define ROUNDUP2(a, n) ((a) > 0 ? (1 + (((a) - 1U) | ((n) - 1))) : (n))
+
+#if __DragonFly__
+ // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/cf0aa2f1e47a3f0a6055fe427563cb3f3e627064/sys/net/route.h#L315C9-L315C19
+ #define ROUNDUP(a) ROUNDUP2((a), sizeof(long))
+#elif __FreeBSD__
+ // https://github.com/freebsd/freebsd-src/blob/e4c0ecba44b20ebb2e4d80978c2cb6d16b730cb9/sys/net/route.h#L368C9-L368C16
+ #define ROUNDUP(a) ROUNDUP2((a), sizeof(long))
+#elif __NetBSD__
+ // https://github.com/NetBSD/src/blob/29beb637d057520c0ed37ac2cde966f7cc0cadf4/sys/net/route.h#L330
+ #define ROUNDUP(a) ROUNDUP2((a), sizeof(uint64_t))
+#elif __OpenBSD__
+ // https://github.com/openbsd/src/blob/ca647cfa4ec3ccb8360714bc0ebc32a394f7fb6a/regress/sys/netinet/bindconnect/bindconnect.c#L250
+ #define ROUNDUP(a) ROUNDUP2((a), sizeof(long))
+#else
+ #error unknown platform
+#endif
+
+static struct sockaddr*
+get_rt_address(struct rt_msghdr* rtm, int desired) {
+ struct sockaddr* sa = (struct sockaddr*) (rtm + 1);
+
+ for (int i = 0; i < RTAX_MAX; i++) {
+ if (rtm->rtm_addrs & (1 << i)) {
+ if ((1 << i) == desired) {
+ return sa;
+ }
+ sa = (struct sockaddr*) (ROUNDUP(sa->sa_len) + (char*) sa);
+ }
+ }
+ return NULL;
+}
+
+bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) {
+ int mib[6] = { CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_FLAGS, RTF_GATEWAY };
+ size_t needed;
+
+ if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0 || needed == 0) {
+ return false;
+ }
+
+ FF_AUTO_FREE char* buf = malloc(needed);
+
+ if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0) {
+ return false;
+ }
+
+ char* lim = buf + needed;
+ struct rt_msghdr* rtm;
+ for (char* next = buf; next < lim; next += rtm->rtm_msglen) {
+ rtm = (struct rt_msghdr*) next;
+ struct sockaddr* sa = (struct sockaddr*) (rtm + 1);
+
+ if ((rtm->rtm_flags & RTF_GATEWAY) && !(rtm->rtm_flags & RTF_REJECT) && (sa->sa_family == AF_INET)) {
+ struct sockaddr_dl* sdl = (struct sockaddr_dl*) get_rt_address(rtm, RTA_IFP);
+ if (sdl && sdl->sdl_family == AF_LINK) {
+ if (sdl->sdl_nlen > IF_NAMESIZE) {
+ continue;
+ }
+ memcpy(result->ifName, sdl->sdl_data, sdl->sdl_nlen);
+ result->ifName[sdl->sdl_nlen] = '\0';
+ result->ifIndex = sdl->sdl_index;
+
+ // Get the preferred source address
+ struct sockaddr_in* src = (struct sockaddr_in*) get_rt_address(rtm, RTA_IFA);
+ if (src && src->sin_family == AF_INET) {
+ result->preferredSourceAddrV4 = src->sin_addr.s_addr;
+ }
+
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) {
+ int mib[6] = { CTL_NET, PF_ROUTE, 0, AF_INET6, NET_RT_FLAGS, RTF_GATEWAY };
+ size_t needed;
+
+ if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0 || needed == 0) {
+ return false;
+ }
+
+ FF_AUTO_FREE char* buf = malloc(needed);
+
+ if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0) {
+ return false;
+ }
+
+ char* lim = buf + needed;
+ struct rt_msghdr* rtm;
+ for (char* next = buf; next < lim; next += rtm->rtm_msglen) {
+ rtm = (struct rt_msghdr*) next;
+ struct sockaddr* sa = (struct sockaddr*) (rtm + 1);
+
+ if ((rtm->rtm_flags & RTF_GATEWAY) && !(rtm->rtm_flags & RTF_REJECT) && (sa->sa_family == AF_INET6)) {
+ struct sockaddr_dl* sdl = (struct sockaddr_dl*) get_rt_address(rtm, RTA_IFP);
+ if (sdl && sdl->sdl_family == AF_LINK) {
+ if (sdl->sdl_nlen > IF_NAMESIZE) {
+ continue;
+ }
+ memcpy(result->ifName, sdl->sdl_data, sdl->sdl_nlen);
+ result->ifName[sdl->sdl_nlen] = '\0';
+ result->ifIndex = sdl->sdl_index;
+
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/src/common/impl/netif_gnu.c b/src/common/impl/netif_gnu.c
new file mode 100644
index 0000000..d2feed5
--- /dev/null
+++ b/src/common/impl/netif_gnu.c
@@ -0,0 +1,36 @@
+#include "common/netif.h"
+#include "common/io.h"
+
+#include <net/if.h>
+#include <stdio.h>
+
+#define FF_STR_INDIR(x) #x
+#define FF_STR(x) FF_STR_INDIR(x)
+
+bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) {
+ FILE* FF_AUTO_CLOSE_FILE netRoute = fopen("/proc/route", "r");
+
+ if (!netRoute) {
+ return false;
+ }
+
+ // skip first line
+ FF_UNUSED(fscanf(netRoute, "%*[^\n]\n"));
+ unsigned long long destination; //, gateway, flags, refCount, use, metric, mask, mtu, ...
+ while (fscanf(netRoute, "%" FF_STR(IF_NAMESIZE) "s%llx%*[^\n]", result->ifName, &destination) == 2) {
+ if (destination != 0) {
+ continue;
+ }
+ result->ifIndex = if_nametoindex(result->ifName);
+ // TODO: Get the preferred source address
+ return true;
+ }
+ result->ifName[0] = '\0';
+ return false;
+}
+
+bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) {
+ // TODO: AF_INET6
+ FF_UNUSED(result);
+ return false;
+}
diff --git a/src/common/impl/netif_haiku.c b/src/common/impl/netif_haiku.c
new file mode 100644
index 0000000..a32138c
--- /dev/null
+++ b/src/common/impl/netif_haiku.c
@@ -0,0 +1,125 @@
+#include "common/netif.h"
+#include "common/io.h"
+#include "common/mallocHelper.h"
+
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <net/route.h>
+#include <sys/socket.h>
+#include <sys/sockio.h>
+#include <stdio.h>
+
+// loosely based on Haiku's src/bin/network/route/route.cpp
+
+bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) {
+ FF_AUTO_CLOSE_FD int pfRoute = socket(AF_INET, SOCK_RAW, AF_INET);
+ if (pfRoute < 0) {
+ return false;
+ }
+
+ struct ifconf config;
+ config.ifc_len = sizeof(config.ifc_value);
+ if (ioctl(pfRoute, SIOCGRTSIZE, &config, sizeof(struct ifconf)) < 0) {
+ return false;
+ }
+
+ int size = config.ifc_value;
+ if (size <= 0) {
+ return false;
+ }
+
+ FF_AUTO_FREE void* buffer = malloc((size_t) size);
+ if (buffer == NULL) {
+ return false;
+ }
+
+ config.ifc_len = size;
+ config.ifc_buf = buffer;
+ if (ioctl(pfRoute, SIOCGRTTABLE, &config, sizeof(struct ifconf)) < 0) {
+ return false;
+ }
+
+ struct ifreq* interface = (struct ifreq*) buffer;
+ struct ifreq* end = (struct ifreq*) ((uint8_t*) buffer + size);
+
+ while (interface < end) {
+ if (interface->ifr_route.flags & RTF_DEFAULT) {
+ // interface->ifr_metric?
+ strlcpy(result->ifName, interface->ifr_name, IF_NAMESIZE);
+ result->ifIndex = if_nametoindex(interface->ifr_name);
+ if (interface->ifr_route.source) {
+ result->preferredSourceAddrV4 = ((struct sockaddr_in*) interface->ifr_route.source)->sin_addr.s_addr;
+ }
+ return true;
+ }
+
+ size_t addressSize = 0;
+ if (interface->ifr_route.destination != NULL) {
+ addressSize += interface->ifr_route.destination->sa_len;
+ }
+ if (interface->ifr_route.mask != NULL) {
+ addressSize += interface->ifr_route.mask->sa_len;
+ }
+ if (interface->ifr_route.gateway != NULL) {
+ addressSize += interface->ifr_route.gateway->sa_len;
+ }
+
+ interface = (struct ifreq*) ((addr_t) interface + IF_NAMESIZE + sizeof(struct route_entry) + addressSize);
+ }
+
+ return false;
+}
+
+bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) {
+ FF_AUTO_CLOSE_FD int pfRoute = socket(AF_INET6, SOCK_RAW, AF_INET6);
+ if (pfRoute < 0) {
+ return false;
+ }
+
+ struct ifconf config;
+ config.ifc_len = sizeof(config.ifc_value);
+ if (ioctl(pfRoute, SIOCGRTSIZE, &config, sizeof(struct ifconf)) < 0) {
+ return false;
+ }
+
+ int size = config.ifc_value;
+ if (size <= 0) {
+ return false;
+ }
+
+ FF_AUTO_FREE void* buffer = malloc((size_t) size);
+ if (buffer == NULL) {
+ return false;
+ }
+
+ config.ifc_len = size;
+ config.ifc_buf = buffer;
+ if (ioctl(pfRoute, SIOCGRTTABLE, &config, sizeof(struct ifconf)) < 0) {
+ return false;
+ }
+
+ struct ifreq* interface = (struct ifreq*) buffer;
+ struct ifreq* end = (struct ifreq*) ((uint8_t*) buffer + size);
+
+ while (interface < end) {
+ if (interface->ifr_route.flags & RTF_DEFAULT) {
+ strlcpy(result->ifName, interface->ifr_name, IF_NAMESIZE);
+ result->ifIndex = if_nametoindex(interface->ifr_name);
+ return true;
+ }
+
+ size_t addressSize = 0;
+ if (interface->ifr_route.destination != NULL) {
+ addressSize += interface->ifr_route.destination->sa_len;
+ }
+ if (interface->ifr_route.mask != NULL) {
+ addressSize += interface->ifr_route.mask->sa_len;
+ }
+ if (interface->ifr_route.gateway != NULL) {
+ addressSize += interface->ifr_route.gateway->sa_len;
+ }
+
+ interface = (struct ifreq*) ((addr_t) interface + IF_NAMESIZE + sizeof(struct route_entry) + addressSize);
+ }
+ return false;
+}
diff --git a/src/common/impl/netif_linux.c b/src/common/impl/netif_linux.c
new file mode 100644
index 0000000..67e4868
--- /dev/null
+++ b/src/common/impl/netif_linux.c
@@ -0,0 +1,443 @@
+#include "common/netif.h"
+#include "common/io.h"
+#include "common/mallocHelper.h"
+#include "common/debug.h"
+
+#include <arpa/inet.h>
+#include <linux/rtnetlink.h>
+#include <net/if.h>
+
+static uint32_t ffNetifGetNetlinkPortId(int sock_fd) {
+ struct sockaddr_nl addr = {};
+ socklen_t addrLen = sizeof(addr);
+ if (getsockname(sock_fd, (struct sockaddr*) &addr, &addrLen) < 0) {
+ FF_DEBUG("Failed to query netlink socket address (use PID instead): %s", strerror(errno));
+ return instance.state.platform.pid;
+ } else {
+ FF_DEBUG("Netlink port ID: %u", addr.nl_pid);
+ return addr.nl_pid;
+ }
+}
+
+bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) {
+ FF_DEBUG("Starting IPv4 default route detection");
+
+ FF_AUTO_CLOSE_FD int sock_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+ if (sock_fd < 0) {
+ FF_DEBUG("Failed to create netlink socket: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("Created netlink socket: fd=%d", sock_fd);
+
+ // Bind socket
+ struct sockaddr_nl addr = {
+ .nl_family = AF_NETLINK,
+ .nl_pid = 0, // Let kernel choose PID
+ .nl_groups = 0, // No multicast groups
+ };
+
+ if (bind(sock_fd, (struct sockaddr*) &addr, sizeof(addr)) < 0) {
+ FF_DEBUG("Failed to bind socket: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("Successfully bound socket");
+
+ uint32_t pid = ffNetifGetNetlinkPortId(sock_fd);
+
+ struct FF_A_PACKED {
+ struct nlmsghdr nlh;
+ struct rtmsg rtm;
+ struct rtattr rta;
+ uint32_t table;
+ } req = {
+ // Netlink message header
+ .nlh = {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = RTM_GETROUTE,
+ .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
+ .nlmsg_seq = 1,
+ .nlmsg_pid = pid,
+ },
+ // Route message
+ .rtm = {
+ .rtm_family = AF_INET,
+ .rtm_dst_len = 0, // Match all destinations
+ .rtm_src_len = 0, // Match all sources
+ .rtm_tos = 0,
+ .rtm_table = RT_TABLE_UNSPEC,
+ .rtm_protocol = RTPROT_UNSPEC,
+ .rtm_scope = RT_SCOPE_UNIVERSE,
+ .rtm_type = RTN_UNSPEC,
+ .rtm_flags = 0,
+ },
+ // Route attribute for main table
+ .rta = {
+ .rta_len = RTA_LENGTH(sizeof(uint32_t)),
+ .rta_type = RTA_TABLE,
+ },
+ .table = RT_TABLE_MAIN,
+ };
+
+ struct sockaddr_nl dest_addr = {
+ .nl_family = AF_NETLINK,
+ .nl_pid = 0, // Kernel
+ .nl_groups = 0, // No multicast groups
+ };
+
+ ssize_t sent = sendto(sock_fd, &req, sizeof(req), 0, (struct sockaddr*) &dest_addr, sizeof(dest_addr));
+
+ if (sent != sizeof(req)) {
+ FF_DEBUG("Failed to send netlink request: sent=%zd, expected=%zu", sent, sizeof(req));
+ return false;
+ }
+ FF_DEBUG("Sent netlink request: %zd bytes", sent);
+
+ struct sockaddr_nl src_addr = {};
+ socklen_t src_addr_len = sizeof(src_addr);
+
+ uint8_t buffer[1024 * 16]; // 16 KB buffer should be sufficient
+ uint32_t minMetric = UINT32_MAX;
+ FF_A_UNUSED int routeCount = 0;
+
+ while (true) {
+ ssize_t received = recvfrom(sock_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &src_addr, &src_addr_len);
+
+ if (received < 0) {
+ FF_DEBUG("Failed to receive netlink response: %s", strerror(errno));
+ return false;
+ }
+
+ if (received >= (ssize_t) sizeof(buffer)) {
+ FF_DEBUG("Received truncated message: received %zd, bufsize %zu", received, sizeof(buffer));
+ return false;
+ }
+ FF_DEBUG("Received netlink response: %zd bytes", received);
+ if (received == 0) {
+ FF_DEBUG("Received zero-length netlink response, ending processing");
+ break;
+ }
+
+ struct {
+ uint32_t metric;
+ uint32_t ifindex;
+ uint32_t prefsrc;
+ } entry;
+
+ for (const struct nlmsghdr* nlh = (struct nlmsghdr*) buffer;
+ NLMSG_OK(nlh, received);
+ nlh = NLMSG_NEXT(nlh, received)) {
+ if (nlh->nlmsg_seq != 1 || nlh->nlmsg_pid != pid) {
+ continue;
+ }
+ if (nlh->nlmsg_type == NLMSG_DONE) {
+ FF_DEBUG("Received NLMSG_DONE, processed %d routes", routeCount);
+ goto exit;
+ }
+
+ if (nlh->nlmsg_type == NLMSG_ERROR) {
+ FF_DEBUG("Netlink reports error: %s", strerror(-((struct nlmsgerr*) NLMSG_DATA(nlh))->error));
+ continue;
+ }
+
+ if (nlh->nlmsg_type != RTM_NEWROUTE) {
+ FF_DEBUG("Skipping non-route message: type=%d", nlh->nlmsg_type);
+ continue;
+ }
+
+ routeCount++;
+ struct rtmsg* rtm = (struct rtmsg*) NLMSG_DATA(nlh);
+ if (rtm->rtm_family != AF_INET) {
+ FF_DEBUG("Skipping non-IPv4 route #%d (family=%d)", routeCount, rtm->rtm_family);
+ continue;
+ }
+
+ if (rtm->rtm_dst_len != 0) {
+ FF_DEBUG("Skipping non-default route #%d (dst_len=%d)", routeCount, rtm->rtm_dst_len);
+ continue;
+ }
+
+ // Skip local/loopback routes
+ if (rtm->rtm_scope == RT_SCOPE_HOST || rtm->rtm_type == RTN_LOCAL) {
+ FF_DEBUG("Skipping local route #%d (scope=%d, type=%d)", routeCount, rtm->rtm_scope, rtm->rtm_type);
+ continue;
+ }
+
+ FF_DEBUG("Processing IPv4 default route candidate #%d", routeCount);
+ entry = (__typeof__(entry)) {}; // Default to zero metric (no RTA_PRIORITY found)
+
+ // Parse route attributes
+ size_t rtm_len = RTM_PAYLOAD(nlh);
+ for (struct rtattr* rta = RTM_RTA(rtm);
+ RTA_OK(rta, rtm_len);
+ rta = RTA_NEXT(rta, rtm_len)) {
+ if (RTA_PAYLOAD(rta) < sizeof(uint32_t)) {
+ continue; // Skip invalid attributes
+ }
+
+ uint32_t rta_data = *(uint32_t*) RTA_DATA(rta);
+ switch (rta->rta_type) {
+ case RTA_DST:
+ FF_DEBUG("Unexpected RTA_DST: %s (len=%u)", inet_ntoa((struct in_addr) { .s_addr = rta_data }), rtm->rtm_dst_len);
+ goto next;
+ case RTA_OIF:
+ entry.ifindex = rta_data;
+ FF_DEBUG("Found interface index: %u", entry.ifindex);
+ break;
+ case RTA_GATEWAY:
+ FF_DEBUG("Found gateway: %s", inet_ntoa(*(struct in_addr*) &rta_data));
+ if (rta_data == 0) {
+ goto next;
+ }
+ break;
+ case RTA_PRIORITY:
+ FF_DEBUG("Found metric: %u", rta_data);
+ if (rta_data >= minMetric) {
+ goto next;
+ }
+ entry.metric = rta_data;
+ break;
+ case RTA_PREFSRC:
+ entry.prefsrc = rta_data;
+ FF_DEBUG("Found preferred source: %s", inet_ntoa(*(struct in_addr*) &rta_data));
+ break;
+ }
+ }
+
+ if (entry.ifindex == 0 || entry.metric >= minMetric) {
+ next:
+ FF_DEBUG("Skipping route: ifindex=%u, metric=%u", entry.ifindex, entry.metric);
+ continue;
+ }
+ minMetric = entry.metric;
+ result->ifIndex = entry.ifindex;
+ FF_DEBUG("Updated best route: ifindex=%u, metric=%u, prefsrc=%x", entry.ifindex, entry.metric, entry.prefsrc);
+ result->preferredSourceAddrV4 = entry.prefsrc;
+ if (minMetric == 0) {
+ FF_DEBUG("Found zero metric route, stopping further processing");
+ break; // Stop processing if we found a zero metric route
+ }
+ }
+ }
+
+exit:
+ if (minMetric < UINT32_MAX) {
+ if_indextoname(result->ifIndex, result->ifName);
+ FF_DEBUG("Found default IPv4 route: interface=%s, index=%u, metric=%u", result->ifName, result->ifIndex, minMetric);
+ return true;
+ }
+ FF_DEBUG("No IPv4 default route found");
+ return false;
+}
+
+bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) {
+ FF_DEBUG("Starting IPv6 default route detection");
+
+ FF_AUTO_CLOSE_FD int sock_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+ if (sock_fd < 0) {
+ FF_DEBUG("Failed to create netlink socket: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("Created netlink socket: fd=%d", sock_fd);
+
+ // Bind socket
+ struct sockaddr_nl addr = {
+ .nl_family = AF_NETLINK,
+ .nl_pid = 0, // Let kernel choose PID
+ .nl_groups = 0, // No multicast groups
+ };
+
+ if (bind(sock_fd, (struct sockaddr*) &addr, sizeof(addr)) < 0) {
+ FF_DEBUG("Failed to bind socket: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("Successfully bound socket");
+
+ uint32_t pid = ffNetifGetNetlinkPortId(sock_fd);
+
+ struct FF_A_PACKED {
+ struct nlmsghdr nlh;
+ struct rtmsg rtm;
+ struct rtattr rta;
+ uint32_t table;
+ } req = {
+ // Netlink message header
+ .nlh = {
+ .nlmsg_len = sizeof(req),
+ .nlmsg_type = RTM_GETROUTE,
+ .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
+ .nlmsg_seq = 1,
+ .nlmsg_pid = pid,
+ },
+ // Route message
+ .rtm = {
+ .rtm_family = AF_INET6, // IPv6 instead of IPv4
+ .rtm_dst_len = 0, // Match all destinations
+ .rtm_src_len = 0, // Match all sources
+ .rtm_tos = 0,
+ .rtm_table = RT_TABLE_UNSPEC,
+ .rtm_protocol = RTPROT_UNSPEC,
+ .rtm_scope = RT_SCOPE_UNIVERSE,
+ .rtm_type = RTN_UNSPEC,
+ .rtm_flags = 0,
+ },
+ // Route attribute for main table
+ .rta = {
+ .rta_len = RTA_LENGTH(sizeof(uint32_t)),
+ .rta_type = RTA_TABLE,
+ },
+ .table = RT_TABLE_MAIN,
+ };
+
+ struct sockaddr_nl dest_addr = {
+ .nl_family = AF_NETLINK,
+ .nl_pid = 0, // Kernel
+ .nl_groups = 0, // No multicast groups
+ };
+
+ ssize_t sent = sendto(sock_fd, &req, sizeof(req), 0, (struct sockaddr*) &dest_addr, sizeof(dest_addr));
+
+ if (sent != sizeof(req)) {
+ FF_DEBUG("Failed to send netlink request: sent=%zd, expected=%zu", sent, sizeof(req));
+ return false;
+ }
+ FF_DEBUG("Sent netlink request: %zd bytes", sent);
+
+ struct sockaddr_nl src_addr = {};
+ socklen_t src_addr_len = sizeof(src_addr);
+
+ uint8_t buffer[1024 * 16]; // 16 KB buffer should be sufficient
+ uint32_t minMetric = UINT32_MAX;
+ FF_A_UNUSED int routeCount = 0;
+
+ while (true) {
+ ssize_t received = recvfrom(sock_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &src_addr, &src_addr_len);
+
+ if (received < 0) {
+ FF_DEBUG("Failed to receive netlink response: %s", strerror(errno));
+ return false;
+ }
+
+ if (received >= (ssize_t) sizeof(buffer)) {
+ FF_DEBUG("Received truncated message: received %zd, bufsize %zu", received, sizeof(buffer));
+ return false;
+ }
+ FF_DEBUG("Received netlink response: %zd bytes", received);
+ if (received == 0) {
+ FF_DEBUG("Received zero-length netlink response, ending processing");
+ break;
+ }
+
+ struct {
+ uint32_t metric;
+ uint32_t ifindex;
+ } entry;
+
+ for (const struct nlmsghdr* nlh = (struct nlmsghdr*) buffer;
+ NLMSG_OK(nlh, received);
+ nlh = NLMSG_NEXT(nlh, received)) {
+ if (nlh->nlmsg_seq != 1 || nlh->nlmsg_pid != pid) {
+ continue;
+ }
+ if (nlh->nlmsg_type == NLMSG_DONE) {
+ FF_DEBUG("Received NLMSG_DONE, processed %d routes", routeCount);
+ goto exit;
+ }
+
+ if (nlh->nlmsg_type == NLMSG_ERROR) {
+ FF_DEBUG("Netlink reports error: %s", strerror(-((struct nlmsgerr*) NLMSG_DATA(nlh))->error));
+ continue;
+ }
+
+ if (nlh->nlmsg_type != RTM_NEWROUTE) {
+ FF_DEBUG("Skipping non-route message: type=%d", nlh->nlmsg_type);
+ continue;
+ }
+
+ routeCount++;
+ struct rtmsg* rtm = (struct rtmsg*) NLMSG_DATA(nlh);
+ if (rtm->rtm_family != AF_INET6) {
+ FF_DEBUG("Skipping non-IPv6 route #%d (family=%d)", routeCount, rtm->rtm_family);
+ continue;
+ }
+
+ if (rtm->rtm_dst_len != 0) {
+ FF_DEBUG("Skipping non-default route #%d (dst_len=%d)", routeCount, rtm->rtm_dst_len);
+ continue;
+ }
+
+ // Skip local/loopback routes
+ if (rtm->rtm_scope == RT_SCOPE_HOST || rtm->rtm_type == RTN_LOCAL) {
+ FF_DEBUG("Skipping local route #%d (scope=%d, type=%d)", routeCount, rtm->rtm_scope, rtm->rtm_type);
+ continue;
+ }
+
+ FF_DEBUG("Processing IPv6 default route candidate #%d", routeCount);
+ entry = (__typeof__(entry)) {}; // Default to zero metric (no RTA_PRIORITY found)
+
+ // Parse route attributes
+ size_t rtm_len = RTM_PAYLOAD(nlh);
+ for (struct rtattr* rta = RTM_RTA(rtm);
+ RTA_OK(rta, rtm_len);
+ rta = RTA_NEXT(rta, rtm_len)) {
+ switch (rta->rta_type) {
+ case RTA_DST:
+ if (RTA_PAYLOAD(rta) >= sizeof(struct in6_addr)) {
+ FF_A_UNUSED char str[INET6_ADDRSTRLEN];
+ FF_DEBUG("Unexpected RTA_DST: %s", inet_ntop(AF_INET6, RTA_DATA(rta), str, sizeof(str)));
+ goto next;
+ }
+ break;
+ case RTA_OIF:
+ if (RTA_PAYLOAD(rta) >= sizeof(uint32_t)) {
+ entry.ifindex = *(uint32_t*) RTA_DATA(rta);
+ FF_DEBUG("Found interface index: %u", entry.ifindex);
+ }
+ break;
+ case RTA_GATEWAY:
+ if (RTA_PAYLOAD(rta) >= sizeof(struct in6_addr)) {
+ struct in6_addr* gw = (struct in6_addr*) RTA_DATA(rta);
+ if (IN6_IS_ADDR_UNSPECIFIED(gw)) {
+ goto next;
+ }
+ FF_A_UNUSED char str[INET6_ADDRSTRLEN];
+ FF_DEBUG("Found gateway: %s", inet_ntop(AF_INET6, gw, str, sizeof(str)));
+ }
+ break;
+ case RTA_PRIORITY:
+ if (RTA_PAYLOAD(rta) >= sizeof(uint32_t)) {
+ uint32_t metric = *(uint32_t*) RTA_DATA(rta);
+ FF_DEBUG("Found metric: %u", metric);
+ if (metric >= minMetric) {
+ goto next;
+ }
+ entry.metric = metric;
+ }
+ break;
+ }
+ }
+
+ if (entry.ifindex == 0 || entry.metric >= minMetric) {
+ next:
+ FF_DEBUG("Skipping route: ifindex=%u, metric=%u", entry.ifindex, entry.metric);
+ continue;
+ }
+ minMetric = entry.metric;
+ result->ifIndex = entry.ifindex;
+ FF_DEBUG("Updated best route: ifindex=%u, metric=%u", entry.ifindex, entry.metric);
+
+ if (minMetric == 0) {
+ FF_DEBUG("Found zero metric route, stopping further processing");
+ break; // Stop processing if we found a zero metric route
+ }
+ }
+ }
+
+exit:
+ if (minMetric < UINT32_MAX) {
+ if_indextoname(result->ifIndex, result->ifName);
+ FF_DEBUG("Found default IPv6 route: interface=%s, index=%u, metric=%u", result->ifName, result->ifIndex, minMetric);
+ return true;
+ }
+ FF_DEBUG("No IPv6 default route found");
+ return false;
+}
diff --git a/src/common/impl/netif_windows.c b/src/common/impl/netif_windows.c
new file mode 100644
index 0000000..7531ae0
--- /dev/null
+++ b/src/common/impl/netif_windows.c
@@ -0,0 +1,92 @@
+#include "common/netif.h"
+
+#include <ws2tcpip.h> // AF_INET6, IN6_IS_ADDR_UNSPECIFIED
+#include <iphlpapi.h>
+
+bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) {
+ PMIB_IPFORWARD_TABLE2 pIpForwardTable = NULL;
+
+ if (!NETIO_SUCCESS(GetIpForwardTable2(AF_INET, &pIpForwardTable))) {
+ return false;
+ }
+
+ bool foundDefault = false;
+ uint32_t smallestMetric = UINT32_MAX;
+
+ for (ULONG i = 0; i < pIpForwardTable->NumEntries; ++i) {
+ MIB_IPFORWARD_ROW2* row = &pIpForwardTable->Table[i];
+
+ if (row->DestinationPrefix.PrefixLength == 0 &&
+ row->DestinationPrefix.Prefix.Ipv4.sin_addr.S_un.S_addr == 0) {
+ MIB_IF_ROW2 ifRow = {
+ .InterfaceIndex = row->InterfaceIndex,
+ };
+ if (NETIO_SUCCESS(GetIfEntry2(&ifRow)) && ifRow.OperStatus == IfOperStatusUp) {
+ MIB_IPINTERFACE_ROW ipInterfaceRow = {
+ .Family = AF_INET,
+ .InterfaceIndex = row->InterfaceIndex,
+ };
+
+ uint32_t realMetric = row->Metric /* Metric offset */;
+
+ if (NETIO_SUCCESS(GetIpInterfaceEntry(&ipInterfaceRow))) {
+ realMetric += ipInterfaceRow.Metric /* Interface metric */;
+ }
+
+ if (realMetric < smallestMetric) {
+ smallestMetric = realMetric;
+ result->ifIndex = row->InterfaceIndex;
+ foundDefault = true;
+ }
+ }
+ }
+ }
+
+ FreeMibTable(pIpForwardTable);
+
+ return foundDefault;
+}
+
+bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) {
+ PMIB_IPFORWARD_TABLE2 pIpForwardTable = NULL;
+
+ if (!NETIO_SUCCESS(GetIpForwardTable2(AF_INET6, &pIpForwardTable))) {
+ return false;
+ }
+
+ bool foundDefault = false;
+ uint32_t smallestMetric = UINT32_MAX;
+
+ for (ULONG i = 0; i < pIpForwardTable->NumEntries; ++i) {
+ MIB_IPFORWARD_ROW2* row = &pIpForwardTable->Table[i];
+
+ if (row->DestinationPrefix.PrefixLength == 0 &&
+ IN6_IS_ADDR_UNSPECIFIED(&row->DestinationPrefix.Prefix.Ipv6.sin6_addr)) {
+ MIB_IF_ROW2 ifRow = {
+ .InterfaceIndex = row->InterfaceIndex,
+ };
+ if (NETIO_SUCCESS(GetIfEntry2(&ifRow)) && ifRow.OperStatus == IfOperStatusUp) {
+ MIB_IPINTERFACE_ROW ipInterfaceRow = {
+ .Family = AF_INET6,
+ .InterfaceIndex = row->InterfaceIndex,
+ };
+
+ uint32_t realMetric = row->Metric /* Metric offset */;
+
+ if (NETIO_SUCCESS(GetIpInterfaceEntry(&ipInterfaceRow))) {
+ realMetric += ipInterfaceRow.Metric /* Interface metric */;
+ }
+
+ if (realMetric < smallestMetric) {
+ smallestMetric = realMetric;
+ result->ifIndex = row->InterfaceIndex;
+ foundDefault = true;
+ }
+ }
+ }
+ }
+
+ FreeMibTable(pIpForwardTable);
+
+ return foundDefault;
+}
diff --git a/src/common/impl/networking_common.c b/src/common/impl/networking_common.c
new file mode 100644
index 0000000..e63979b
--- /dev/null
+++ b/src/common/impl/networking_common.c
@@ -0,0 +1,192 @@
+#include "fastfetch.h"
+#include "common/library.h"
+#include "common/networking.h"
+#include "common/strutil.h"
+#include "common/debug.h"
+
+#ifdef FF_HAVE_ZLIB
+ #include <zlib.h>
+
+struct FFZlibLibrary {
+ FF_LIBRARY_SYMBOL(inflateInit2_)
+ FF_LIBRARY_SYMBOL(inflate)
+ FF_LIBRARY_SYMBOL(inflateEnd)
+
+ bool inited;
+} zlibData;
+
+const char* ffNetworkingLoadZlibLibrary(void) {
+ if (!zlibData.inited) {
+ zlibData.inited = true;
+ FF_LIBRARY_LOAD_MESSAGE(zlib,
+ #ifdef _WIN32
+ "zlib1"
+ #else
+ "libz"
+ #endif
+ FF_LIBRARY_EXTENSION,
+ 2)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateInit2_)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflate)
+ FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateEnd)
+ zlib = NULL; // don't auto dlclose
+ }
+ return zlibData.ffinflateEnd == NULL ? "Failed to load libz" : NULL;
+}
+
+// Try to pre-read gzip header to determine uncompressed size
+static uint32_t guessGzipOutputSize(const void* data, uint32_t dataSize) {
+ // gzip file format: http://www.zlib.org/rfc-gzip.html
+ if (dataSize < 10 || ((const uint8_t*) data)[0] != 0x1f || ((const uint8_t*) data)[1] != 0x8b) {
+ return 0;
+ }
+
+ // Uncompressed size in gzip format is stored in the last 4 bytes, but only valid if data is less than 4GB
+ if (dataSize > 18) {
+ // Get ISIZE value from the end of file (little endian)
+ const uint8_t* tail = (const uint8_t*) data + dataSize - 4;
+ uint32_t uncompressedSize = (uint32_t) tail[0] | ((uint32_t) tail[1] << 8u) | ((uint32_t) tail[2] << 16u) | ((uint32_t) tail[3] << 24u);
+
+ // For valid gzip files, this value is the length of the uncompressed data modulo 2^32
+ if (uncompressedSize > 0) {
+ FF_DEBUG("Read uncompressed size from GZIP trailer: %u bytes", uncompressedSize);
+ // Add some margin to the estimated size for safety
+ return uncompressedSize + 64;
+ }
+ }
+
+ // If unable to get size from trailer or size is 0, use estimated value
+ // Typically, text data compression ratio is between 3-5x, we use the larger value
+ uint32_t estimatedSize = dataSize * 5;
+ FF_DEBUG("Unable to read exact uncompressed size, estimated as 5x of compressed data: %u bytes", estimatedSize);
+ return estimatedSize;
+}
+
+// Decompress gzip content
+bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) {
+ assert(headerEnd != NULL && *headerEnd == '\r');
+
+ // Calculate header size
+ uint32_t headerSize = (uint32_t) (headerEnd - buffer->chars);
+
+ *headerEnd = '\0'; // Replace delimiter with null character for easier processing
+ // Ensure Content-Encoding is in response headers, not in response body
+ bool hasGzip = strcasestr(buffer->chars, "\nContent-Encoding: gzip") != NULL;
+ *headerEnd = '\r'; // Restore delimiter
+
+ if (!hasGzip) {
+ FF_DEBUG("No gzip compressed content detected, skipping decompression");
+ return true;
+ }
+
+ FF_DEBUG("Gzip compressed content detected, preparing for decompression");
+
+ const char* bodyStart = headerEnd + 4; // Skip delimiter
+
+ if (buffer->length <= headerSize + 4) {
+ // No content to decompress
+ FF_DEBUG("Compressed content size is 0, skipping decompression");
+ return true;
+ }
+
+ // Calculate compressed content size
+ uint32_t compressedSize = buffer->length - headerSize - 4;
+
+ // Check if content is actually in gzip format (gzip header magic is 0x1f 0x8b)
+ if (compressedSize < 2 || (uint8_t) bodyStart[0] != 0x1f || (uint8_t) bodyStart[1] != 0x8b) {
+ FF_DEBUG("Content is not valid gzip format, skipping decompression");
+ return false;
+ }
+
+ // Predict uncompressed size
+ uint32_t estimatedSize = guessGzipOutputSize(bodyStart, compressedSize);
+
+ // Create decompression buffer with estimated size
+ FF_STRBUF_AUTO_DESTROY decompressedBuffer = ffStrbufCreateA(estimatedSize > 0 ? estimatedSize : compressedSize * 5);
+ FF_DEBUG("Created decompression buffer: %u bytes", decompressedBuffer.allocated);
+
+ // Initialize decompression
+ z_stream zs = {
+ .zalloc = Z_NULL,
+ .zfree = Z_NULL,
+ .opaque = Z_NULL,
+ .avail_in = (uInt) compressedSize,
+ .next_in = (Bytef*) bodyStart,
+ .avail_out = (uInt) ffStrbufGetFree(&decompressedBuffer),
+ .next_out = (Bytef*) decompressedBuffer.chars,
+ };
+
+ // Initialize decompression engine
+ if (zlibData.ffinflateInit2_(&zs, 16 + MAX_WBITS, ZLIB_VERSION, (int) sizeof(z_stream)) != Z_OK) {
+ FF_DEBUG("Failed to initialize decompression engine");
+ return false;
+ }
+ uInt availableOut = zs.avail_out;
+
+ // Perform decompression
+ int result = zlibData.ffinflate(&zs, Z_FINISH);
+
+ // If output buffer is insufficient, try to extend buffer
+ while (result == Z_BUF_ERROR || (result != Z_STREAM_END && zs.avail_out == 0)) {
+ FF_DEBUG("Output buffer insufficient, trying to extend");
+
+ // Save already decompressed data amount
+ uint32_t alreadyDecompressed = (uint32_t) (availableOut - zs.avail_out);
+ decompressedBuffer.length += alreadyDecompressed;
+ decompressedBuffer.chars[decompressedBuffer.length] = '\0';
+
+ ffStrbufEnsureFree(&decompressedBuffer, decompressedBuffer.length / 2);
+
+ // Set output parameters to point to new buffer
+ zs.avail_out = (uInt) ffStrbufGetFree(&decompressedBuffer);
+ zs.next_out = (Bytef*) (decompressedBuffer.chars + decompressedBuffer.length);
+ availableOut = zs.avail_out;
+
+ // Decompress again
+ result = zlibData.ffinflate(&zs, Z_FINISH);
+ }
+
+ // Check for decompression errors before using result
+ if (result != Z_STREAM_END) {
+ FF_DEBUG("Decompression failed with zlib error: %d", result);
+ zlibData.ffinflateEnd(&zs);
+ return false;
+ }
+
+ zlibData.ffinflateEnd(&zs);
+
+ // Calculate decompressed size (from the last inflate call)
+ uint32_t decompressedSize = (uint32_t) (availableOut - zs.avail_out);
+ decompressedBuffer.length += decompressedSize;
+ decompressedBuffer.chars[decompressedBuffer.length] = '\0';
+ FF_DEBUG("Successfully decompressed %u bytes compressed data to %u bytes", compressedSize, decompressedBuffer.length);
+
+ // Modify Content-Length header and remove Content-Encoding header
+ // Use decompressedBuffer.length (total) not decompressedSize (last chunk only)
+ FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerSize + decompressedBuffer.length + 64);
+
+ char* line = NULL;
+ size_t len = 0;
+ while (ffStrbufGetline(&line, &len, buffer)) {
+ if (ffStrStartsWithIgnCase(line, "Content-Encoding:")) {
+ continue;
+ } else if (ffStrStartsWithIgnCase(line, "Content-Length:")) {
+ ffStrbufAppendF(&newBuffer, "Content-Length: %u\r\n", decompressedBuffer.length);
+ continue;
+ } else if (line[0] == '\r') {
+ ffStrbufAppendS(&newBuffer, "\r\n");
+ ffStrbufGetlineRestore(&line, &len, buffer);
+ break;
+ }
+
+ ffStrbufAppendS(&newBuffer, line); // Including the trailing \r
+ ffStrbufAppendC(&newBuffer, '\n');
+ }
+
+ ffStrbufAppend(&newBuffer, &decompressedBuffer);
+ ffStrbufDestroy(buffer);
+ ffStrbufInitMove(buffer, &newBuffer);
+
+ return true;
+}
+#endif // FF_HAVE_ZLIB
diff --git a/src/common/impl/networking_linux.c b/src/common/impl/networking_linux.c
new file mode 100644
index 0000000..e253451
--- /dev/null
+++ b/src/common/impl/networking_linux.c
@@ -0,0 +1,502 @@
+#include "fastfetch.h"
+#include "common/networking.h"
+#include "common/time.h"
+#include "common/library.h"
+#include "common/strutil.h"
+#include "common/mallocHelper.h"
+#include "common/debug.h"
+
+#include <unistd.h>
+#include <poll.h>
+#include <sys/time.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <netinet/in.h> // For FreeBSD
+#include <netinet/tcp.h>
+#include <errno.h>
+#include <fcntl.h>
+
+static const char* tryNonThreadingFastPath(FFNetworkingState* state) {
+#if defined(TCP_FASTOPEN) || __APPLE__
+
+ if (!state->tfo) {
+ #if __linux__ || __GNU__
+ // Linux doesn't support sendto() on unconnected sockets
+ FF_DEBUG("TCP Fast Open disabled, skipping");
+ return "TCP Fast Open disabled";
+ #endif
+ } else {
+ FF_DEBUG("Attempting to use TCP Fast Open to connect");
+
+ #ifndef __APPLE__ // On macOS, TCP_FASTOPEN doesn't seem to be needed
+ // Set TCP Fast Open
+ int flag = 1;
+ if (setsockopt(state->sockfd, IPPROTO_TCP,
+ #ifdef __APPLE__
+ // https://github.com/rust-lang/libc/pull/3135
+ 0x218 // TCP_FASTOPEN_FORCE_ENABLE
+ #else
+ TCP_FASTOPEN
+ #endif
+ ,
+ &flag,
+ sizeof(flag)) != 0) {
+ FF_DEBUG("Failed to set TCP_FASTOPEN option: %s", strerror(errno));
+ return "setsockopt(TCP_FASTOPEN) failed";
+ } else {
+ #if __linux__ || __GNU__
+ FF_DEBUG("Successfully set TCP_FASTOPEN option, queue length: %d", flag);
+ #elif defined(__APPLE__)
+ FF_DEBUG("Successfully set TCP_FASTOPEN_FORCE_ENABLE option");
+ #else
+ FF_DEBUG("Successfully set TCP_FASTOPEN option");
+ #endif
+ }
+ #endif
+ }
+
+ #ifndef __APPLE__
+ FF_DEBUG("Using sendto() + MSG_DONTWAIT to send %u bytes of data", state->command.length);
+ ssize_t sent = sendto(state->sockfd,
+ state->command.chars,
+ state->command.length,
+ #ifdef MSG_FASTOPEN
+ MSG_FASTOPEN |
+ #endif
+ #ifdef MSG_NOSIGNAL
+ MSG_NOSIGNAL |
+ #endif
+ MSG_DONTWAIT,
+ state->addr->ai_addr,
+ state->addr->ai_addrlen);
+ #else
+ if (fcntl(state->sockfd, F_SETFL, O_NONBLOCK) == -1) {
+ FF_DEBUG("fcntl(F_SETFL) failed: %s", strerror(errno));
+ return "fcntl(F_SETFL) failed";
+ }
+ FF_DEBUG("Using connectx() to send %u bytes of data", state->command.length);
+ // Use connectx to establish connection and send data in one call
+ size_t sent;
+ if (connectx(state->sockfd,
+ &(sa_endpoints_t) {
+ .sae_dstaddr = state->addr->ai_addr,
+ .sae_dstaddrlen = state->addr->ai_addrlen,
+ },
+ SAE_ASSOCID_ANY,
+ state->tfo ? CONNECT_DATA_IDEMPOTENT : 0,
+ &(struct iovec) {
+ .iov_base = state->command.chars,
+ .iov_len = state->command.length,
+ },
+ 1,
+ &sent,
+ NULL) != 0) {
+ sent = 0;
+ }
+ if (fcntl(state->sockfd, F_SETFL, 0) == -1) {
+ FF_DEBUG("fcntl(F_SETFL) failed: %s", strerror(errno));
+ return "fcntl(F_SETFL) failed";
+ }
+ #endif
+ if (sent > 0 || (errno == EAGAIN || errno == EWOULDBLOCK
+ #ifdef __APPLE__
+ // On macOS EINPROGRESS means the connection cannot be completed immediately
+ // On Linux, it means the TFO cookie is not available locally
+ || errno == EINPROGRESS
+ #endif
+ )) {
+ FF_DEBUG(
+ #ifdef __APPLE__
+ "connectx()"
+ #else
+ "sendto()"
+ #endif
+ " %s (sent=%zd, %s)",
+ errno == 0 ? "succeeded" : "was in progress",
+ sent,
+ strerror(errno));
+ freeaddrinfo(state->addr);
+ state->addr = NULL;
+ ffStrbufDestroy(&state->command);
+ return NULL;
+ }
+
+ FF_DEBUG(
+ #ifdef __APPLE__
+ "connectx()"
+ #else
+ "sendto()"
+ #endif
+ " failed: %s",
+ strerror(errno));
+ #ifdef __APPLE__
+ return "connectx() failed";
+ #else
+ return "sendto() failed";
+ #endif
+#else
+ FF_UNUSED(state);
+ return "TFO support is not available";
+#endif
+}
+
+// Traditional connect and send function
+static const char* connectAndSend(FFNetworkingState* state) {
+ const char* ret = NULL;
+ FF_DEBUG("Using traditional connection method to connect");
+
+ FF_DEBUG("Attempting connect() to server...");
+ if (connect(state->sockfd, state->addr->ai_addr, state->addr->ai_addrlen) == -1) {
+ FF_DEBUG("connect() failed: %s", strerror(errno));
+ ret = "connect() failed";
+ goto error;
+ }
+ FF_DEBUG("connect() succeeded");
+
+ FF_DEBUG("Attempting to send %u bytes of data...", state->command.length);
+ if (send(state->sockfd, state->command.chars, state->command.length, 0) < 0) {
+ FF_DEBUG("send() failed: %s", strerror(errno));
+ ret = "send() failed";
+ goto error;
+ }
+ FF_DEBUG("Data sent successfully");
+
+ goto exit;
+
+error:
+ FF_DEBUG("Error occurred, closing socket");
+ close(state->sockfd);
+ state->sockfd = -1;
+
+exit:
+ FF_DEBUG("Releasing address info and other resources");
+ freeaddrinfo(state->addr);
+ state->addr = NULL;
+ ffStrbufDestroy(&state->command);
+
+ return ret;
+}
+
+FF_THREAD_ENTRY_DECL_WRAPPER(connectAndSend, FFNetworkingState*);
+
+// Parallel DNS resolution and socket creation
+static const char* initNetworkingState(FFNetworkingState* state, const char* host, const char* path, const char* headers) {
+ FF_DEBUG("Initializing network connection state: host=%s, path=%s", host, path);
+
+ // Initialize command and host information
+ ffStrbufInitA(&state->command, 128);
+ ffStrbufAppendS(&state->command, "GET ");
+ ffStrbufAppendS(&state->command, path);
+ ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: ");
+ ffStrbufAppendS(&state->command, host);
+ ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly tell the server we don't need to keep the connection
+
+ // If compression needs to be enabled
+ if (state->compression) {
+ FF_DEBUG("Enabling HTTP content compression");
+ ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n");
+ }
+
+ ffStrbufAppendS(&state->command, headers);
+ ffStrbufAppendS(&state->command, "\r\n");
+
+#ifdef FF_HAVE_THREADS
+ state->thread = 0;
+ FF_DEBUG("Thread ID initialized to 0");
+#endif
+
+ const char* ret = NULL;
+
+ struct addrinfo hints = {
+ .ai_family = state->ipv6 ? AF_INET6 : AF_INET,
+ .ai_socktype = SOCK_STREAM,
+ .ai_flags = AI_NUMERICSERV
+ };
+
+ FF_DEBUG("Resolving address: %s (%s)", host, state->ipv6 ? "IPv6" : "IPv4");
+ // Use AI_NUMERICSERV flag to indicate the service is a numeric port, reducing parsing time
+
+ int gaiRes = getaddrinfo(host, "80", &hints, &state->addr);
+ if (gaiRes != 0) {
+ FF_DEBUG("getaddrinfo() failed: %s (res=%d)", gai_strerror(gaiRes), gaiRes);
+ ret = "getaddrinfo() failed";
+ goto error;
+ }
+ FF_DEBUG("Address resolution successful");
+
+ FF_DEBUG("Creating socket");
+ state->sockfd = socket(state->addr->ai_family, state->addr->ai_socktype, state->addr->ai_protocol);
+ if (state->sockfd == -1) {
+ FF_DEBUG("socket() failed: %s", strerror(errno));
+ ret = "socket() failed";
+ goto error;
+ }
+ FF_DEBUG("Socket creation successful: fd=%d", state->sockfd);
+
+ int flag = 1;
+#ifdef TCP_NODELAY
+ // Disable Nagle's algorithm to reduce small packet transmission delay
+ if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)) != 0) {
+ FF_DEBUG("Failed to set TCP_NODELAY: %s", strerror(errno));
+ } else {
+ FF_DEBUG("Successfully disabled Nagle's algorithm");
+ }
+#endif
+
+#ifdef TCP_QUICKACK
+ // Set TCP_QUICKACK option to avoid delayed acknowledgments
+ if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_QUICKACK, &flag, sizeof(flag)) != 0) {
+ FF_DEBUG("Failed to set TCP_QUICKACK: %s", strerror(errno));
+ } else {
+ FF_DEBUG("Successfully enabled TCP quick acknowledgment");
+ }
+#endif
+
+ if (state->timeout > 0) {
+ FF_DEBUG("Setting connection timeout: %u ms", state->timeout);
+ FF_A_UNUSED uint32_t sec = state->timeout / 1000;
+ if (sec == 0) {
+ sec = 1;
+ }
+
+#ifdef TCP_CONNECTIONTIMEOUT
+ FF_DEBUG("Using TCP_CONNECTIONTIMEOUT: %u seconds", sec);
+ setsockopt(state->sockfd, IPPROTO_TCP, TCP_CONNECTIONTIMEOUT, &sec, sizeof(sec));
+#elif defined(TCP_KEEPINIT)
+ FF_DEBUG("Using TCP_KEEPINIT: %u seconds", sec);
+ setsockopt(state->sockfd, IPPROTO_TCP, TCP_KEEPINIT, &sec, sizeof(sec));
+#elif defined(TCP_USER_TIMEOUT)
+ FF_DEBUG("Using TCP_USER_TIMEOUT: %u milliseconds", state->timeout);
+ setsockopt(state->sockfd, IPPROTO_TCP, TCP_USER_TIMEOUT, &state->timeout, sizeof(state->timeout));
+#else
+ FF_DEBUG("Current platform does not support TCP connection timeout");
+#endif
+ }
+
+ return NULL;
+
+error:
+ FF_DEBUG("Error occurred during initialization");
+ if (state->addr != NULL) {
+ FF_DEBUG("Releasing address information");
+ freeaddrinfo(state->addr);
+ state->addr = NULL;
+ }
+
+ if (state->sockfd > 0) {
+ FF_DEBUG("Closing socket: fd=%d", state->sockfd);
+ close(state->sockfd);
+ state->sockfd = -1;
+ }
+ return ret;
+}
+
+const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers) {
+ FF_DEBUG("Preparing to send HTTP request: host=%s, path=%s", host, path);
+
+ if (state->compression) {
+ FF_DEBUG("Compression enabled, checking if zlib is available");
+
+#ifdef FF_HAVE_ZLIB
+ const char* zlibError = ffNetworkingLoadZlibLibrary();
+ // Only enable compression if zlib library is successfully loaded
+ if (zlibError == NULL) {
+ FF_DEBUG("Successfully loaded zlib library, compression enabled");
+ } else {
+ FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError);
+ state->compression = false;
+ }
+#else
+ FF_DEBUG("zlib not supported at build time, compression disabled");
+ state->compression = false;
+#endif
+ } else {
+ FF_DEBUG("Compression disabled");
+ }
+
+ const char* initResult = initNetworkingState(state, host, path, headers);
+ if (initResult != NULL) {
+ FF_DEBUG("Initialization failed: %s", initResult);
+ return initResult;
+ }
+ FF_DEBUG("Network state initialization successful");
+
+ const char* tfoResult = tryNonThreadingFastPath(state);
+ if (tfoResult == NULL) {
+ FF_DEBUG("TryNonThreadingFastPath() succeeded or in progress");
+ return NULL;
+ }
+ FF_DEBUG("TryNonThreadingFastPath() failed: %s, trying traditional connection", tfoResult);
+
+#ifdef FF_HAVE_THREADS
+ if (instance.config.general.multithreading) {
+ FF_DEBUG("Multithreading mode enabled, creating connection thread");
+ state->thread = ffThreadCreate(connectAndSendThreadMain, state);
+ if (state->thread) {
+ FF_DEBUG("Thread creation successful: thread=%p", (void*) (uintptr_t) state->thread);
+ return NULL;
+ }
+ FF_DEBUG("Thread creation failed");
+ } else {
+ FF_DEBUG("Multithreading mode disabled, connecting in main thread");
+ }
+#endif
+
+ return connectAndSend(state);
+}
+
+const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer) {
+ assert(buffer->allocated > 0);
+ FF_DEBUG("Preparing to receive HTTP response");
+ uint32_t timeout = state->timeout;
+
+#ifdef FF_HAVE_THREADS
+ if (state->thread) {
+ FF_DEBUG("Connection thread is running, waiting for it to complete (timeout=%u ms)", timeout);
+ if (!ffThreadJoin(state->thread, timeout)) {
+ FF_DEBUG("Thread join failed or timed out");
+ return "ffThreadJoin() failed or timeout";
+ }
+ FF_DEBUG("Thread completed successfully");
+ state->thread = 0;
+ }
+#endif
+
+ if (state->sockfd == -1) {
+ FF_DEBUG("Invalid socket, HTTP request might have failed");
+ return "ffNetworkingSendHttpRequest() failed";
+ }
+
+ // Set larger initial receive buffer instead of small repeated receives
+ int rcvbuf = 65536; // 64KB
+ setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
+
+#ifdef __APPLE__
+ // poll for the socket to be readable.
+ // Because of the non-blocking connectx() call, the connection might not be established yet
+ FF_DEBUG("Using poll() to check if socket is readable");
+ {
+ int pollRes = poll(&(struct pollfd) {
+ .fd = state->sockfd,
+ .events = POLLIN },
+ 1,
+ timeout > 0 ? (int) timeout : -1);
+ if (pollRes == 0) {
+ FF_DEBUG("poll() timed out after %u ms", timeout);
+ close(state->sockfd);
+ state->sockfd = -1;
+ return "poll() timeout";
+ } else if (pollRes == -1) {
+ FF_DEBUG("poll() failed: %s", strerror(errno));
+ close(state->sockfd);
+ state->sockfd = -1;
+ return "poll() failed";
+ }
+ }
+ FF_DEBUG("Socket is readable, proceeding to receive data");
+#else
+ if (timeout > 0) {
+ FF_DEBUG("Setting receive timeout: %u ms", timeout);
+ struct timeval timev;
+ timev.tv_sec = timeout / 1000;
+ timev.tv_usec = (__typeof__(timev.tv_usec)) ((timeout % 1000) * 1000); // milliseconds to microseconds
+ setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, &timev, sizeof(timev));
+ }
+#endif
+
+ if (shutdown(state->sockfd, SHUT_WR) == -1) {
+ FF_DEBUG("Failed to shutdown socket send: %s", strerror(errno));
+ // Not a critical error, continue anyway
+ }
+
+ FF_DEBUG("Starting data reception");
+ FF_A_UNUSED int recvCount = 0;
+ uint32_t contentLength = 0;
+ uint32_t headerEnd = 0;
+
+ do {
+ FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u",
+ ++recvCount,
+ buffer->length,
+ ffStrbufGetFree(buffer));
+
+ // We set `Connection: close`, so the server will close the connection when done.
+ // Thus we can use MSG_WAITALL to wait until the buffer is full or the connection is closed.
+ ssize_t received = recv(state->sockfd, buffer->chars + buffer->length, ffStrbufGetFree(buffer), MSG_WAITALL);
+
+ if (received <= 0) {
+ if (received == 0) {
+ FF_DEBUG("Connection closed (received=0)");
+ } else {
+ FF_DEBUG("Reception failed: %s", strerror(errno));
+ }
+ break;
+ }
+
+ buffer->length += (uint32_t) received;
+ buffer->chars[buffer->length] = '\0';
+
+ FF_DEBUG("Successfully received %zd bytes of data, total: %u bytes", received, buffer->length);
+
+ // Check if HTTP header end marker is found
+ if (headerEnd == 0) {
+ char* pHeaderEnd = memmem(buffer->chars, buffer->length, "\r\n\r\n", 4);
+ if (pHeaderEnd) {
+ headerEnd = (uint32_t) (pHeaderEnd - buffer->chars);
+ FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd);
+
+ // Check for Content-Length header to pre-allocate enough memory
+ const char* clHeader = strcasestr(buffer->chars, "Content-Length:");
+ if (clHeader) {
+ contentLength = (uint32_t) strtoul(clHeader + 15, NULL, 10);
+ if (contentLength > 0) {
+ FF_DEBUG("Detected Content-Length: %u, pre-allocating buffer", contentLength);
+ // Ensure buffer is large enough, adding header size and some margin
+ ffStrbufEnsureFree(buffer, contentLength + 16);
+ FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated);
+ }
+ }
+ }
+ }
+ } while (ffStrbufGetFree(buffer) > 0);
+
+ FF_DEBUG("Closing socket: fd=%d", state->sockfd);
+ close(state->sockfd);
+ state->sockfd = -1;
+
+ if (buffer->length == 0) {
+ FF_DEBUG("Server response is empty");
+ return "Empty server response received";
+ }
+
+ if (headerEnd == 0) {
+ FF_DEBUG("No HTTP header end marker found");
+ return "No HTTP header end found";
+ }
+
+ if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n")) {
+ FF_DEBUG("Invalid response: %.40s...", buffer->chars);
+ return "Invalid response";
+ }
+ FF_DEBUG("Received valid HTTP 200 response, content %u bytes, total %u bytes", contentLength, buffer->length);
+
+ if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
+ FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
+ return "Content length mismatch";
+ }
+
+// If compression was used, try to decompress
+#ifdef FF_HAVE_ZLIB
+ if (state->compression) {
+ FF_DEBUG("Content received, checking if compressed");
+ if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) {
+ FF_DEBUG("Decompression failed or invalid compression format");
+ return "Failed to decompress or invalid format";
+ } else {
+ FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length);
+ }
+ }
+#endif
+
+ return NULL;
+}
diff --git a/src/common/impl/networking_windows.c b/src/common/impl/networking_windows.c
new file mode 100644
index 0000000..d3a7752
--- /dev/null
+++ b/src/common/impl/networking_windows.c
@@ -0,0 +1,376 @@
+#include <mswsock.h>
+#include <ws2tcpip.h>
+
+// Must be included after <mswsock.h>
+#include "fastfetch.h"
+#include "common/networking.h"
+#include "common/strutil.h"
+#include "common/debug.h"
+
+static LPFN_CONNECTEX ConnectEx;
+
+static const char* initWsaData(WSADATA* wsaData) {
+ FF_DEBUG("Initializing WinSock");
+ if (WSAStartup(MAKEWORD(2, 2), wsaData) != 0) {
+ FF_DEBUG("WSAStartup() failed");
+ return "WSAStartup() failed";
+ }
+
+ if (LOBYTE(wsaData->wVersion) != 2 || HIBYTE(wsaData->wVersion) != 2) {
+ FF_DEBUG("Invalid wsaData version found: %d.%d", LOBYTE(wsaData->wVersion), HIBYTE(wsaData->wVersion));
+ WSACleanup();
+ return "Invalid wsaData version found";
+ }
+
+ // Dummy socket needed for WSAIoctl
+ SOCKET sockfd = WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, 0);
+ if (sockfd == INVALID_SOCKET) {
+ FF_DEBUG("WSASocketW(AF_INET, SOCK_STREAM) failed");
+ WSACleanup();
+ return "WSASocketW(AF_INET, SOCK_STREAM) failed";
+ }
+
+ DWORD dwBytes;
+ GUID guid = WSAID_CONNECTEX;
+ if (WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &ConnectEx, sizeof(ConnectEx), &dwBytes, NULL, NULL) != 0) {
+ FF_DEBUG("WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed");
+ closesocket(sockfd);
+ WSACleanup();
+ return "WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed";
+ }
+
+ closesocket(sockfd);
+ FF_DEBUG("WinSock initialized successfully");
+
+ return NULL;
+}
+
+const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers) {
+ FF_DEBUG("Preparing to send HTTP request: host=%s, path=%s", host, path);
+
+ if (state->compression) {
+#ifdef FF_HAVE_ZLIB
+ const char* zlibError = ffNetworkingLoadZlibLibrary();
+ // Only enable compression if zlib library is successfully loaded
+ if (zlibError == NULL) {
+ FF_DEBUG("Successfully loaded zlib library, compression enabled");
+ } else {
+ FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError);
+ state->compression = false;
+ }
+#else
+ FF_DEBUG("zlib not supported at build time, compression disabled");
+ state->compression = false;
+#endif
+ } else {
+ FF_DEBUG("Compression disabled");
+ }
+
+ static WSADATA wsaData;
+ if (wsaData.wVersion == 0) {
+ const char* error = initWsaData(&wsaData);
+ if (error != NULL) {
+ wsaData.wVersion = (WORD) -1;
+ FF_DEBUG("WinSock initialization failed: %s", error);
+ return error;
+ }
+ } else if (wsaData.wVersion == (WORD) -1) {
+ FF_DEBUG("WinSock initialization previously failed");
+ return "initWsaData() failed before";
+ }
+
+ ADDRINFOW* addr;
+ ADDRINFOW hints = {
+ .ai_flags = AI_NUMERICSERV,
+ .ai_family = state->ipv6 ? AF_INET6 : AF_INET,
+ .ai_socktype = SOCK_STREAM,
+ };
+
+ wchar_t hostW[256];
+ if (!NT_SUCCESS(RtlUTF8ToUnicodeN(hostW, (ULONG) sizeof(hostW), NULL, host, (ULONG) strlen(host) + 1))) {
+ FF_DEBUG("Failed to convert host to wide string: %s", host);
+ return "Failed to convert host to wide string";
+ }
+
+ FF_DEBUG("Resolving address: %s (%s)", host, state->ipv6 ? "IPv6" : "IPv4");
+ if (GetAddrInfoW(hostW, L"80", &hints, &addr) != 0) {
+ FF_DEBUG("GetAddrInfoW() failed");
+ return "GetAddrInfoW() failed";
+ }
+
+ state->sockfd = WSASocketW(addr->ai_family, addr->ai_socktype, addr->ai_protocol, NULL, 0, 0);
+ if (state->sockfd == INVALID_SOCKET) {
+ FF_DEBUG("WSASocketW() failed");
+ FreeAddrInfoW(addr);
+ return "WSASocketW() failed";
+ }
+
+ DWORD flag = 1;
+#ifdef TCP_NODELAY
+ // Enable TCP_NODELAY to disable Nagle's algorithm
+ if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)) != 0) {
+ FF_DEBUG("Failed to set TCP_NODELAY: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ } else {
+ FF_DEBUG("Successfully disabled Nagle's algorithm");
+ }
+#endif
+
+ // Set timeout if needed
+ if (state->timeout > 0) {
+ FF_DEBUG("Setting connection timeout: %u ms", state->timeout);
+ setsockopt(state->sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char*) &state->timeout, sizeof(state->timeout));
+ }
+
+ // ConnectEx requires the socket to be initially bound
+ if ((state->ipv6
+ ? bind(state->sockfd, (SOCKADDR*) &(struct sockaddr_in6) {
+ .sin6_family = AF_INET6,
+ .sin6_addr = in6addr_any,
+ },
+ sizeof(struct sockaddr_in6))
+ : bind(state->sockfd, (SOCKADDR*) &(struct sockaddr_in) {
+ .sin_family = AF_INET,
+ .sin_addr.s_addr = INADDR_ANY,
+ },
+ sizeof(struct sockaddr_in))) != 0) {
+ FF_DEBUG("bind() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ closesocket(state->sockfd);
+ FreeAddrInfoW(addr);
+ state->sockfd = INVALID_SOCKET;
+ return "bind() failed";
+ }
+
+ // Initialize overlapped structure with WSA event for asynchronous I/O
+ state->overlapped = (OVERLAPPED) {
+ .hEvent = WSACreateEvent()
+ };
+
+ if (state->overlapped.hEvent == WSA_INVALID_EVENT) {
+ FF_DEBUG("WSACreateEvent() failed");
+ closesocket(state->sockfd);
+ FreeAddrInfoW(addr);
+ state->sockfd = INVALID_SOCKET;
+ return "WSACreateEvent() failed";
+ }
+
+ // Build HTTP command
+ ffStrbufInitA(&state->command, 128);
+ ffStrbufAppendS(&state->command, "GET ");
+ ffStrbufAppendS(&state->command, path);
+ ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: ");
+ ffStrbufAppendS(&state->command, host);
+ ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly request connection closure
+
+ // Add compression support if enabled
+ if (state->compression) {
+ FF_DEBUG("Enabling HTTP content compression");
+ ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n");
+ }
+
+ ffStrbufAppendS(&state->command, headers);
+ ffStrbufAppendS(&state->command, "\r\n");
+
+#ifdef TCP_FASTOPEN
+ if (state->tfo) {
+ // Set TCP Fast Open
+ flag = 1;
+ if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_FASTOPEN, (char*) &flag, sizeof(flag)) != 0) {
+ FF_DEBUG("Failed to set TCP_FASTOPEN option: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ } else {
+ FF_DEBUG("Successfully set TCP_FASTOPEN option");
+ }
+ } else {
+ FF_DEBUG("TCP Fast Open disabled");
+ }
+#endif
+
+ FF_DEBUG("Using ConnectEx to send %u bytes of data", state->command.length);
+ DWORD sent = 0;
+ BOOL result = ConnectEx(state->sockfd, addr->ai_addr, (int) addr->ai_addrlen, state->command.chars, state->command.length, &sent, &state->overlapped);
+
+ FreeAddrInfoW(addr);
+ addr = NULL;
+
+ if (!result) {
+ if (WSAGetLastError() != WSA_IO_PENDING) {
+ FF_DEBUG("ConnectEx() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ WSACloseEvent(state->overlapped.hEvent);
+ closesocket(state->sockfd);
+ state->sockfd = INVALID_SOCKET;
+ ffStrbufDestroy(&state->command);
+ return "ConnectEx() failed";
+ } else {
+ FF_DEBUG("ConnectEx() pending");
+ }
+ } else {
+ FF_DEBUG("ConnectEx() succeeded, sent %u bytes of data", (unsigned) sent);
+ }
+
+ // No need to cleanup state fields here since we need them in the receive function
+ return NULL;
+}
+
+const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer) {
+ assert(buffer->allocated > 0);
+ FF_DEBUG("Preparing to receive HTTP response");
+
+ if (state->sockfd == INVALID_SOCKET) {
+ FF_DEBUG("Invalid socket, HTTP request might have failed");
+ return "ffNetworkingSendHttpRequest() failed";
+ }
+
+ uint32_t timeout = state->timeout;
+ if (timeout > 0) {
+ FF_DEBUG("WSAWaitForMultipleEvents with timeout: %u ms", timeout);
+ DWORD result = WSAWaitForMultipleEvents(1, &state->overlapped.hEvent, TRUE, timeout, FALSE);
+ if (result != WSA_WAIT_EVENT_0) {
+ if (result == WSA_WAIT_TIMEOUT) {
+ FF_DEBUG("WSAWaitForMultipleEvents timed out");
+ } else {
+ FF_DEBUG("WSAWaitForMultipleEvents failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ }
+ if (CancelIoEx((HANDLE) state->sockfd, &state->overlapped)) {
+ WSAWaitForMultipleEvents(1, &state->overlapped.hEvent, TRUE, 10, TRUE);
+ }
+ WSACloseEvent(state->overlapped.hEvent);
+ closesocket(state->sockfd);
+ ffStrbufDestroy(&state->command);
+ return "WSAWaitForMultipleEvents() failed or timeout";
+ }
+ }
+
+ DWORD transfer, flags;
+ if (!WSAGetOverlappedResult(state->sockfd, &state->overlapped, &transfer, TRUE, &flags)) {
+ FF_DEBUG("WSAGetOverlappedResult failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ closesocket(state->sockfd);
+ WSACloseEvent(state->overlapped.hEvent);
+ ffStrbufDestroy(&state->command);
+ return "WSAGetOverlappedResult() failed";
+ }
+ FF_DEBUG("WSAGetOverlappedResult succeeded, %u bytes sent", (unsigned) transfer);
+ ffStrbufDestroy(&state->command);
+ WSACloseEvent(state->overlapped.hEvent);
+ state->overlapped.hEvent = NULL;
+
+ if (setsockopt(state->sockfd, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, NULL, 0) != 0) {
+ FF_DEBUG("Failed to update connect context: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ // Not a critical error, continue anyway
+ }
+
+ if (shutdown(state->sockfd, SD_SEND) == SOCKET_ERROR) {
+ FF_DEBUG("Failed to shutdown socket send: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ // Not a critical error, continue anyway
+ }
+
+ if (timeout > 0) {
+ FF_DEBUG("Setting receive timeout: %u ms", timeout);
+ setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*) &timeout, sizeof(timeout));
+ }
+
+ // Set larger receive buffer for better performance
+ int rcvbuf = 65536; // 64KB
+ if (setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, (const char*) &rcvbuf, sizeof(rcvbuf))) {
+ FF_DEBUG("Failed to set SO_RCVBUF: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ // Not a critical error, continue anyway
+ }
+
+ FF_DEBUG("Starting data reception");
+ FF_A_UNUSED int recvCount = 0;
+ uint32_t contentLength = 0;
+ uint32_t headerEnd = 0;
+
+ do {
+ FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u",
+ ++recvCount,
+ buffer->length,
+ ffStrbufGetFree(buffer));
+
+ DWORD received = 0, recvFlags = 0;
+ int recvResult = WSARecv(state->sockfd, &(WSABUF) {
+ .buf = buffer->chars + buffer->length,
+ .len = (ULONG) ffStrbufGetFree(buffer),
+ },
+ 1,
+ &received,
+ &recvFlags,
+ NULL,
+ NULL);
+
+ if (recvResult == SOCKET_ERROR || received == 0) {
+ if (recvResult == 0 && received == 0) {
+ FF_DEBUG("Connection closed (received=0)");
+ } else {
+ FF_DEBUG("Reception failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError()));
+ }
+ break;
+ }
+
+ buffer->length += (uint32_t) received;
+ buffer->chars[buffer->length] = '\0';
+
+ FF_DEBUG("Successfully received %u bytes of data, total: %u bytes", (unsigned) received, buffer->length);
+
+ // Check if HTTP header end marker is found
+ if (headerEnd == 0) {
+ char* pHeaderEnd = strstr(buffer->chars, "\r\n\r\n");
+ if (pHeaderEnd) {
+ headerEnd = (uint32_t) (pHeaderEnd - buffer->chars);
+ FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd);
+
+ // Check for Content-Length header to pre-allocate enough memory
+ const char* clHeader = strcasestr(buffer->chars, "Content-Length:");
+ if (clHeader) {
+ contentLength = (uint32_t) strtoul(clHeader + 15, NULL, 10);
+ if (contentLength > 0) {
+ FF_DEBUG("Detected Content-Length: %u, pre-allocating buffer", contentLength);
+ // Ensure buffer is large enough, adding header size and some margin
+ ffStrbufEnsureFree(buffer, contentLength + 16);
+ FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated);
+ }
+ }
+ }
+ }
+ } while (ffStrbufGetFree(buffer) > 0);
+
+ FF_DEBUG("Closing socket: fd=%u", (unsigned) state->sockfd);
+ closesocket(state->sockfd);
+ state->sockfd = INVALID_SOCKET;
+
+ if (buffer->length == 0) {
+ FF_DEBUG("Server response is empty");
+ return "Empty server response received";
+ }
+
+ if (headerEnd == 0) {
+ FF_DEBUG("No HTTP header end marker found");
+ return "No HTTP header end found";
+ }
+
+ if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n")) {
+ FF_DEBUG("Invalid response: %.40s...", buffer->chars);
+ return "Invalid response";
+ }
+ FF_DEBUG("Received valid HTTP 200 response, content length: %u bytes, total length: %u bytes",
+ contentLength,
+ buffer->length);
+
+ if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) {
+ FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4);
+ return "Content length mismatch";
+ }
+
+// If compression was used, try to decompress
+#ifdef FF_HAVE_ZLIB
+ if (state->compression) {
+ FF_DEBUG("Content received, checking if compressed");
+ if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) {
+ FF_DEBUG("Decompression failed or invalid compression format");
+ return "Failed to decompress or invalid format";
+ } else {
+ FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length);
+ }
+ }
+#endif
+
+ return NULL;
+}
diff --git a/src/common/impl/option.c b/src/common/impl/option.c
new file mode 100644
index 0000000..3d592d0
--- /dev/null
+++ b/src/common/impl/option.c
@@ -0,0 +1,185 @@
+#include "fastfetch.h"
+#include "common/option.h"
+#include "common/color.h"
+#include "common/strutil.h"
+
+#include <limits.h>
+
+// Return start position of the inner key if the argument key belongs to the module specified, NULL otherwise
+const char* ffOptionTestPrefix(const char* argumentKey, const char* moduleName) {
+ assert(argumentKey && moduleName);
+
+ const char* subKey = argumentKey;
+ if (!(subKey[0] == '-' && subKey[1] == '-')) {
+ return NULL;
+ }
+
+ subKey += 2;
+ uint32_t moduleNameLen = (uint32_t) strlen(moduleName);
+ if (strncasecmp(subKey, moduleName, moduleNameLen) != 0) {
+ return NULL;
+ }
+
+ subKey += moduleNameLen;
+
+ if (subKey[0] == '\0') {
+ return subKey;
+ }
+
+ if (subKey[0] != '-') {
+ return NULL;
+ }
+
+ subKey += 1;
+
+ return subKey;
+}
+
+void ffOptionParseString(const char* argumentKey, const char* value, FFstrbuf* buffer) {
+ if (value == NULL) {
+ fprintf(stderr, "Error: usage: %s <str>\n", argumentKey);
+ exit(477);
+ }
+
+ ffStrbufSetS(buffer, value);
+}
+
+uint32_t ffOptionParseUInt32(const char* argumentKey, const char* value) {
+ if (value == NULL) {
+ fprintf(stderr, "Error: usage: %s <num>\n", argumentKey);
+ exit(480);
+ }
+
+ char* end;
+ unsigned long num = strtoul(value, &end, 10);
+ if (value[0] == '-' || *end != '\0' || num > UINT32_MAX) {
+ fprintf(stderr, "Error: usage: %s <num>\n", argumentKey);
+ exit(479);
+ }
+
+ return (uint32_t) num;
+}
+
+int32_t ffOptionParseInt32(const char* argumentKey, const char* value) {
+ if (value == NULL) {
+ fprintf(stderr, "Error: usage: %s <num>\n", argumentKey);
+ exit(480);
+ }
+
+ char* end;
+ long num = strtol(value, &end, 10);
+ if (*end != '\0' || num < INT32_MIN || num > INT32_MAX) {
+ fprintf(stderr, "Error: usage: %s <num>\n", argumentKey);
+ exit(479);
+ }
+
+ return (int32_t) num;
+}
+
+int ffOptionParseEnum(const char* argumentKey, const char* requestedKey, FFKeyValuePair pairs[]) {
+ if (requestedKey == NULL) {
+ fprintf(stderr, "Error: usage: %s <value>\n", argumentKey);
+ exit(476);
+ }
+
+ for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) {
+ if (ffStrEqualsIgnCase(requestedKey, pPair->key)) {
+ return pPair->value;
+ }
+ }
+
+ fprintf(stderr, "Error: unknown %s value: %s\n", argumentKey, requestedKey);
+ exit(478);
+}
+
+bool ffOptionParseBoolean(const char* str) {
+ return (
+ !ffStrSet(str) ||
+ ffStrEqualsIgnCase(str, "true") ||
+ ffStrEqualsIgnCase(str, "yes") ||
+ ffStrEqualsIgnCase(str, "on") ||
+ ffStrEqualsIgnCase(str, "1"));
+}
+
+void ffOptionParseColorNoClear(const char* value, FFstrbuf* buffer) {
+ if (!value || value[0] == '\0') {
+ return;
+ }
+
+ // If value is already an ANSI escape code, use it
+ if (value[0] == '\e' && value[1] == '[') {
+ ffStrbufAppendS(buffer, value + 2);
+ ffStrbufTrimRight(buffer, 'm');
+ return;
+ }
+
+ ffStrbufEnsureFree(buffer, 63);
+
+ while (*value != '\0') {
+#define FF_APPEND_COLOR_CODE_COND(prefix, code) \
+ if (ffStrStartsWithIgnCase(value, #prefix)) { \
+ ffStrbufAppendS(buffer, code); \
+ value += strlen(#prefix); \
+ continue; \
+ }
+#define FF_APPEND_COLOR_PROP_COND(prefix, prop) \
+ if (ffStrStartsWithIgnCase(value, #prefix)) { \
+ if (instance.config.display.prop.length) ffStrbufAppend(buffer, &instance.config.display.prop); \
+ else ffStrbufAppendS(buffer, FF_COLOR_FG_DEFAULT); \
+ value += strlen(#prefix); \
+ continue; \
+ }
+
+ if (ffCharIsEnglishAlphabet(value[0])) {
+ FF_APPEND_COLOR_CODE_COND(reset_, FF_COLOR_MODE_RESET)
+ else FF_APPEND_COLOR_CODE_COND(bold_, FF_COLOR_MODE_BOLD) else FF_APPEND_COLOR_CODE_COND(bright_, FF_COLOR_MODE_BOLD) else FF_APPEND_COLOR_CODE_COND(dim_, FF_COLOR_MODE_DIM) else FF_APPEND_COLOR_CODE_COND(italic_, FF_COLOR_MODE_ITALIC) else FF_APPEND_COLOR_CODE_COND(underline_, FF_COLOR_MODE_UNDERLINE) else FF_APPEND_COLOR_CODE_COND(blink_, FF_COLOR_MODE_BLINK) else FF_APPEND_COLOR_CODE_COND(inverse_, FF_COLOR_MODE_INVERSE) else FF_APPEND_COLOR_CODE_COND(hidden_, FF_COLOR_MODE_HIDDEN) else FF_APPEND_COLOR_CODE_COND(strike_, FF_COLOR_MODE_STRIKETHROUGH) else FF_APPEND_COLOR_CODE_COND(black, FF_COLOR_FG_BLACK) else FF_APPEND_COLOR_CODE_COND(red, FF_COLOR_FG_RED) else FF_APPEND_COLOR_CODE_COND(green, FF_COLOR_FG_GREEN) else FF_APPEND_COLOR_CODE_COND(yellow, FF_COLOR_FG_YELLOW) else FF_APPEND_COLOR_CODE_COND(blue, FF_COLOR_FG_BLUE) else FF_APPEND_COLOR_CODE_COND(magenta, FF_COLOR_FG_MAGENTA) else FF_APPEND_COLOR_CODE_COND(cyan, FF_COLOR_FG_CYAN) else FF_APPEND_COLOR_CODE_COND(white, FF_COLOR_FG_WHITE) else FF_APPEND_COLOR_CODE_COND(default, FF_COLOR_FG_DEFAULT) else FF_APPEND_COLOR_CODE_COND(light_black, FF_COLOR_FG_LIGHT_BLACK) else FF_APPEND_COLOR_CODE_COND(light_red, FF_COLOR_FG_LIGHT_RED) else FF_APPEND_COLOR_CODE_COND(light_green, FF_COLOR_FG_LIGHT_GREEN) else FF_APPEND_COLOR_CODE_COND(light_yellow, FF_COLOR_FG_LIGHT_YELLOW) else FF_APPEND_COLOR_CODE_COND(light_blue, FF_COLOR_FG_LIGHT_BLUE) else FF_APPEND_COLOR_CODE_COND(light_magenta, FF_COLOR_FG_LIGHT_MAGENTA) else FF_APPEND_COLOR_CODE_COND(light_cyan, FF_COLOR_FG_LIGHT_CYAN) else FF_APPEND_COLOR_CODE_COND(light_white, FF_COLOR_FG_LIGHT_WHITE) else FF_APPEND_COLOR_PROP_COND(keys, colorKeys) else FF_APPEND_COLOR_PROP_COND(title, colorTitle) else FF_APPEND_COLOR_PROP_COND(output, colorOutput) else FF_APPEND_COLOR_PROP_COND(separator, colorSeparator) else {
+ fprintf(stderr, "Error: invalid color code found: %s\n", value);
+ exit(479);
+ }
+ } else if (value[0] == '@') {
+ // Xterm 256 color
+ ++value;
+ char* pend = NULL;
+ uint32_t color = (uint32_t) strtoul(value, &pend, 10);
+ if (pend == value || color > 255) {
+ fprintf(stderr, "Error: invalid 256 color code found: %s\n", value);
+ exit(479);
+ }
+
+ ffStrbufAppendS(buffer, FF_COLOR_FG_256);
+ ffStrbufAppendUInt(buffer, color);
+ value = pend;
+ continue;
+ } else if (value[0] == '#') {
+ // RGB color
+ ++value;
+ char* pend = NULL;
+ uint32_t rgb = (uint32_t) strtoul(value, &pend, 16);
+ if (pend == value) {
+ fprintf(stderr, "Error: invalid RGB color code found: %s\n", value);
+ exit(479);
+ }
+ if (pend - value > 6) {
+ fprintf(stderr, "Error: RGB color code too long: %s\n", value);
+ exit(479);
+ } else if (pend - value == 3) {
+ rgb = ((rgb & 0xF00) >> 8) * 0x110000 +
+ ((rgb & 0x0F0) >> 4) * 0x001100 +
+ ((rgb & 0x00F) >> 0) * 0x000011;
+ } else if (pend - value != 6) {
+ fprintf(stderr, "Error: invalid RGB color code length: %s\n", value);
+ exit(479);
+ }
+
+ uint32_t r = rgb >> 16, g = (rgb >> 8) & 0xFF, b = rgb & 0xFF;
+ ffStrbufAppendF(buffer, FF_COLOR_FG_RGB "%u;%u;%u", r, g, b);
+ value = pend;
+ continue;
+ }
+ ffStrbufAppendC(buffer, *value);
+ ++value;
+
+#undef FF_APPEND_COLOR_CODE_COND
+#undef FF_APPEND_COLOR_PROP_COND
+ }
+}
diff --git a/src/common/impl/parsing.c b/src/common/impl/parsing.c
new file mode 100644
index 0000000..e88d10c
--- /dev/null
+++ b/src/common/impl/parsing.c
@@ -0,0 +1,136 @@
+#include "fastfetch.h"
+#include "common/parsing.h"
+
+#ifdef _WIN32
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wformat"
+#endif
+
+void ffParseSemver(FFstrbuf* buffer, const FFstrbuf* major, const FFstrbuf* minor, const FFstrbuf* patch) {
+ if (major->length > 0) {
+ ffStrbufAppend(buffer, major);
+ } else if (minor->length > 0 || patch->length > 0) {
+ ffStrbufAppendC(buffer, '1');
+ }
+
+ if (minor->length == 0 && patch->length == 0) {
+ return;
+ }
+
+ ffStrbufAppendC(buffer, '.');
+
+ if (minor->length > 0) {
+ ffStrbufAppend(buffer, minor);
+ } else if (patch->length > 0) {
+ ffStrbufAppendC(buffer, '0');
+ }
+
+ if (patch->length == 0) {
+ return;
+ }
+
+ ffStrbufAppendC(buffer, '.');
+
+ ffStrbufAppend(buffer, patch);
+}
+
+int8_t ffVersionCompare(const FFVersion* version1, const FFVersion* version2) {
+ if (version1->major != version2->major) {
+ return version1->major > version2->major ? 1 : -1;
+ }
+
+ if (version1->minor != version2->minor) {
+ return version1->minor > version2->minor ? 1 : -1;
+ }
+
+ if (version1->patch != version2->patch) {
+ return version1->patch > version2->patch ? 1 : -1;
+ }
+
+ return 0;
+}
+
+void ffVersionToPretty(const FFVersion* version, FFstrbuf* pretty) {
+ if (version->major > 0 || version->minor > 0 || version->patch > 0) {
+ ffStrbufAppendUInt(pretty, version->major);
+ }
+
+ if (version->minor > 0 || version->patch > 0) {
+ ffStrbufAppendC(pretty, '.');
+ ffStrbufAppendUInt(pretty, version->minor);
+ }
+
+ if (version->patch > 0) {
+ ffStrbufAppendC(pretty, '.');
+ ffStrbufAppendUInt(pretty, version->patch);
+ }
+}
+
+void ffParseGTK(FFstrbuf* buffer, const FFstrbuf* gtk2, const FFstrbuf* gtk3, const FFstrbuf* gtk4) {
+ if (gtk2->length > 0 && gtk3->length > 0 && gtk4->length > 0) {
+ if ((ffStrbufIgnCaseEqual(gtk2, gtk3)) && (ffStrbufIgnCaseEqual(gtk2, gtk4))) {
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK2/3/4]");
+ } else if (ffStrbufIgnCaseEqual(gtk2, gtk3)) {
+ ffStrbufAppend(buffer, gtk3);
+ ffStrbufAppendS(buffer, " [GTK2/3], ");
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK4]");
+ } else if (ffStrbufIgnCaseEqual(gtk3, gtk4)) {
+ ffStrbufAppend(buffer, gtk2);
+ ffStrbufAppendS(buffer, " [GTK2], ");
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK3/4]");
+ } else {
+ ffStrbufAppend(buffer, gtk2);
+ ffStrbufAppendS(buffer, " [GTK2], ");
+ ffStrbufAppend(buffer, gtk3);
+ ffStrbufAppendS(buffer, " [GTK3], ");
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK4]");
+ }
+ } else if (gtk2->length > 0 && gtk3->length > 0) {
+ if (ffStrbufIgnCaseEqual(gtk2, gtk3)) {
+ ffStrbufAppend(buffer, gtk3);
+ ffStrbufAppendS(buffer, " [GTK2/3]");
+ } else {
+ ffStrbufAppend(buffer, gtk2);
+ ffStrbufAppendS(buffer, " [GTK2], ");
+ ffStrbufAppend(buffer, gtk3);
+ ffStrbufAppendS(buffer, " [GTK3]");
+ }
+ } else if (gtk2->length > 0 && gtk4->length > 0) {
+ if (ffStrbufIgnCaseEqual(gtk2, gtk4)) {
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK2/4]");
+ } else {
+ ffStrbufAppend(buffer, gtk2);
+ ffStrbufAppendS(buffer, " [GTK2], ");
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK4]");
+ }
+ } else if (gtk3->length > 0 && gtk4->length > 0) {
+ if (ffStrbufIgnCaseEqual(gtk3, gtk4)) {
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK3/4]");
+ } else {
+ ffStrbufAppend(buffer, gtk3);
+ ffStrbufAppendS(buffer, " [GTK3], ");
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK4]");
+ }
+ } else if (gtk2->length > 0) {
+ ffStrbufAppend(buffer, gtk2);
+ ffStrbufAppendS(buffer, " [GTK2]");
+ } else if (gtk3->length > 0) {
+ ffStrbufAppend(buffer, gtk3);
+ ffStrbufAppendS(buffer, " [GTK3]");
+ } else if (gtk4->length > 0) {
+ ffStrbufAppend(buffer, gtk4);
+ ffStrbufAppendS(buffer, " [GTK4]");
+ }
+}
+
+#ifdef _WIN32
+ #pragma GCC diagnostic pop
+#endif
diff --git a/src/common/impl/path.c b/src/common/impl/path.c
new file mode 100644
index 0000000..c97210e
--- /dev/null
+++ b/src/common/impl/path.c
@@ -0,0 +1,277 @@
+#include "common/path.h"
+#include "common/io.h"
+#include "common/arrutil.h"
+
+#if !_WIN32
+const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
+ char* path = getenv("PATH");
+ if (!path) {
+ return "$PATH not set";
+ }
+
+ #ifdef _WIN32
+ const bool appendExe = !ffStrEndsWithIgnCase(name, ".exe");
+ #endif
+
+ for (char* token = path; *token; path = token + 1) {
+ token = strchr(path,
+ #ifdef _WIN32
+ ';'
+ #else
+ ':'
+ #endif
+ );
+ if (!token) {
+ token = path + strlen(path);
+ }
+
+ ffStrbufSetNS(result, (uint32_t) (token - path), path);
+ ffStrbufEnsureEndsWithC(result,
+ #ifdef _WIN32
+ '\\'
+ #else
+ '/'
+ #endif
+ );
+ ffStrbufAppendS(result, name);
+ #ifdef _WIN32
+ if (appendExe) {
+ ffStrbufAppendS(result, ".exe");
+ }
+ if (!ffPathExists(result->chars, FF_PATHTYPE_FILE)) {
+ continue;
+ }
+ #else
+ if (access(result->chars, X_OK) != 0) {
+ continue;
+ }
+ #endif
+
+ return NULL;
+ }
+ ffStrbufClear(result);
+ return "Executable not found";
+}
+#else
+ #include <windows.h>
+ #include <winioctl.h>
+ #include <errno.h>
+ #include <stdalign.h>
+
+const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) {
+ char buffer[MAX_PATH + 1];
+ DWORD length = SearchPathA(NULL, name, ".exe", sizeof(buffer), buffer, NULL);
+ if (length == 0) {
+ ffStrbufClear(result);
+ return "Executable not found";
+ }
+ ffStrbufSetS(result, buffer);
+ return NULL;
+}
+
+static inline int winerr2Errno(DWORD err) {
+ switch (err) {
+ case ERROR_FILE_NOT_FOUND:
+ case ERROR_PATH_NOT_FOUND:
+ case ERROR_INVALID_NAME:
+ return ENOENT;
+ case ERROR_ACCESS_DENIED:
+ case ERROR_SHARING_VIOLATION:
+ case ERROR_LOCK_VIOLATION:
+ return EACCES;
+ case ERROR_BUFFER_OVERFLOW:
+ case ERROR_INSUFFICIENT_BUFFER:
+ return ENAMETOOLONG;
+ case ERROR_INVALID_PARAMETER:
+ case ERROR_NOT_A_REPARSE_POINT:
+ return EINVAL;
+ default:
+ return EIO;
+ }
+}
+
+char* frealpath(HANDLE hFile, char* resolved_name) {
+ if (__builtin_expect(hFile == INVALID_HANDLE_VALUE || !hFile, false)) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ wchar_t resolvedNameW[MAX_PATH + 4]; /* +4 for "\\\\?\\" prefix */
+ DWORD lenW = GetFinalPathNameByHandleW(hFile, resolvedNameW, (DWORD) ARRAY_SIZE(resolvedNameW), FILE_NAME_NORMALIZED);
+
+ if (lenW == 0) {
+ errno = winerr2Errno(GetLastError());
+ return NULL;
+ }
+ if (lenW >= ARRAY_SIZE(resolvedNameW)) {
+ errno = E2BIG;
+ return NULL;
+ }
+ lenW++; // Include null terminator
+
+ wchar_t* srcW = resolvedNameW;
+ DWORD srcLenW = lenW;
+
+ if (srcLenW >= 8 && wcsncmp(resolvedNameW, L"\\\\?\\UNC\\", 8) == 0) {
+ /* Convert "\\?\UNC\server\share" to "\\server\share" */
+ srcW += 6;
+ srcLenW -= 6;
+ *srcW = L'\\';
+ } else if (srcLenW >= 4 && wcsncmp(resolvedNameW, L"\\\\?\\", 4) == 0) {
+ srcW += 4;
+ srcLenW -= 4;
+ }
+
+ if (resolved_name) {
+ ULONG outBytes = 0;
+ if (!NT_SUCCESS(RtlUnicodeToUTF8N(resolved_name, MAX_PATH, &outBytes, srcW, (ULONG) (srcLenW * sizeof(wchar_t))))) {
+ errno = E2BIG;
+ return NULL;
+ }
+ } else {
+ /* UTF-8 worst-case: up to 4 bytes per UTF-16 code unit */
+ char tmp[(MAX_PATH + 4) * 4];
+ ULONG outBytes = 0;
+
+ if (!NT_SUCCESS(RtlUnicodeToUTF8N(tmp, (ULONG) sizeof(tmp), &outBytes, srcW, (ULONG) (srcLenW * sizeof(wchar_t))))) {
+ errno = E2BIG;
+ return NULL;
+ }
+
+ resolved_name = (char*) malloc(outBytes);
+ if (!resolved_name) {
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ memcpy(resolved_name, tmp, outBytes);
+ }
+
+ return resolved_name;
+}
+
+char* realpath(const char* __restrict file_name, char* __restrict resolved_name) {
+ if (!file_name) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ wchar_t fileNameW[MAX_PATH];
+ ULONG lenBytes = 0;
+
+ if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &lenBytes, file_name, (ULONG) strlen(file_name) + 1))) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileW(
+ fileNameW,
+ 0,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ NULL,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS,
+ NULL);
+
+ if (hFile == INVALID_HANDLE_VALUE) {
+ errno = winerr2Errno(GetLastError());
+ return NULL;
+ }
+
+ return frealpath(hFile, resolved_name);
+}
+
+ssize_t freadlink(HANDLE hFile, char* buf, size_t bufsiz) {
+ if (__builtin_expect(hFile == INVALID_HANDLE_VALUE || !buf || bufsiz == 0, false)) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ alignas(REPARSE_DATA_BUFFER) BYTE reparseBuf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
+ DWORD bytesReturned = 0;
+ if (!DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, NULL, 0, reparseBuf, (DWORD) sizeof(reparseBuf), &bytesReturned, NULL)) {
+ errno = winerr2Errno(GetLastError());
+ return -1;
+ }
+
+ REPARSE_DATA_BUFFER* rp = (REPARSE_DATA_BUFFER*) reparseBuf;
+ const wchar_t* targetW = NULL;
+ USHORT targetBytes = 0;
+
+ if (rp->ReparseTag == IO_REPARSE_TAG_SYMLINK) {
+ if (rp->SymbolicLinkReparseBuffer.PrintNameLength > 0) {
+ targetW = rp->SymbolicLinkReparseBuffer.PathBuffer +
+ (rp->SymbolicLinkReparseBuffer.PrintNameOffset / sizeof(wchar_t));
+ targetBytes = rp->SymbolicLinkReparseBuffer.PrintNameLength;
+ } else {
+ targetW = rp->SymbolicLinkReparseBuffer.PathBuffer +
+ (rp->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(wchar_t));
+ targetBytes = rp->SymbolicLinkReparseBuffer.SubstituteNameLength;
+
+ if (targetBytes >= 8 &&
+ wcsncmp(targetW, L"\\??\\", 4) == 0) {
+ targetW += 4;
+ targetBytes -= 8;
+ }
+ }
+ } else if (rp->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) {
+ if (rp->MountPointReparseBuffer.PrintNameLength > 0) {
+ targetW = rp->MountPointReparseBuffer.PathBuffer +
+ (rp->MountPointReparseBuffer.PrintNameOffset / sizeof(wchar_t));
+ targetBytes = rp->MountPointReparseBuffer.PrintNameLength;
+ } else {
+ targetW = rp->MountPointReparseBuffer.PathBuffer +
+ (rp->MountPointReparseBuffer.SubstituteNameOffset / sizeof(wchar_t));
+ targetBytes = rp->MountPointReparseBuffer.SubstituteNameLength;
+
+ if (targetBytes >= 8 &&
+ wcsncmp(targetW, L"\\??\\", 4) == 0) {
+ targetW += 4;
+ targetBytes -= 8;
+ }
+ }
+ } else {
+ errno = EINVAL;
+ return -1;
+ }
+
+ ULONG outBytes = 0;
+ if (!NT_SUCCESS(RtlUnicodeToUTF8N(buf, (ULONG) bufsiz, &outBytes, targetW, targetBytes))) {
+ errno = E2BIG;
+ return -1;
+ }
+
+ // Not null-terminated
+ return (ssize_t) outBytes;
+}
+
+ssize_t readlink(const char* path, char* buf, size_t bufsiz) {
+ if (!path || !buf || bufsiz == 0) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ wchar_t pathW[MAX_PATH];
+ ULONG pathWBytes = 0;
+ if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathW, (ULONG) sizeof(pathW), &pathWBytes, path, (ULONG) strlen(path) + 1))) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileW(
+ pathW,
+ 0,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
+ NULL,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
+ NULL);
+
+ if (hFile == INVALID_HANDLE_VALUE) {
+ errno = winerr2Errno(GetLastError());
+ return -1;
+ }
+
+ return freadlink(hFile, buf, bufsiz);
+}
+#endif
diff --git a/src/common/impl/percent.c b/src/common/impl/percent.c
new file mode 100644
index 0000000..266588c
--- /dev/null
+++ b/src/common/impl/percent.c
@@ -0,0 +1,293 @@
+#include "fastfetch.h"
+#include "common/percent.h"
+#include "common/color.h"
+#include "common/option.h"
+#include "common/jsonconfig.h"
+#include "common/textModifier.h"
+#include "common/strutil.h"
+
+static void appendOutputColor(FFstrbuf* buffer, const FFModuleArgs* module) {
+ if (module->outputColor.length) {
+ ffStrbufAppendF(buffer, "\e[%sm", module->outputColor.chars);
+ } else if (instance.config.display.colorOutput.length) {
+ ffStrbufAppendF(buffer, "\e[%sm", instance.config.display.colorOutput.chars);
+ }
+}
+
+const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFlags* result) {
+ if (yyjson_is_uint(jsonVal)) {
+ *result = (FFPercentageTypeFlags) yyjson_get_uint(jsonVal);
+ return NULL;
+ }
+ if (yyjson_is_arr(jsonVal)) {
+ FFPercentageTypeFlags flags = 0;
+
+ yyjson_val* item;
+ size_t idx, max;
+ yyjson_arr_foreach (jsonVal, idx, max, item) {
+ const char* flag = yyjson_get_str(item);
+ if (!flag) {
+ return "Error: percent.type: invalid flag string";
+ }
+ if (ffStrEqualsIgnCase(flag, "num")) {
+ flags |= FF_PERCENTAGE_TYPE_NUM_BIT;
+ } else if (ffStrEqualsIgnCase(flag, "bar")) {
+ flags |= FF_PERCENTAGE_TYPE_BAR_BIT;
+ } else if (ffStrEqualsIgnCase(flag, "hide-others")) {
+ flags |= FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT;
+ } else if (ffStrEqualsIgnCase(flag, "num-color")) {
+ flags |= FF_PERCENTAGE_TYPE_NUM_COLOR_BIT;
+ } else if (ffStrEqualsIgnCase(flag, "bar-monochrome")) {
+ flags |= FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT;
+ } else {
+ return "Error: percent.type: unknown flag string";
+ }
+ }
+
+ *result = flags;
+ return NULL;
+ }
+
+ return "Error: usage: percent.type must be a number or an array of strings";
+}
+
+void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, const FFModuleArgs* module) {
+ uint8_t green = config.green, yellow = config.yellow;
+ assert(green <= 100 && yellow <= 100);
+
+ const FFOptionsDisplay* options = &instance.config.display;
+
+ const bool borderAsValue = options->barBorderLeftElapsed.length && options->barBorderRightElapsed.length;
+
+ if (!borderAsValue && options->barBorderLeft.length) {
+ if (!options->pipe && options->barColorBorder.length > 0) {
+ ffStrbufAppendF(buffer, "\e[%sm", options->barColorBorder.chars);
+ }
+ ffStrbufAppend(buffer, &options->barBorderLeft);
+ }
+
+ if (percent == -DBL_MAX) {
+ // Use total color for simplification
+ if (!options->pipe && options->barColorTotal.length > 0) {
+ ffStrbufAppendS(buffer, "\e[" FF_COLOR_FG_LIGHT_BLACK "m");
+ }
+
+ for (uint8_t i = 0; i < options->barWidth; ++i) {
+ ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeft : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRight
+ : &options->barCharTotal);
+ }
+ } else {
+ const char* colorGreen = options->percentColorGreen.chars;
+ const char* colorYellow = options->percentColorYellow.chars;
+ const char* colorRed = options->percentColorRed.chars;
+
+ FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type;
+
+ uint8_t blocksPercent = (uint8_t) (percent / 100.0 * options->barWidth + 0.5);
+ assert(blocksPercent <= options->barWidth);
+
+ bool autoColorElapsed = ffStrbufIgnCaseEqualS(&options->barColorElapsed, "auto");
+
+ bool monochrome = (percentType & FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT) || !autoColorElapsed;
+ if (!options->pipe && options->barColorElapsed.length > 0 && monochrome) {
+ const char* color = NULL;
+ if (!autoColorElapsed) {
+ color = options->barColorElapsed.chars;
+ } else if (green <= yellow) {
+ if (percent < green) {
+ color = colorGreen;
+ } else if (percent < yellow) {
+ color = colorYellow;
+ } else {
+ color = colorRed;
+ }
+ } else {
+ if (percent < yellow) {
+ color = colorRed;
+ } else if (percent < green) {
+ color = colorYellow;
+ } else {
+ color = colorGreen;
+ }
+ }
+ ffStrbufAppendF(buffer, "\e[%sm", color);
+ }
+ for (uint8_t i = 0; i < blocksPercent; ++i) {
+ if (!options->pipe && options->barColorElapsed.length > 0 && !monochrome) {
+ uint32_t section1Begin = (uint32_t) ((green <= yellow ? green : yellow) / 100.0 * options->barWidth + 0.5);
+ uint32_t section2Begin = (uint32_t) ((green > yellow ? green : yellow) / 100.0 * options->barWidth + 0.5);
+ if (i == section2Begin) {
+ ffStrbufAppendF(buffer, "\e[%sm", (green > yellow ? colorGreen : colorRed));
+ } else if (i == section1Begin) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorYellow);
+ } else if (i == 0) {
+ ffStrbufAppendF(buffer, "\e[%sm", (green <= yellow ? colorGreen : colorRed));
+ }
+ }
+ ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeftElapsed : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRightElapsed
+ : &options->barCharElapsed);
+ }
+
+ if (blocksPercent < options->barWidth) {
+ if (!options->pipe && options->barColorTotal.length > 0) {
+ ffStrbufAppendF(buffer, "\e[%sm", options->barColorTotal.chars);
+ }
+ for (uint8_t i = blocksPercent; i < options->barWidth; ++i) {
+ ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeft : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRight
+ : &options->barCharTotal);
+ }
+ }
+ }
+
+ if (!borderAsValue && options->barBorderRight.length) {
+ if (!options->pipe && options->barColorBorder.length > 0) {
+ ffStrbufAppendF(buffer, "\e[%sm", options->barColorBorder.chars);
+ }
+ ffStrbufAppend(buffer, &options->barBorderRight);
+ }
+
+ if (!options->pipe && (options->barColorElapsed.length > 0 || options->barColorTotal.length > 0 || options->barColorBorder.length > 0)) {
+ ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
+ appendOutputColor(buffer, module);
+ }
+}
+
+void ffPercentAppendNum(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, bool parentheses, const FFModuleArgs* module) {
+ uint8_t green = config.green, yellow = config.yellow;
+ assert(green <= 100 && yellow <= 100);
+
+ const FFOptionsDisplay* options = &instance.config.display;
+ FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type;
+
+ bool colored = !!(percentType & FF_PERCENTAGE_TYPE_NUM_COLOR_BIT);
+
+ if (parentheses) {
+ ffStrbufAppendC(buffer, '(');
+ }
+
+ if (colored && !options->pipe) {
+ const char* colorGreen = options->percentColorGreen.chars;
+ const char* colorYellow = options->percentColorYellow.chars;
+ const char* colorRed = options->percentColorRed.chars;
+
+ if (percent == -DBL_MAX) {
+ ffStrbufAppendS(buffer, "\e[" FF_COLOR_FG_LIGHT_BLACK "m");
+ } else if (green <= yellow) {
+ if (percent > yellow) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorRed);
+ } else if (percent > green) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorYellow);
+ } else {
+ ffStrbufAppendF(buffer, "\e[%sm", colorGreen);
+ }
+ } else {
+ if (percent < yellow) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorRed);
+ } else if (percent < green) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorYellow);
+ } else {
+ ffStrbufAppendF(buffer, "\e[%sm", colorGreen);
+ }
+ }
+ }
+ if (percent == -DBL_MAX) {
+ ffStrbufAppendS(buffer, "-");
+ } else {
+ ffStrbufAppendF(buffer, "%*.*f%s%%", options->percentWidth, options->percentNdigits, percent, options->percentSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : "");
+ }
+
+ if (colored && !options->pipe) {
+ ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
+ appendOutputColor(buffer, module);
+ }
+
+ if (parentheses) {
+ ffStrbufAppendC(buffer, ')');
+ }
+}
+
+bool ffPercentParseCommandOptions(const char* key, const char* subkey, const char* value, FFPercentageModuleConfig* config) {
+ if (!ffStrStartsWithIgnCase(subkey, "percent-")) {
+ return false;
+ }
+
+ subkey += strlen("percent-");
+
+ if (ffStrEqualsIgnCase(subkey, "green")) {
+ uint32_t num = ffOptionParseUInt32(key, value);
+ if (num > 100) {
+ fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key);
+ exit(480);
+ }
+ config->green = (uint8_t) num;
+ return true;
+ }
+
+ if (ffStrEqualsIgnCase(subkey, "yellow")) {
+ uint32_t num = ffOptionParseUInt32(key, value);
+ if (num > 100) {
+ fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key);
+ exit(480);
+ }
+ config->yellow = (uint8_t) num;
+ return true;
+ }
+
+ if (ffStrEqualsIgnCase(subkey, "type")) {
+ config->type = (FFPercentageTypeFlags) ffOptionParseUInt32(key, value);
+ return true;
+ }
+
+ return false;
+}
+
+bool ffPercentParseJsonObject(yyjson_val* key, yyjson_val* value, FFPercentageModuleConfig* config) {
+ assert(key);
+
+ if (!unsafe_yyjson_equals_str(key, "percent")) {
+ return false;
+ }
+
+ if (!yyjson_is_obj(value)) {
+ fprintf(stderr, "Error: usage: %s must be an object\n", unsafe_yyjson_get_str(key));
+ exit(480);
+ }
+
+ yyjson_val* greenVal = yyjson_obj_get(value, "green");
+ if (greenVal) {
+ int num = yyjson_get_int(greenVal);
+ if (num < 0 || num > 100) {
+ fputs("Error: usage: percent.green must be between 0 and 100\n", stderr);
+ exit(480);
+ }
+ config->green = (uint8_t) num;
+ }
+
+ yyjson_val* yellowVal = yyjson_obj_get(value, "yellow");
+ if (yellowVal) {
+ int num = yyjson_get_int(yellowVal);
+ if (num < 0 || num > 100) {
+ fputs("Error: usage: percent.yellow must be between 0 and 100\n", stderr);
+ exit(480);
+ }
+ config->yellow = (uint8_t) num;
+ }
+
+ yyjson_val* typeVal = yyjson_obj_get(value, "type");
+ if (typeVal) {
+ const char* error = ffPercentParseTypeJsonConfig(typeVal, &config->type);
+ if (error) {
+ fputs(error, stderr);
+ exit(480);
+ }
+ }
+
+ return true;
+}
+
+void ffPercentGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFPercentageModuleConfig config) {
+ yyjson_mut_val* percent = yyjson_mut_obj_add_obj(doc, module, "percent");
+ yyjson_mut_obj_add_uint(doc, percent, "green", config.green);
+ yyjson_mut_obj_add_uint(doc, percent, "yellow", config.yellow);
+ yyjson_mut_obj_add_uint(doc, percent, "type", config.type);
+}
diff --git a/src/common/impl/printing.c b/src/common/impl/printing.c
new file mode 100644
index 0000000..9f372d9
--- /dev/null
+++ b/src/common/impl/printing.c
@@ -0,0 +1,156 @@
+#include "fastfetch.h"
+#include "common/printing.h"
+#include "common/textModifier.h"
+#include "logo/logo.h"
+
+void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType) {
+ ffLogoPrintLine();
+
+ // This is used by --set-keyless, in this case we want neither the module name nor the separator
+ if (moduleName == NULL) {
+ return;
+ }
+
+ // This is used as a magic value for hiding keys
+ if (!(moduleArgs && ffStrbufEqualS(&moduleArgs->key, " ")) && instance.config.display.keyType != FF_MODULE_KEY_TYPE_NONE) {
+ ffPrintCharTimes(' ', instance.config.display.keyPaddingLeft);
+
+ if (!instance.config.display.pipe) {
+ fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout);
+ if (instance.config.display.brightColor) {
+ fputs(FASTFETCH_TEXT_MODIFIER_BOLT, stdout);
+ }
+
+ if (moduleArgs && !(printType & FF_PRINT_TYPE_NO_CUSTOM_KEY_COLOR) && moduleArgs->keyColor.length > 0) {
+ ffPrintColor(&moduleArgs->keyColor);
+ } else {
+ ffPrintColor(&instance.config.display.colorKeys);
+ }
+ }
+
+ if (instance.config.display.keyType & FF_MODULE_KEY_TYPE_ICON && moduleArgs && moduleArgs->keyIcon.length > 0) {
+ ffStrbufWriteTo(&moduleArgs->keyIcon, stdout);
+ }
+
+ if (instance.config.display.keyType & FF_MODULE_KEY_TYPE_STRING) {
+ ffPrintCharTimes(' ', instance.config.display.keyType >> FF_MODULE_KEY_TYPE_SPACE_SHIFT);
+
+ // NULL check is required for modules with custom keys, e.g. disk with the folder path
+ if ((printType & FF_PRINT_TYPE_NO_CUSTOM_KEY) || !moduleArgs || moduleArgs->key.length == 0) {
+ fputs(moduleName, stdout);
+
+ if (moduleIndex > 0) {
+ printf(" %hhu", moduleIndex);
+ }
+ } else {
+ FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate();
+ FF_PARSE_FORMAT_STRING_CHECKED(&key, &moduleArgs->key, ((FFformatarg[]) {
+ FF_ARG(moduleIndex, "index"),
+ FF_ARG(moduleArgs->keyIcon, "icon"),
+ }));
+ ffStrbufWriteTo(&key, stdout);
+ }
+ }
+
+ if (!instance.config.display.pipe) {
+ fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout);
+ ffPrintColor(&instance.config.display.colorSeparator);
+ }
+
+ ffStrbufWriteTo(&instance.config.display.keyValueSeparator, stdout);
+
+ if (!instance.config.display.pipe && instance.config.display.colorSeparator.length) {
+ fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout);
+ }
+
+ if (!(printType & FF_PRINT_TYPE_NO_CUSTOM_KEY_WIDTH)) {
+ uint32_t keyWidth = moduleArgs && moduleArgs->keyWidth > 0 ? moduleArgs->keyWidth : instance.config.display.keyWidth;
+ if (keyWidth > 0) {
+ printf("\e[%uG", (unsigned) (keyWidth + instance.state.logoWidth));
+ }
+ }
+ }
+
+ if (!instance.config.display.pipe) {
+ fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout);
+ if (moduleArgs && moduleArgs->outputColor.length) {
+ ffPrintColor(&moduleArgs->outputColor);
+ } else if (instance.config.display.colorOutput.length) {
+ ffPrintColor(&instance.config.display.colorOutput);
+ }
+ }
+}
+
+bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments) {
+ FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate();
+ bool success;
+ if (__builtin_expect(moduleArgs != NULL, 1)) {
+ success = ffParseFormatString(&buffer, &moduleArgs->outputFormat, numArgs, arguments);
+ } else {
+ ffStrbufSetStatic(&buffer, "undefined format");
+ success = false;
+ }
+
+ if (success) {
+ ffPrintLogoAndKey(moduleName, moduleIndex, moduleArgs, printType);
+ ffStrbufPutTo(&buffer, stdout);
+ } else {
+ ffPrintError(moduleName, moduleIndex, moduleArgs, printType, "%s", buffer.chars);
+ }
+
+ return success;
+}
+
+void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...) {
+ if (!instance.config.display.showErrors) {
+ return;
+ }
+
+ ffPrintLogoAndKey(moduleName, moduleIndex, moduleArgs, printType);
+
+ if (!instance.config.display.pipe) {
+ fputs(FASTFETCH_TEXT_MODIFIER_ERROR, stdout);
+ }
+
+ va_list arguments;
+ va_start(arguments, message);
+ vprintf(message, arguments);
+ va_end(arguments);
+
+ if (!instance.config.display.pipe) {
+ fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout);
+ }
+
+ putchar('\n');
+}
+
+void ffPrintColor(const FFstrbuf* colorValue) {
+ // If the color is not set, this would reset in \033[m, which resets everything.
+ // So we only print it, if the main color is at least one char.
+ if (colorValue->length == 0) {
+ return;
+ }
+
+ printf("\e[%sm", colorValue->chars);
+}
+
+void ffPrintCharTimes(char c, uint32_t times) {
+ if (times == 0) {
+ return;
+ }
+
+ if (times == 1) {
+ putchar(c);
+ return;
+ }
+
+ char str[32];
+ memset(str, c, sizeof(str)); // 2 instructions when compiling with AVX2 enabled
+ for (uint32_t i = sizeof(str); i <= times; i += (uint32_t) sizeof(str)) {
+ fwrite(str, 1, sizeof(str), stdout);
+ }
+ uint32_t remaining = times % sizeof(str);
+ if (remaining > 0) {
+ fwrite(str, 1, remaining, stdout);
+ }
+}
diff --git a/src/common/impl/processing_linux.c b/src/common/impl/processing_linux.c
new file mode 100644
index 0000000..2b87c3b
--- /dev/null
+++ b/src/common/impl/processing_linux.c
@@ -0,0 +1,672 @@
+#include "fastfetch.h"
+#include "common/processing.h"
+#include "common/io.h"
+#include "common/strutil.h"
+#include "common/mallocHelper.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <signal.h>
+#include <poll.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+#if !(__ANDROID__ || __OpenBSD__)
+ #include <spawn.h>
+#endif
+
+#if defined(__FreeBSD__) || defined(__APPLE__)
+ #include <sys/types.h>
+ #include <sys/user.h>
+ #include <sys/sysctl.h>
+#endif
+#if defined(__APPLE__)
+ #include <libproc.h>
+#elif defined(__sun)
+ #include <procfs.h>
+#elif defined(__OpenBSD__)
+ #include <sys/param.h>
+ #include <sys/sysctl.h>
+ #include <kvm.h>
+#elif defined(__NetBSD__)
+ #include <sys/types.h>
+ #include <sys/sysctl.h>
+#elif defined(__HAIKU__)
+ #include <OS.h>
+ #include <image.h>
+#endif
+
+#ifndef environ
+extern char** environ;
+#endif
+
+enum { FF_PIPE_BUFSIZ = 8192 };
+
+static inline int ffPipe2(int* fds, int flags) {
+#ifndef FF_HAVE_PIPE2
+ if (pipe(fds) == -1) {
+ return -1;
+ }
+ fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | flags);
+ fcntl(fds[1], F_SETFL, fcntl(fds[1], F_GETFL) | flags);
+ return 0;
+#else
+ return pipe2(fds, flags);
+#endif
+}
+
+// Not thread-safe
+const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle) {
+ int pipes[2];
+ if (ffPipe2(pipes, O_CLOEXEC) == -1) {
+ return "pipe() failed";
+ }
+
+ pid_t childPid = -1;
+ int nullFile = ffGetNullFD();
+
+#if !(__ANDROID__ || __OpenBSD__)
+
+ // NetBSD / Darwin: native syscall
+ // Linux (glibc): clone3-execve
+ // FreeBSD: vfork-execve
+ // illumos: vforkx-execve
+ // OpenBSD / Android (bionic): fork-execve
+
+ posix_spawn_file_actions_t file_actions;
+ posix_spawn_file_actions_init(&file_actions);
+ posix_spawn_file_actions_adddup2(&file_actions, pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO);
+ posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
+
+ static char* oldLang = NULL;
+ static int langIndex = -1;
+
+ if (langIndex >= 0) {
+ // Found before
+ if (oldLang) // oldLang was set only if it needed to be changed
+ {
+ if (environ[langIndex] != oldLang) {
+ // environ is changed outside of this function
+ langIndex = -1;
+ } else {
+ environ[langIndex] = (char*) "LANG=C.UTF-8";
+ }
+ }
+ }
+ if (langIndex < 0) {
+ for (int i = 0; environ[i] != NULL; i++) {
+ if (ffStrStartsWith(environ[i], "LANG=")) {
+ langIndex = i;
+ const char* langValue = environ[i] + 5; // Skip "LANG="
+ if (ffStrEqualsIgnCase(langValue, "C") ||
+ ffStrStartsWithIgnCase(environ[i], "C.") ||
+ ffStrEqualsIgnCase(langValue, "en_US") ||
+ ffStrStartsWithIgnCase(langValue, "en_US.")) {
+ break; // No need to change LANG
+ }
+ oldLang = environ[i];
+ environ[i] = (char*) "LANG=C.UTF-8"; // Set LANG to C.UTF-8 for consistent output
+ break;
+ }
+ }
+ }
+
+ int ret = posix_spawnp(&childPid, argv[0], &file_actions, NULL, argv, environ);
+
+ if (oldLang) {
+ environ[langIndex] = oldLang;
+ }
+
+ posix_spawn_file_actions_destroy(&file_actions);
+
+ if (ret != 0) {
+ close(pipes[0]);
+ close(pipes[1]);
+ if (ret == ENOENT) {
+ return "command not found";
+ }
+ return "posix_spawnp() failed";
+ }
+
+#else
+
+ // https://github.com/termux/termux-packages/issues/25369
+ childPid = fork();
+ if (childPid == -1) {
+ close(pipes[0]);
+ close(pipes[1]);
+ return "fork() failed";
+ }
+
+ if (childPid == 0) {
+ // Child process
+ dup2(pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO);
+ dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO);
+ putenv("LANG=C.UTF-8");
+ execvp(argv[0], argv);
+ _exit(127);
+ }
+
+#endif
+
+ close(pipes[1]);
+ outHandle->pid = childPid;
+ outHandle->pipeRead = pipes[0];
+ return NULL;
+}
+
+const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
+ assert(handle->pipeRead != -1);
+ assert(handle->pid != -1);
+
+ const int32_t timeout = instance.config.general.processingTimeout;
+ FF_AUTO_CLOSE_FD int childPipeFd = handle->pipeRead;
+ pid_t childPid = handle->pid;
+ handle->pipeRead = -1;
+ handle->pid = -1;
+ char str[FF_PIPE_BUFSIZ];
+
+ for (;;) {
+ if (timeout >= 0) {
+ struct pollfd pollfd = { childPipeFd, POLLIN, 0 };
+ int pollret = poll(&pollfd, 1, timeout);
+ if (pollret == 0) {
+ kill(childPid, SIGTERM);
+ waitpid(childPid, NULL, 0);
+ return "poll(&pollfd, 1, timeout) timeout (try increasing --processing-timeout)";
+ } else if (pollret < 0 || (pollfd.revents & POLLERR)) {
+ kill(childPid, SIGTERM);
+ waitpid(childPid, NULL, 0);
+ return pollret < 0
+ ? "poll(&pollfd, 1, timeout) error: pollret < 0"
+ : "poll(&pollfd, 1, timeout) error: pollfd.revents & POLLERR";
+ }
+ }
+
+ ssize_t nRead = read(childPipeFd, str, FF_PIPE_BUFSIZ);
+ if (nRead > 0) {
+ ffStrbufAppendNS(buffer, (uint32_t) nRead, str);
+ } else if (nRead == 0) {
+ int stat_loc = 0;
+ if (childPid > 0 && waitpid(childPid, &stat_loc, 0) == childPid) {
+ if (!WIFEXITED(stat_loc)) {
+ return "child process exited abnormally";
+ }
+ if (WEXITSTATUS(stat_loc) == 127) {
+ return "command not found";
+ }
+ // We only handle 127 as an error. See `getTerminalVersionUrxvt` in `terminalshell.c`
+ return NULL;
+ }
+ return NULL;
+ } else if (nRead < 0) {
+ break;
+ }
+ }
+
+ return "read(childPipeFd, str, FF_PIPE_BUFSIZ) failed";
+}
+
+void ffProcessGetInfoLinux(pid_t pid, FFstrbuf* processName, FFstrbuf* exe, const char** exeName, FFstrbuf* exePath) {
+ assert(processName->length > 0);
+ ffStrbufClear(exe);
+ if (exePath) {
+ ffStrbufClear(exePath);
+ }
+
+#if defined(__linux__) || defined(__GNU__)
+
+ char filePath[64];
+ snprintf(filePath, sizeof(filePath), "/proc/%d/cmdline", (int) pid);
+
+ if (ffReadFileBuffer(filePath, exe)) {
+ const char* p = exe->chars;
+ uint32_t len = (uint32_t) strlen(p);
+
+ if (len + 1 < exe->length) {
+ const char* name = memrchr(p, '/', len);
+ if (name) {
+ name++;
+ } else {
+ name = p;
+ }
+
+ // For interpreters, try to find the real script path in the arguments
+ if (ffStrStartsWith(name, "python")
+ #ifndef __ANDROID__
+ || ffStrEquals(name, "guile") // for shepherd
+ #endif
+ ) {
+ // `cmdline` always ends with a trailing '\0', and ffReadFileBuffer appends another \0
+ // So `exe->chars` is always double '\0' terminated
+ for (p = p + len + 1; *p && *p == '-'; p += strlen(p) + 1) { // Skip arguments
+ assert(p - exe->chars < exe->allocated);
+ }
+ if (*p) {
+ len = (uint32_t) strlen(p);
+ memmove(exe->chars, p, len + 1);
+ }
+ }
+ }
+
+ assert(len < exe->allocated);
+ exe->length = len;
+ ffStrbufTrimLeft(exe, '-'); // Login shells start with a dash
+ }
+
+ if (exePath) {
+ snprintf(filePath, sizeof(filePath), "/proc/%d/exe", (int) pid);
+ char buf[PATH_MAX];
+ ssize_t length = readlink(filePath, buf, PATH_MAX - 1);
+ if (length > 0) // doesn't contain trailing NUL
+ {
+ buf[length] = '\0';
+ // When the process is a deleted executable, the resolved path is like `/usr/bin/app (deleted)`
+ // But we can still access the binary via `/proc/pid/exe`. See #2136
+ if (ffPathExists(buf, FF_PATHTYPE_ANY)) {
+ ffStrbufSetNS(exePath, (uint32_t) length, buf);
+ }
+ }
+
+ if (exePath->length == 0) {
+ ffStrbufSetS(exePath, filePath);
+ }
+ }
+
+#elif defined(__APPLE__)
+
+ size_t len = 0;
+ int mibs[] = { CTL_KERN, KERN_PROCARGS2, pid };
+ if (sysctl(mibs, ARRAY_SIZE(mibs), NULL, &len, NULL, 0) == 0) { // try get arg0
+ // don't know why if don't let len longer, proArgs2 and len will change during the following sysctl() in old MacOS version.
+ len++;
+ FF_AUTO_FREE char* const procArgs2 = malloc(len);
+ if (sysctl(mibs, ARRAY_SIZE(mibs), procArgs2, &len, NULL, 0) == 0) {
+ // https://gist.github.com/nonowarn/770696#file-getargv-c-L46
+ uint32_t argc = *(uint32_t*) procArgs2;
+ const char* realExePath = procArgs2 + sizeof(argc);
+
+ const char* arg0 = memchr(realExePath, '\0', len - (size_t) (realExePath - procArgs2));
+ if (exePath) {
+ ffStrbufSetNS(exePath, (uint32_t) (arg0 - realExePath), realExePath);
+ }
+
+ do {
+ arg0++;
+ } while (*arg0 == '\0');
+ assert(arg0 < procArgs2 + len);
+
+ if (argc > 1) {
+ // #977
+ const char* p = strrchr(arg0, '/');
+ if (p) {
+ p++;
+ } else {
+ p = arg0;
+ }
+ if (ffStrStartsWithIgnCase(p, "python")) { // /opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/Resources/Python.app/Contents/MacOS/Python /Users/carter/.local/bin/xonsh
+ arg0 = p + strlen(p) + 1;
+ }
+ }
+
+ if (*arg0 == '-') {
+ arg0++; // Login shells
+ }
+
+ ffStrbufSetS(exe, arg0);
+ }
+ }
+
+ if (exePath || exe->length == 0) {
+ char buf[PROC_PIDPATHINFO_MAXSIZE];
+ int length = proc_pidpath(pid, buf, ARRAY_SIZE(buf));
+ if (length > 0) {
+ if (exe->length == 0) {
+ ffStrbufSetNS(exe, (uint32_t) length, buf);
+ }
+ if (exePath) {
+ // We don't use exec_path above as exePath because it's a relative path and can be different
+ // from the actual executable being run (for example, when the original file is moved)
+ ffStrbufSetNS(exePath, (uint32_t) length, buf);
+ }
+ }
+ }
+
+#elif defined(__FreeBSD__) || defined(__NetBSD__)
+
+ size_t size = ARG_MAX;
+ FF_AUTO_FREE char* args = malloc(size);
+
+ static_assert(ARG_MAX > PATH_MAX, "");
+
+ if (exePath && sysctl((int[]) { CTL_KERN,
+ #if __FreeBSD__
+ KERN_PROC,
+ KERN_PROC_PATHNAME,
+ pid
+ #else
+ KERN_PROC_ARGS,
+ pid,
+ KERN_PROC_PATHNAME
+ #endif
+ },
+ 4,
+ args,
+ &size,
+ NULL,
+ 0) == 0)
+ ffStrbufSetNS(exePath, (uint32_t) (size - 1), args);
+
+ size = ARG_MAX;
+ if (sysctl(
+ (int[]) { CTL_KERN,
+ #if __FreeBSD__
+ KERN_PROC,
+ KERN_PROC_ARGS,
+ pid
+ #else
+ KERN_PROC_ARGS,
+ pid,
+ KERN_PROC_ARGV,
+ #endif
+ },
+ 4,
+ args,
+ &size,
+ NULL,
+ 0) == 0) {
+ char* arg0 = args;
+ size_t arg0Len = strlen(args);
+ if (size > arg0Len + 1) {
+ char* p = (char*) memrchr(args, '/', arg0Len);
+ if (p) {
+ p++;
+ } else {
+ p = arg0;
+ }
+ if (ffStrStartsWith(p, "python")) // /usr/local/bin/python3.9 /home/carter/.local/bin/xonsh
+ {
+ arg0 += arg0Len + 1;
+ }
+ }
+ if (arg0[0] == '-') {
+ arg0++;
+ }
+ ffStrbufSetS(exe, arg0);
+ }
+
+#elif defined(__sun)
+
+ char filePath[128];
+ snprintf(filePath, sizeof(filePath), "/proc/%d/psinfo", (int) pid);
+ psinfo_t proc;
+ if (ffReadFileData(filePath, sizeof(proc), &proc) == sizeof(proc)) {
+ const char* args = proc.pr_psargs;
+ if (args[0] == '-') {
+ ++args;
+ }
+ const char* end = strchr(args, ' ');
+ ffStrbufSetNS(exe, end ? (uint32_t) (end - args) : (uint32_t) strlen(args), args);
+ }
+
+ if (exePath) {
+ snprintf(filePath, sizeof(filePath), "/proc/%d/path/a.out", (int) pid);
+ char buf[PATH_MAX];
+ ssize_t length = readlink(filePath, buf, PATH_MAX - 1);
+ if (length > 0) // doesn't contain trailing NUL
+ {
+ buf[length] = '\0';
+ ffStrbufSetNS(exePath, (uint32_t) length, buf);
+ }
+ }
+
+#elif defined(__OpenBSD__)
+
+ kvm_t* kd = kvm_open(NULL, NULL, NULL, KVM_NO_FILES, NULL);
+ int count = 0;
+ const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(struct kinfo_proc), &count);
+ if (proc) {
+ char** argv = kvm_getargv(kd, proc, 0);
+ if (argv) {
+ const char* arg0 = argv[0];
+ if (arg0[0] == '-') {
+ arg0++;
+ }
+ ffStrbufSetS(exe, arg0);
+ }
+ }
+ kvm_close(kd);
+
+#elif defined(__HAIKU__)
+
+ image_info info;
+ int32 cookie = 0;
+
+ while (get_next_image_info(pid, &cookie, &info) == B_OK) {
+ if (info.type != B_APP_IMAGE) {
+ continue;
+ }
+ ffStrbufSetS(exe, info.name);
+
+ if (exePath) {
+ ffStrbufSet(exePath, exe);
+ }
+ break;
+ }
+
+#endif
+
+ if (exe->length == 0) {
+ ffStrbufSet(exe, processName);
+ }
+
+ assert(exe->length > 0);
+ uint32_t lastSlashIndex = ffStrbufLastIndexC(exe, '/');
+ if (lastSlashIndex < exe->length) {
+ *exeName = exe->chars + lastSlashIndex + 1;
+ }
+}
+
+const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, int32_t* tty) {
+ if (pid <= 0) {
+ return "Invalid pid";
+ }
+
+#if defined(__linux__) || defined(__GNU__)
+
+ char procFilePath[64];
+ #if __linux__
+ if (ppid || tty)
+ #endif
+ {
+ snprintf(procFilePath, sizeof(procFilePath), "/proc/%d/stat", (int) pid);
+ char buf[PROC_FILE_BUFFSIZ];
+ ssize_t nRead = ffReadFileData(procFilePath, sizeof(buf) - 1, buf);
+ if (nRead <= 8) {
+ return "ffReadFileData(/proc/pid/stat, PROC_FILE_BUFFSIZ-1, buf) failed";
+ }
+ buf[nRead] = '\0'; // pid (comm) state ppid pgrp session tty
+
+ const char* pState = NULL;
+
+ {
+ // comm in `/proc/pid/stat` is not encoded, and may contain ' ', ')' or even `\n`
+ const char* start = memchr(buf, '(', (size_t) nRead);
+ if (!start) {
+ return "memchr(stat, '(') failed";
+ }
+ start++;
+ const char* end = memrchr(start, ')', (size_t) nRead - (size_t) (start - buf));
+ if (!end) {
+ return "memrchr(stat, ')') failed";
+ }
+ ffStrbufSetNS(name, (uint32_t) (end - start), start);
+ ffStrbufTrimRightSpace(name);
+ if (name->chars[0] == '\0') {
+ return "process name is empty";
+ }
+ pState = end + 2; // skip ") "
+ }
+
+ #if !__linux__
+ if (ppid || tty)
+ #endif
+ {
+ int ppid_, tty_;
+ if (sscanf(pState + 2, "%d %*d %*d %d", &ppid_, &tty_) < 2) {
+ return "sscanf(stat) failed";
+ }
+
+ if (ppid) {
+ *ppid = (pid_t) ppid_;
+ }
+ if (tty) {
+ *tty = tty_ & 0xFF;
+ }
+ }
+ }
+ #if __linux__
+ else {
+ snprintf(procFilePath, sizeof(procFilePath), "/proc/%d/comm", (int) pid);
+ ssize_t nRead = ffReadFileBuffer(procFilePath, name);
+ if (nRead <= 0) {
+ return "ffReadFileBuffer(/proc/pid/comm, name) failed";
+ }
+ ffStrbufTrimRightSpace(name);
+ }
+ #endif
+
+#elif defined(__APPLE__)
+
+ struct kinfo_proc proc;
+ size_t size = sizeof(proc);
+ if (sysctl(
+ (int[]) { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }, 4, &proc, &size, NULL, 0)) {
+ return "sysctl(KERN_PROC_PID) failed";
+ }
+
+ ffStrbufSetS(name, proc.kp_proc.p_comm); // trancated to 16 chars
+ if (ppid) {
+ *ppid = (pid_t) proc.kp_eproc.e_ppid;
+ }
+ if (tty) {
+ *tty = ((proc.kp_eproc.e_tdev >> 24) & 0xFF) == 0x10
+ ? proc.kp_eproc.e_tdev & 0xFFFFFF
+ : -1;
+ }
+
+#elif defined(__FreeBSD__)
+
+ #ifdef __DragonFly__
+ #define ki_comm kp_comm
+ #define ki_ppid kp_ppid
+ #define ki_tdev kp_tdev
+ #define ki_flag kp_flags
+ #endif
+
+ struct kinfo_proc proc;
+ size_t size = sizeof(proc);
+ if (sysctl(
+ (int[]) { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }, 4, &proc, &size, NULL, 0)) {
+ return "sysctl(KERN_PROC_PID) failed";
+ }
+
+ ffStrbufSetS(name, proc.ki_comm);
+ if (ppid) {
+ *ppid = (pid_t) proc.ki_ppid;
+ }
+ if (tty) {
+ if (proc.ki_tdev != NODEV && proc.ki_flag & P_CONTROLT) {
+ const char* ttyName = devname(proc.ki_tdev, S_IFCHR);
+ if (ffStrStartsWith(ttyName, "pts/")) {
+ *tty = (int32_t) strtol(ttyName + strlen("pts/"), NULL, 10);
+ } else {
+ *tty = -1;
+ }
+ } else {
+ *tty = -1;
+ }
+ }
+
+#elif defined(__NetBSD__)
+
+ struct kinfo_proc2 proc;
+ size_t size = sizeof(proc);
+ if (sysctl(
+ (int[]) { CTL_KERN, KERN_PROC2, KERN_PROC_PID, pid, sizeof(proc), 1 }, 6, &proc, &size, NULL, 0) != 0) {
+ return "sysctl(KERN_PROC_PID) failed";
+ }
+
+ ffStrbufSetS(name, proc.p_comm);
+ if (ppid) {
+ *ppid = (pid_t) proc.p_ppid;
+ }
+ if (tty) {
+ if (proc.p_flag & P_CONTROLT) {
+ const char* ttyName = devname(proc.p_tdev, S_IFCHR);
+ if (ffStrStartsWith(ttyName, "pts/")) {
+ *tty = (int32_t) strtol(ttyName + strlen("pts/"), NULL, 10);
+ } else {
+ *tty = -1;
+ }
+ } else {
+ *tty = -1;
+ }
+ }
+
+#elif defined(__sun)
+ char path[128];
+ snprintf(path, sizeof(path), "/proc/%d/psinfo", (int) pid);
+ psinfo_t proc;
+ if (ffReadFileData(path, sizeof(proc), &proc) != sizeof(proc)) {
+ return "ffReadFileData(psinfo) failed";
+ }
+
+ ffStrbufSetS(name, proc.pr_fname);
+ if (ppid) {
+ *ppid = proc.pr_ppid;
+ }
+ if (tty) {
+ *tty = (int) proc.pr_ttydev;
+ }
+
+#elif defined(__OpenBSD__)
+
+ kvm_t* kd = kvm_open(NULL, NULL, NULL, KVM_NO_FILES, NULL);
+ int count = 0;
+ const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(struct kinfo_proc), &count);
+ if (proc) {
+ ffStrbufSetS(name, proc->p_comm);
+ if (ppid) {
+ *ppid = proc->p_ppid;
+ }
+ if (tty) {
+ *tty = (int) proc->p_tdev;
+ }
+ }
+ kvm_close(kd);
+ if (!proc) {
+ return "kvm_getprocs() failed";
+ }
+
+#elif defined(__HAIKU__)
+
+ team_info info;
+ if (get_team_info(pid, &info) == B_OK) {
+ ffStrbufSetS(name, info.name);
+ if (ppid) {
+ *ppid = info.parent;
+ }
+ }
+
+ FF_UNUSED(tty);
+
+#else
+
+ return "Unsupported platform";
+
+#endif
+
+ return NULL;
+}
diff --git a/src/common/impl/processing_windows.c b/src/common/impl/processing_windows.c
new file mode 100644
index 0000000..846276b
--- /dev/null
+++ b/src/common/impl/processing_windows.c
@@ -0,0 +1,281 @@
+#include "fastfetch.h"
+#include "common/mallocHelper.h"
+#include "common/processing.h"
+#include "common/io.h"
+#include "common/windows/unicode.h"
+#include "common/windows/nt.h"
+
+#include <stdalign.h>
+#include <windows.h>
+#include <ntstatus.h>
+
+enum { FF_PIPE_BUFSIZ = 8192 };
+
+static void argvToCmdline(char* const argv[], FFstrbuf* result) {
+ // From https://gist.github.com/jin-x/cdd641d98887524b091fb1f82a68717d
+
+ FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate();
+ for (int i = 0; argv[i] != NULL; i++) {
+ ffStrbufSetS(&temp, argv[i]);
+ // Add slash (\) before double quotes (") and duplicate slashes before it
+ for (
+ uint32_t pos = ffStrbufFirstIndexC(&temp, '"'), cnt;
+ pos != temp.length;
+ pos = ffStrbufNextIndexC(&temp, pos + cnt * 2, '"')) {
+ cnt = 1;
+ while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; }
+ ffStrbufInsertNC(&temp, pos, cnt, '\\');
+ }
+
+ // Add quotes around string if whitespace chars are present (with slash duplicating at the end of string)
+ if (ffStrbufFirstIndexS(&temp, " \t") != temp.length) {
+ uint32_t pos = temp.length;
+ uint32_t cnt = 0;
+ while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; }
+ if (cnt > 0) {
+ ffStrbufAppendNC(&temp, cnt, '\\');
+ }
+ ffStrbufPrependC(&temp, '"');
+ ffStrbufAppendC(&temp, '"');
+ }
+
+ // Add space delimiter
+ if (i > 0) {
+ ffStrbufAppendC(result, ' ');
+ }
+ ffStrbufAppend(result, &temp);
+ ffStrbufClear(&temp);
+ }
+}
+
+const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle) {
+ const int32_t timeout = instance.config.general.processingTimeout;
+
+ wchar_t pipeName[32];
+ static unsigned pidCounter = 0;
+ swprintf(pipeName, ARRAY_SIZE(pipeName), L"\\\\.\\pipe\\FASTFETCH-%u-%u", instance.state.platform.pid, ++pidCounter);
+
+ FF_AUTO_CLOSE_FD HANDLE hChildPipeRead = CreateNamedPipeW(
+ pipeName,
+ PIPE_ACCESS_INBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | (timeout < 0 ? 0 : FILE_FLAG_OVERLAPPED),
+ 0,
+ 1,
+ FF_PIPE_BUFSIZ,
+ FF_PIPE_BUFSIZ,
+ 0,
+ NULL);
+ if (hChildPipeRead == INVALID_HANDLE_VALUE) {
+ return "CreateNamedPipeW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed";
+ }
+
+ HANDLE hChildPipeWrite = CreateFileW(
+ pipeName,
+ GENERIC_WRITE,
+ 0,
+ &(SECURITY_ATTRIBUTES) {
+ .nLength = sizeof(SECURITY_ATTRIBUTES),
+ .lpSecurityDescriptor = NULL,
+ .bInheritHandle = TRUE,
+ },
+ OPEN_EXISTING,
+ 0,
+ NULL);
+ if (hChildPipeWrite == INVALID_HANDLE_VALUE) {
+ return "CreateFileW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed";
+ }
+
+ PROCESS_INFORMATION piProcInfo = {};
+ STARTUPINFOW siStartInfo = {
+ .cb = sizeof(siStartInfo),
+ .dwFlags = STARTF_USESTDHANDLES,
+ };
+ if (useStdErr) {
+ siStartInfo.hStdOutput = ffGetNullFD();
+ siStartInfo.hStdError = hChildPipeWrite;
+ } else {
+ siStartInfo.hStdOutput = hChildPipeWrite;
+ siStartInfo.hStdError = ffGetNullFD();
+ }
+
+ FF_AUTO_FREE wchar_t* cmdline = NULL;
+ {
+ FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate();
+ argvToCmdline(argv, &buf);
+ uint32_t cmdlineBytes = (buf.length + 1) * sizeof(wchar_t);
+ cmdline = malloc(cmdlineBytes);
+ if (!NT_SUCCESS(RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, NULL, buf.chars, buf.length + 1))) {
+ return "RtlUTF8ToUnicodeN() failed";
+ }
+ }
+
+ BOOL success = CreateProcessW(
+ NULL, // application name
+ cmdline, // command line
+ NULL, // process security attributes
+ NULL, // primary thread security attributes
+ TRUE, // handles are inherited
+ 0, // creation flags
+ NULL, // use parent's environment
+ NULL, // use parent's current directory
+ &siStartInfo, // STARTUPINFO pointer
+ &piProcInfo // receives PROCESS_INFORMATION
+ );
+
+ NtClose(hChildPipeWrite);
+ if (!success) {
+ if (GetLastError() == ERROR_FILE_NOT_FOUND) {
+ return "command not found";
+ }
+ return "CreateProcessW() failed";
+ }
+
+ NtClose(piProcInfo.hThread); // we don't need the thread handle
+ outHandle->pid = piProcInfo.hProcess;
+ outHandle->pipeRead = hChildPipeRead;
+ hChildPipeRead = INVALID_HANDLE_VALUE; // ownership transferred, don't close it
+
+ return NULL;
+}
+
+static void terminateChildProcess(HANDLE hProcess, HANDLE hChildPipeRead, HANDLE hReadEvent, IO_STATUS_BLOCK* piosb) {
+ IO_STATUS_BLOCK cancelIosb = {};
+ if (NT_SUCCESS(NtCancelIoFileEx(hChildPipeRead, piosb, &cancelIosb))) {
+ if (hReadEvent) {
+ NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = -100000 }); // wait for cancellation to complete
+ }
+ }
+ NtTerminateProcess(hProcess, 1);
+}
+
+const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) {
+ assert(handle->pipeRead != INVALID_HANDLE_VALUE);
+ assert(handle->pid != INVALID_HANDLE_VALUE);
+
+ int32_t timeout = instance.config.general.processingTimeout;
+ FF_AUTO_CLOSE_FD HANDLE hProcess = handle->pid;
+ FF_AUTO_CLOSE_FD HANDLE hChildPipeRead = handle->pipeRead;
+ FF_AUTO_CLOSE_FD HANDLE hReadEvent = NULL;
+ handle->pid = INVALID_HANDLE_VALUE;
+ handle->pipeRead = INVALID_HANDLE_VALUE;
+
+ if (timeout >= 0 && !NT_SUCCESS(NtCreateEvent(&hReadEvent, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE))) {
+ return "NtCreateEvent() failed";
+ }
+
+ char str[FF_PIPE_BUFSIZ];
+ uint32_t nRead = 0;
+ IO_STATUS_BLOCK iosb = {};
+ do {
+ NTSTATUS status = NtReadFile(
+ hChildPipeRead,
+ hReadEvent,
+ NULL,
+ NULL,
+ &iosb,
+ str,
+ (ULONG) sizeof(str),
+ NULL,
+ NULL);
+ if (status == STATUS_PENDING) {
+ switch (NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) timeout * -10000 })) {
+ case STATUS_WAIT_0:
+ status = iosb.Status;
+ break;
+
+ case STATUS_TIMEOUT: {
+ terminateChildProcess(hProcess, hChildPipeRead, hReadEvent, &iosb);
+ return "NtReadFile(hChildPipeRead) timed out";
+ }
+
+ default:
+ terminateChildProcess(hProcess, hChildPipeRead, hReadEvent, &iosb);
+ return "NtWaitForSingleObject(hReadEvent) failed";
+ }
+ }
+
+ if (status == STATUS_PIPE_BROKEN || status == STATUS_END_OF_FILE) {
+ goto exit;
+ }
+
+ if (!NT_SUCCESS(status)) {
+ terminateChildProcess(hProcess, hChildPipeRead, NULL, &iosb);
+ return "NtReadFile(hChildPipeRead) failed";
+ }
+
+ nRead = (uint32_t) iosb.Information;
+ ffStrbufAppendNS(buffer, nRead, str);
+ } while (nRead > 0);
+
+exit: {
+ PROCESS_BASIC_INFORMATION info = {};
+ ULONG size;
+ if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size))) {
+ assert(size == sizeof(info));
+ if (info.ExitStatus != STILL_ACTIVE && info.ExitStatus != 0) {
+ return "Child process exited with an error";
+ }
+ } else {
+ return "NtQueryInformationProcess(ProcessBasicInformation) failed";
+ }
+}
+
+ return NULL;
+}
+
+bool ffProcessGetInfoWindows(uint32_t pid, uint32_t* ppid, FFstrbuf* pname, FFstrbuf* exe, const char** exeName, FFstrbuf* exePath, bool* gui) {
+ FF_AUTO_CLOSE_FD HANDLE hProcess = NtCurrentProcess();
+ if (pid != 0) {
+ if (!NT_SUCCESS(NtOpenProcess(&hProcess, PROCESS_QUERY_LIMITED_INFORMATION, &(OBJECT_ATTRIBUTES) {
+ .Length = sizeof(OBJECT_ATTRIBUTES),
+ },
+ &(CLIENT_ID) { .UniqueProcess = (HANDLE) (uintptr_t) pid }))) {
+ return false;
+ }
+ }
+
+ if (ppid) {
+ PROCESS_BASIC_INFORMATION info = {};
+ ULONG size;
+ if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size))) {
+ assert(size == sizeof(info));
+ *ppid = (uint32_t) info.InheritedFromUniqueProcessId;
+ } else {
+ return false;
+ }
+ }
+
+ if (exe) {
+ // TODO: It's possible to query the command line with `NtQueryInformationProcess(60/*ProcessCommandLineInformation*/)` since Windows 8.1
+
+ alignas(UNICODE_STRING) uint8_t buffer[4096];
+ ULONG size;
+ if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageFileNameWin32, &buffer, sizeof(buffer), &size))) {
+ UNICODE_STRING* imagePath = (UNICODE_STRING*) buffer;
+ ffStrbufSetNWS(exe, imagePath->Length / sizeof(wchar_t), imagePath->Buffer);
+
+ if (exePath) {
+ ffStrbufSet(exePath, exe);
+ }
+
+ if (pname && exeName) {
+ *exeName = exe->chars + ffStrbufLastIndexC(exe, '\\') + 1;
+ ffStrbufSetS(pname, *exeName);
+ }
+ } else {
+ return false;
+ }
+ }
+
+ if (gui) {
+ SECTION_IMAGE_INFORMATION info = {};
+ ULONG size;
+ if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageInformation, &info, sizeof(info), &size))) {
+ assert(size == sizeof(info));
+ *gui = info.SubSystemType == IMAGE_SUBSYSTEM_WINDOWS_GUI;
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/src/common/impl/properties.c b/src/common/impl/properties.c
new file mode 100644
index 0000000..c13ed1c
--- /dev/null
+++ b/src/common/impl/properties.c
@@ -0,0 +1,172 @@
+#include "fastfetch.h"
+#include "common/properties.h"
+#include "common/io.h"
+#include "common/mallocHelper.h"
+
+#include <stdlib.h>
+#include <ctype.h>
+#ifdef _WIN32
+ #include "common/windows/getline.h"
+#endif
+
+bool ffParsePropLinePointer(const char** line, const char* start, FFstrbuf* buffer) {
+ if (**line == '\0') {
+ return false;
+ }
+
+ // Skip any amount of whitespace at the begin of line
+ while (**line == ' ' || **line == '\t') {
+ ++(*line);
+ }
+
+ while (*start != '\0') {
+ // Any amount of whitespace in the format string matches any amount of whitespace in the line, even none
+ if (*start == ' ' || *start == '\t') {
+ while (*start == ' ' || *start == '\t') {
+ ++start;
+ }
+
+ while (**line == ' ' || **line == '\t') {
+ ++(*line);
+ }
+
+ continue;
+ }
+
+ // Line doesn't match start, skip it
+ if (tolower(**line) != tolower(*start) || **line == '\0') {
+ return false;
+ }
+
+ // Line and start match, continue testing
+ ++(*line);
+ ++start;
+ }
+
+ char valueEnd = '\n';
+
+ // Allow faster parsing of XML
+ if (*(*line - 1) == '>') {
+ valueEnd = '<';
+ }
+
+ // Skip any amount of whitespace at the begin of the value
+ while (**line == ' ' || **line == '\t') {
+ ++(*line);
+ }
+
+ // Allow faster parsing of quoted values
+ if (**line == '"' || **line == '\'') {
+ valueEnd = **line;
+ ++(*line);
+ }
+
+ // Copy the value to the buffer
+ while (**line != valueEnd && **line != '\n' && **line != '\0') {
+ ffStrbufAppendC(buffer, **line);
+ ++(*line);
+ }
+
+ ffStrbufTrimRight(buffer, ' ');
+
+ return true;
+}
+
+bool ffParsePropLines(const char* lines, const char* start, FFstrbuf* buffer) {
+ while (!ffParsePropLinePointer(&lines, start, buffer)) {
+ while (*lines != '\0' && *lines != '\n') {
+ ++lines;
+ }
+
+ if (*lines == '\0') {
+ return false;
+ }
+
+ // Skip '\n'
+ ++lines;
+ }
+
+ return true;
+}
+
+// The following functions return true if the file was found, independently if start was found
+// Buffers which already contain content are not overwritten
+// The last occurrence of start in the first file will be the one used
+
+bool ffParsePropFileValues(const char* filename, uint32_t numQueries, FFpropquery* queries) {
+ FF_AUTO_CLOSE_FILE FILE* file = fopen(filename, "r");
+ if (file == NULL) {
+ return false;
+ }
+
+ bool valueStorage[32];
+ bool* unsetValues = valueStorage;
+
+ if (numQueries > ARRAY_SIZE(valueStorage)) {
+ unsetValues = malloc(sizeof(bool) * numQueries);
+ }
+
+ bool allSet = true;
+ for (uint32_t i = 0; i < numQueries; i++) {
+ unsetValues[i] = queries[i].buffer->length == 0;
+ if (unsetValues[i]) {
+ allSet = false;
+ }
+ }
+
+ if (!allSet) {
+ FF_AUTO_FREE char* line = NULL;
+ size_t len = 0;
+
+ while (getline(&line, &len, file) != -1) {
+ for (uint32_t i = 0; i < numQueries; i++) {
+ if (!unsetValues[i]) {
+ continue;
+ }
+
+ uint32_t currentLength = queries[i].buffer->length;
+ queries[i].buffer->length = 0;
+ if (!ffParsePropLine(line, queries[i].start, queries[i].buffer)) {
+ queries[i].buffer->length = currentLength;
+ }
+ }
+ }
+ }
+
+ if (unsetValues != valueStorage) {
+ free(unsetValues);
+ }
+ return true;
+}
+
+bool ffParsePropFileHomeValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries) {
+ FF_STRBUF_AUTO_DESTROY absolutePath = ffStrbufCreateF("%s/%s", instance.state.platform.homeDir.chars, relativeFile);
+ return ffParsePropFileValues(absolutePath.chars, numQueries, queries);
+}
+
+bool ffParsePropFileListValues(const FFlist* list, const char* relativeFile, uint32_t numQueries, FFpropquery* queries) {
+ bool foundAFile = false;
+
+ FF_LIST_FOR_EACH (FFstrbuf, dirPrefix, *list) {
+ const uint32_t dirPrefixLength = dirPrefix->length;
+ ffStrbufAppendS(dirPrefix, relativeFile);
+ if (ffParsePropFileValues(dirPrefix->chars, numQueries, queries)) {
+ foundAFile = true;
+ }
+ ffStrbufSubstrBefore(dirPrefix, dirPrefixLength);
+
+ bool allSet = true;
+ for (uint32_t k = 0; k < numQueries; k++) {
+ if (queries[k].buffer->length == 0) {
+ allSet = false;
+ break;
+ }
+ }
+
+ if (allSet) {
+ break;
+ }
+ }
+
+ return foundAFile;
+}
diff --git a/src/common/impl/settings.c b/src/common/impl/settings.c
new file mode 100644
index 0000000..e7d246b
--- /dev/null
+++ b/src/common/impl/settings.c
@@ -0,0 +1,609 @@
+#include "fastfetch.h"
+#include "common/settings.h"
+#include "common/library.h"
+#include "common/thread.h"
+#include "common/io.h"
+
+#include <string.h>
+
+#ifdef FF_HAVE_GIO
+ #include <gio/gio.h>
+
+typedef struct GVariantGetters {
+ FF_LIBRARY_SYMBOL(g_variant_dup_string)
+ FF_LIBRARY_SYMBOL(g_variant_get_boolean)
+ FF_LIBRARY_SYMBOL(g_variant_get_int32)
+ FF_LIBRARY_SYMBOL(g_variant_unref)
+} GVariantGetters;
+
+static FFvariant getGVariantValue(GVariant* variant, FFvarianttype type, const GVariantGetters* variantGetters) {
+ FFvariant result;
+
+ if (variant == NULL) {
+ result = FF_VARIANT_NULL;
+ } else if (type == FF_VARIANT_TYPE_STRING) {
+ result = (FFvariant) { .strValue = variantGetters->ffg_variant_dup_string(variant, NULL) }; // Dup string, so that variant itself can be freed
+ } else if (type == FF_VARIANT_TYPE_BOOL) {
+ result = (FFvariant) { .boolValue = (bool) variantGetters->ffg_variant_get_boolean(variant), .boolValueSet = true };
+ } else if (type == FF_VARIANT_TYPE_INT) {
+ result = (FFvariant) { .intValue = variantGetters->ffg_variant_get_int32(variant) };
+ } else {
+ result = FF_VARIANT_NULL;
+ }
+
+ if (variant) {
+ variantGetters->ffg_variant_unref(variant);
+ }
+
+ return result;
+}
+
+typedef struct GSettingsData {
+ FF_LIBRARY_SYMBOL(g_settings_schema_source_lookup)
+ FF_LIBRARY_SYMBOL(g_settings_schema_has_key)
+ FF_LIBRARY_SYMBOL(g_settings_new_full)
+ FF_LIBRARY_SYMBOL(g_settings_get_value)
+ FF_LIBRARY_SYMBOL(g_settings_get_user_value)
+ FF_LIBRARY_SYMBOL(g_settings_get_default_value)
+ FF_LIBRARY_SYMBOL(g_settings_schema_source_get_default)
+ GSettingsSchemaSource* schemaSource;
+ GVariantGetters variantGetters;
+
+ bool inited;
+} GSettingsData;
+
+static const GSettingsData* getGSettingsData(void) {
+ static GSettingsData data;
+
+ if (!data.inited) {
+ data.inited = true;
+ FF_LIBRARY_LOAD(libgsettings, NULL, "libgio-2.0" FF_LIBRARY_EXTENSION, 1);
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_schema_source_lookup, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_schema_has_key, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_new_full, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_get_value, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_get_user_value, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_get_default_value, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_schema_source_get_default, NULL)
+
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data.variantGetters, g_variant_dup_string, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data.variantGetters, g_variant_get_boolean, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data.variantGetters, g_variant_get_int32, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data.variantGetters, g_variant_unref, NULL);
+
+ data.schemaSource = data.ffg_settings_schema_source_get_default();
+ if (data.schemaSource) {
+ libgsettings = NULL;
+ }
+ }
+ if (!data.schemaSource) {
+ return NULL;
+ }
+
+ return &data;
+}
+
+FFvariant ffSettingsGetGSettings(const char* schemaName, const char* path, const char* key, FFvarianttype type) {
+ const GSettingsData* data = getGSettingsData();
+ if (data == NULL) {
+ return FF_VARIANT_NULL;
+ }
+
+ GSettingsSchema* schema = data->ffg_settings_schema_source_lookup(data->schemaSource, schemaName, true);
+ if (schema == NULL) {
+ return FF_VARIANT_NULL;
+ }
+
+ if (data->ffg_settings_schema_has_key(schema, key) == false) {
+ return FF_VARIANT_NULL;
+ }
+
+ GSettings* settings = data->ffg_settings_new_full(schema, NULL, path);
+ if (settings == NULL) {
+ return FF_VARIANT_NULL;
+ }
+
+ GVariant* variant = data->ffg_settings_get_value(settings, key);
+ if (variant != NULL) {
+ return getGVariantValue(variant, type, &data->variantGetters);
+ }
+
+ variant = data->ffg_settings_get_user_value(settings, key);
+ if (variant != NULL) {
+ return getGVariantValue(variant, type, &data->variantGetters);
+ }
+
+ variant = data->ffg_settings_get_default_value(settings, key);
+ return getGVariantValue(variant, type, &data->variantGetters);
+}
+#else // FF_HAVE_GIO
+FFvariant ffSettingsGetGSettings(const char* schemaName, const char* path, const char* key, FFvarianttype type) {
+ FF_UNUSED(schemaName, path, key, type)
+ return FF_VARIANT_NULL;
+}
+#endif // FF_HAVE_GIO
+
+#ifdef FF_HAVE_DCONF
+ #include <dconf.h>
+
+typedef struct DConfData {
+ FF_LIBRARY_SYMBOL(dconf_client_read_full)
+ FF_LIBRARY_SYMBOL(dconf_client_new)
+ GVariantGetters variantGetters;
+ DConfClient* client;
+
+ bool inited;
+} DConfData;
+
+static const DConfData* getDConfData(void) {
+ static DConfData data;
+
+ if (!data.inited) {
+ data.inited = true;
+
+ FF_LIBRARY_LOAD(libdconf, NULL, "libdconf" FF_LIBRARY_EXTENSION, 2);
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data, dconf_client_read_full, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data, dconf_client_new, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data.variantGetters, g_variant_dup_string, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data.variantGetters, g_variant_get_boolean, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data.variantGetters, g_variant_get_int32, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data.variantGetters, g_variant_unref, NULL)
+
+ data.client = data.ffdconf_client_new();
+ if (data.client) {
+ libdconf = NULL;
+ }
+ }
+ if (!data.client) {
+ return NULL;
+ }
+
+ return &data;
+}
+
+FFvariant ffSettingsGetDConf(const char* key, FFvarianttype type) {
+ const DConfData* data = getDConfData();
+ if (data == NULL) {
+ return FF_VARIANT_NULL;
+ }
+
+ GVariant* variant = data->ffdconf_client_read_full(data->client, key, DCONF_READ_FLAGS_NONE, NULL);
+ if (variant != NULL) {
+ return getGVariantValue(variant, type, &data->variantGetters);
+ }
+
+ variant = data->ffdconf_client_read_full(data->client, key, DCONF_READ_USER_VALUE, NULL);
+ if (variant != NULL) {
+ return getGVariantValue(variant, type, &data->variantGetters);
+ }
+
+ variant = data->ffdconf_client_read_full(data->client, key, DCONF_READ_DEFAULT_VALUE, NULL);
+ return getGVariantValue(variant, type, &data->variantGetters);
+}
+#else // FF_HAVE_DCONF
+FFvariant ffSettingsGetDConf(const char* key, FFvarianttype type) {
+ FF_UNUSED(key, type)
+ return FF_VARIANT_NULL;
+}
+#endif // FF_HAVE_DCONF
+
+FFvariant ffSettingsGetGnome(const char* dconfKey, const char* gsettingsSchemaName, const char* gsettingsPath, const char* gsettingsKey, FFvarianttype type) {
+ FFvariant gsettings = ffSettingsGetGSettings(gsettingsSchemaName, gsettingsPath, gsettingsKey, type);
+
+ if (
+ (type == FF_VARIANT_TYPE_BOOL && gsettings.boolValueSet) ||
+ (type != FF_VARIANT_TYPE_BOOL && gsettings.strValue != NULL)) {
+ return gsettings;
+ }
+
+ return ffSettingsGetDConf(dconfKey, type);
+}
+
+#ifdef FF_HAVE_DBUS
+ #include "common/dbus.h"
+
+FFvariant ffSettingsGetXFConf(const char* channelName, const char* propertyName, FFvarianttype type) {
+ FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {};
+ if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != NULL) {
+ return FF_VARIANT_NULL;
+ }
+
+ DBusMessage* reply = ffDBusGetMethodReply(&dbus, "org.xfce.Xfconf", "/org/xfce/Xfconf", "org.xfce.Xfconf", "GetProperty", channelName, propertyName);
+ if (!reply) {
+ return FF_VARIANT_NULL;
+ }
+
+ DBusMessageIter rootIterator;
+ if (!dbus.lib->ffdbus_message_iter_init(reply, &rootIterator)) {
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+ }
+
+ if (type == FF_VARIANT_TYPE_INT) {
+ int64_t value;
+ if (ffDBusGetInt(&dbus, &rootIterator, &value)) {
+ dbus.lib->ffdbus_message_unref(reply);
+ return (FFvariant) { .intValue = (int32_t) value };
+ }
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+ }
+
+ if (type == FF_VARIANT_TYPE_STRING) {
+ FFstrbuf value = ffStrbufCreate();
+ if (ffDBusGetString(&dbus, &rootIterator, &value)) {
+ dbus.lib->ffdbus_message_unref(reply);
+ return (FFvariant) { .strValue = value.chars }; // Leaks value.chars
+ }
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+ }
+
+ if (type == FF_VARIANT_TYPE_BOOL) {
+ bool value;
+ if (ffDBusGetBool(&dbus, &rootIterator, &value)) {
+ dbus.lib->ffdbus_message_unref(reply);
+ return (FFvariant) { .boolValue = value, .boolValueSet = true };
+ }
+ }
+
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+}
+
+ #define FF_DBUS_ITER_CONTINUE(dbus, iterator) \
+ { \
+ if (!(dbus).lib->ffdbus_message_iter_next(iterator)) \
+ break; \
+ continue; \
+ }
+
+FFvariant ffSettingsGetXFConfFirstMatch(const char* channelName, const char* propertyPrefix, FFvarianttype type, void* data, FFTestXfconfPropCallback* cb) {
+ FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {};
+ if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != NULL) {
+ return FF_VARIANT_NULL;
+ }
+
+ DBusMessage* reply = ffDBusGetMethodReply(&dbus, "org.xfce.Xfconf", "/org/xfce/Xfconf", "org.xfce.Xfconf", "GetAllProperties", channelName, propertyPrefix);
+ if (!reply) {
+ return FF_VARIANT_NULL;
+ }
+
+ DBusMessageIter rootIterator;
+ if (!dbus.lib->ffdbus_message_iter_init(reply, &rootIterator)) {
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+ }
+
+ DBusMessageIter arrayIterator;
+ dbus.lib->ffdbus_message_iter_recurse(&rootIterator, &arrayIterator);
+
+ while (true) {
+ if (dbus.lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_DICT_ENTRY) {
+ FF_DBUS_ITER_CONTINUE(dbus, &arrayIterator)
+ }
+
+ DBusMessageIter dictIterator;
+ dbus.lib->ffdbus_message_iter_recurse(&arrayIterator, &dictIterator);
+
+ const char* key;
+ dbus.lib->ffdbus_message_iter_get_basic(&dictIterator, &key);
+
+ if (cb(data, key)) {
+ FF_DBUS_ITER_CONTINUE(dbus, &arrayIterator)
+ }
+ dbus.lib->ffdbus_message_iter_next(&dictIterator);
+
+ if (type == FF_VARIANT_TYPE_INT) {
+ int64_t value;
+ if (ffDBusGetInt(&dbus, &dictIterator, &value)) {
+ dbus.lib->ffdbus_message_unref(reply);
+ return (FFvariant) { .intValue = (int32_t) value };
+ }
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+ }
+
+ if (type == FF_VARIANT_TYPE_STRING) {
+ FFstrbuf value = ffStrbufCreate();
+ if (ffDBusGetString(&dbus, &dictIterator, &value)) {
+ dbus.lib->ffdbus_message_unref(reply);
+ return (FFvariant) { .strValue = value.chars }; // Leaks value.chars
+ }
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+ }
+
+ if (type == FF_VARIANT_TYPE_BOOL) {
+ bool value;
+ if (ffDBusGetBool(&dbus, &dictIterator, &value)) {
+ dbus.lib->ffdbus_message_unref(reply);
+ return (FFvariant) { .boolValue = value, .boolValueSet = true };
+ }
+ }
+
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+ }
+
+ dbus.lib->ffdbus_message_unref(reply);
+ return FF_VARIANT_NULL;
+}
+#else // FF_HAVE_DBUS
+FFvariant ffSettingsGetXFConf(const char* channelName, const char* propertyName, FFvarianttype type) {
+ FF_UNUSED(channelName, propertyName, type)
+ return FF_VARIANT_NULL;
+}
+FFvariant ffSettingsGetXFConfFirstMatch(const char* channelName, const char* propertyPrefix, FFvarianttype type, void* data, FFTestXfconfPropCallback* cb) {
+ FF_UNUSED(channelName, propertyPrefix, type, data, cb);
+ return FF_VARIANT_NULL;
+}
+#endif // FF_HAVE_DBUS
+
+#ifdef FF_HAVE_SQLITE3
+ #include <sqlite3.h>
+
+typedef struct SQLiteData {
+ FF_LIBRARY_SYMBOL(sqlite3_open_v2)
+ FF_LIBRARY_SYMBOL(sqlite3_prepare_v2)
+ FF_LIBRARY_SYMBOL(sqlite3_step)
+ FF_LIBRARY_SYMBOL(sqlite3_data_count)
+ FF_LIBRARY_SYMBOL(sqlite3_column_int)
+ FF_LIBRARY_SYMBOL(sqlite3_column_text)
+ FF_LIBRARY_SYMBOL(sqlite3_finalize)
+ FF_LIBRARY_SYMBOL(sqlite3_close)
+
+ bool inited;
+} SQLiteData;
+
+static const SQLiteData* getSQLiteData(void) {
+ static SQLiteData data;
+
+ if (!data.inited) {
+ data.inited = true;
+ FF_LIBRARY_LOAD(libsqlite, NULL, "libsqlite3" FF_LIBRARY_EXTENSION, 1);
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_open_v2, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_prepare_v2, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_step, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_data_count, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_column_int, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_column_text, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_finalize, NULL)
+ FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_close, NULL)
+ libsqlite = NULL;
+ }
+
+ if (!data.ffsqlite3_close) {
+ return NULL;
+ }
+
+ return &data;
+}
+
+int ffSettingsGetSQLite3Int(const char* dbPath, const char* query) {
+ if (!ffPathExists(dbPath, FF_PATHTYPE_FILE)) {
+ return 0;
+ }
+
+ const SQLiteData* data = getSQLiteData();
+ if (data == NULL) {
+ return 0;
+ }
+
+ sqlite3* db;
+ if (data->ffsqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
+ return 0;
+ }
+
+ sqlite3_stmt* stmt;
+ if (data->ffsqlite3_prepare_v2(db, query, (int) strlen(query), &stmt, NULL) != SQLITE_OK) {
+ data->ffsqlite3_close(db);
+ return 0;
+ }
+
+ if (data->ffsqlite3_step(stmt) != SQLITE_ROW || data->ffsqlite3_data_count(stmt) < 1) {
+ data->ffsqlite3_finalize(stmt);
+ data->ffsqlite3_close(db);
+ return 0;
+ }
+
+ int result = data->ffsqlite3_column_int(stmt, 0);
+
+ data->ffsqlite3_finalize(stmt);
+ data->ffsqlite3_close(db);
+
+ return result;
+}
+
+bool ffSettingsGetSQLite3String(const char* dbPath, const char* query, FFstrbuf* result) {
+ if (!ffPathExists(dbPath, FF_PATHTYPE_FILE)) {
+ return false;
+ }
+
+ const SQLiteData* data = getSQLiteData();
+ if (data == NULL) {
+ return false;
+ }
+
+ sqlite3* db;
+ if (data->ffsqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
+ return false;
+ }
+
+ sqlite3_stmt* stmt;
+ if (data->ffsqlite3_prepare_v2(db, query, (int) strlen(query), &stmt, NULL) != SQLITE_OK) {
+ data->ffsqlite3_close(db);
+ return false;
+ }
+
+ if (data->ffsqlite3_step(stmt) != SQLITE_ROW || data->ffsqlite3_data_count(stmt) < 1) {
+ data->ffsqlite3_finalize(stmt);
+ data->ffsqlite3_close(db);
+ return false;
+ }
+
+ ffStrbufSetS(result, (const char*) data->ffsqlite3_column_text(stmt, 0));
+
+ data->ffsqlite3_finalize(stmt);
+ data->ffsqlite3_close(db);
+
+ return true;
+}
+#else // FF_HAVE_SQLITE3
+int ffSettingsGetSQLite3Int(const char* dbPath, const char* query) {
+ FF_UNUSED(dbPath, query)
+ return 0;
+}
+bool ffSettingsGetSQLite3String(const char* dbPath, const char* query, FFstrbuf* result) {
+ FF_UNUSED(dbPath, query, result)
+ return false;
+}
+#endif // FF_HAVE_SQLITE3
+
+#ifdef __ANDROID__
+ #include <sys/system_properties.h>
+bool ffSettingsGetAndroidProperty(const char* propName, FFstrbuf* result) {
+ ffStrbufEnsureFree(result, PROP_VALUE_MAX);
+ int len = __system_property_get(propName, result->chars + result->length);
+ if (len <= 0) {
+ return false;
+ }
+ result->length += (uint32_t) len;
+ result->chars[result->length] = '\0';
+ return true;
+}
+#elif defined(__FreeBSD__)
+ #include <kenv.h>
+bool ffSettingsGetFreeBSDKenv(const char* propName, FFstrbuf* result) {
+ // https://wiki.ghostbsd.org/index.php/Kenv
+ ffStrbufEnsureFree(result, KENV_MVALLEN);
+ int len = kenv(KENV_GET, propName, result->chars + result->length, KENV_MVALLEN);
+ if (len <= 1) {
+ return false; // number of bytes copied, including NUL terminator
+ }
+ result->length += (uint32_t) len - 1;
+ return true;
+}
+#endif
+
+#ifdef FF_HAVE_EET
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wconversion"
+ #pragma GCC diagnostic ignored "-Wsign-conversion"
+ #pragma GCC diagnostic ignored "-Wfloat-conversion"
+ #include <Eet.h>
+ #pragma GCC diagnostic pop
+
+typedef struct E_Font_Default {
+ char* text_class;
+ char* font;
+ int size;
+} E_Font_Default;
+
+typedef struct E_Config {
+ char* theme_default_border_style;
+ char* icon_theme;
+ int use_e_cursor;
+ int cursor_size;
+ char* desktop_default_background;
+ Eina_List* font_defaults;
+} E_Config; // Must be the same name as the top level struct in e.cfg
+
+ #define FF_EET_EINA_FILE_DATA_DESCRIPTOR_CLASS_SET(clas, type) \
+ (ffeet_eina_file_data_descriptor_class_set(clas, sizeof(*(clas)), #type, sizeof(type)))
+ #define FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, struct_type, member, type) \
+ do { \
+ struct_type ___ett; \
+ ffeet_data_descriptor_element_add(edd, #member, type, EET_G_UNKNOWN, (char*) (&(___ett.member)) - (char*) (&(___ett)), 0, /* 0, */ NULL, NULL); \
+ } while (0)
+ #define FF_EET_DATA_DESCRIPTOR_ADD_LIST(edd, struct_type, member, subtype) \
+ do { \
+ struct_type ___ett; \
+ ffeet_data_descriptor_element_add(edd, #member, EET_T_UNKNOW, EET_G_LIST, (char*) (&(___ett.member)) - (char*) (&(___ett)), 0, /* 0, */ NULL, subtype); \
+ } while (0)
+
+bool ffSettingsGetEnlightenmentProperty(ffEnlightenmentSettings* result) {
+ FF_LIBRARY_LOAD(libeet, false, "libeet" FF_LIBRARY_EXTENSION, 1);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_init, false);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_open, false);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_data_descriptor_file_new, false);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_data_read, false);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_close, false);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_shutdown, false);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_data_descriptor_free, false);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_eina_file_data_descriptor_class_set, false);
+ FF_LIBRARY_LOAD_SYMBOL(libeet, eet_data_descriptor_element_add, false);
+
+ if (ffeet_init() == 0) {
+ return false;
+ }
+
+ FF_STRBUF_AUTO_DESTROY fileName = ffStrbufCreateCopy(&instance.state.platform.homeDir);
+ ffStrbufAppendS(&fileName, ".e/e/config/standard/e.cfg");
+
+ Eet_File* ef = ffeet_open(fileName.chars, EET_FILE_MODE_READ);
+ if (!ef) {
+ ffeet_shutdown();
+ return false;
+ }
+
+ Eet_Data_Descriptor_Class fontDdc;
+ FF_EET_EINA_FILE_DATA_DESCRIPTOR_CLASS_SET(&fontDdc, E_Font_Default);
+ Eet_Data_Descriptor* fontDdd = ffeet_data_descriptor_file_new(&fontDdc);
+ if (!fontDdd) {
+ ffeet_close(ef);
+ ffeet_shutdown();
+ return false;
+ }
+ FF_EET_DATA_DESCRIPTOR_ADD_BASIC(fontDdd, E_Font_Default, text_class, EET_T_STRING);
+ FF_EET_DATA_DESCRIPTOR_ADD_BASIC(fontDdd, E_Font_Default, font, EET_T_STRING);
+ FF_EET_DATA_DESCRIPTOR_ADD_BASIC(fontDdd, E_Font_Default, size, EET_T_INT);
+
+ Eet_Data_Descriptor_Class eddc;
+ FF_EET_EINA_FILE_DATA_DESCRIPTOR_CLASS_SET(&eddc, E_Config);
+ Eet_Data_Descriptor* edd = ffeet_data_descriptor_file_new(&eddc);
+ if (!edd) {
+ ffeet_data_descriptor_free(fontDdd);
+ ffeet_close(ef);
+ ffeet_shutdown();
+ return false;
+ }
+
+ FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, theme_default_border_style, EET_T_STRING);
+ FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, icon_theme, EET_T_STRING);
+ FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, use_e_cursor, EET_T_INT);
+ FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, cursor_size, EET_T_INT);
+ FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, desktop_default_background, EET_T_STRING);
+ FF_EET_DATA_DESCRIPTOR_ADD_LIST(edd, E_Config, font_defaults, fontDdd);
+
+ E_Config* parsed = ffeet_data_read(ef, edd, "config");
+
+ if (parsed) {
+ // TODO: find a better method to get the main theme name
+ result->theme = parsed->theme_default_border_style;
+ result->icon_theme = parsed->icon_theme;
+ result->use_e_cursor = !!parsed->use_e_cursor;
+ result->cursor_size = parsed->cursor_size;
+ result->desktop_default_background = parsed->desktop_default_background;
+
+ E_Font_Default* firstFont = eina_list_data_get(parsed->font_defaults);
+ if (firstFont) {
+ result->font = firstFont->font;
+ }
+ }
+
+ ffeet_close(ef);
+ ffeet_data_descriptor_free(edd);
+ ffeet_data_descriptor_free(fontDdd);
+ if (!parsed) {
+ // We don't shutdown eet so that `result->*` are not freed
+ ffeet_shutdown();
+ }
+
+ return !!parsed;
+}
+#else
+bool ffSettingsGetEnlightenmentProperty(FF_A_UNUSED ffEnlightenmentSettings* result) {
+ return false;
+}
+#endif
diff --git a/src/common/impl/size.c b/src/common/impl/size.c
new file mode 100644
index 0000000..4e8d1e3
--- /dev/null
+++ b/src/common/impl/size.c
@@ -0,0 +1,42 @@
+#include "common/size.h"
+
+#include <inttypes.h>
+
+static void appendNum(FFstrbuf* result, uint64_t bytes, uint32_t base, const char** prefixes) {
+ const FFOptionsDisplay* options = &instance.config.display;
+ double size = (double) bytes;
+ uint8_t counter = 0;
+
+ while (size >= base && counter < options->sizeMaxPrefix && prefixes[counter + 1]) {
+ size /= base;
+ counter++;
+ }
+
+ if (counter == 0) {
+ ffStrbufAppendUInt(result, bytes);
+ } else {
+ ffStrbufAppendDouble(result, size, (int8_t) options->sizeNdigits, true);
+ }
+ if (options->sizeSpaceBeforeUnit != FF_SPACE_BEFORE_UNIT_NEVER) {
+ ffStrbufAppendC(result, ' ');
+ }
+ ffStrbufAppendS(result, prefixes[counter]);
+}
+
+void ffSizeAppendNum(uint64_t bytes, FFstrbuf* result) {
+ const FFOptionsDisplay* options = &instance.config.display;
+ switch (options->sizeBinaryPrefix) {
+ case FF_SIZE_BINARY_PREFIX_TYPE_IEC:
+ appendNum(result, bytes, 1024, (const char*[]) { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", NULL });
+ break;
+ case FF_SIZE_BINARY_PREFIX_TYPE_SI:
+ appendNum(result, bytes, 1000, (const char*[]) { "B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", NULL });
+ break;
+ case FF_SIZE_BINARY_PREFIX_TYPE_JEDEC:
+ appendNum(result, bytes, 1024, (const char*[]) { "B", "KB", "MB", "GB", "TB", NULL });
+ break;
+ default:
+ appendNum(result, bytes, 1024, (const char*[]) { "B", NULL });
+ break;
+ }
+}
diff --git a/src/common/impl/smbios.c b/src/common/impl/smbios.c
new file mode 100644
index 0000000..028bcd9
--- /dev/null
+++ b/src/common/impl/smbios.c
@@ -0,0 +1,805 @@
+#include "common/smbios.h"
+#include "common/io.h"
+#include "common/mallocHelper.h"
+#include "common/debug.h"
+
+bool ffIsSmbiosValueSet(FFstrbuf* value) {
+ ffStrbufTrimRightSpace(value);
+ return value->length > 0 &&
+ !ffStrbufStartsWithIgnCaseS(value, "To be filled") &&
+ !ffStrbufStartsWithIgnCaseS(value, "To be set") &&
+ !ffStrbufStartsWithIgnCaseS(value, "OEM") &&
+ !ffStrbufStartsWithIgnCaseS(value, "O.E.M.") &&
+ !ffStrbufStartsWithIgnCaseS(value, "System Product") &&
+ !ffStrbufStartsWithIgnCaseS(value, "Unknown Product") &&
+ !ffStrbufIgnCaseEqualS(value, "None") &&
+ !ffStrbufIgnCaseEqualS(value, "System Name") &&
+ !ffStrbufIgnCaseEqualS(value, "System Version") &&
+ !ffStrbufIgnCaseEqualS(value, "System SKU#") &&
+ !ffStrbufIgnCaseEqualS(value, "Default string") &&
+ !ffStrbufIgnCaseEqualS(value, "Undefined") &&
+ !ffStrbufIgnCaseEqualS(value, "Not Specified") &&
+ !ffStrbufIgnCaseEqualS(value, "Not Applicable") &&
+ !ffStrbufIgnCaseEqualS(value, "Not Defined") &&
+ !ffStrbufIgnCaseEqualS(value, "Not Available") &&
+ !ffStrbufIgnCaseEqualS(value, "INVALID") &&
+ !ffStrbufIgnCaseEqualS(value, "Type1ProductConfigId") &&
+ !ffStrbufIgnCaseEqualS(value, "TBD by OEM") &&
+ !ffStrbufIgnCaseEqualS(value, "No Enclosure") &&
+ !ffStrbufIgnCaseEqualS(value, "Chassis Version") &&
+ !ffStrbufIgnCaseEqualS(value, "All Series") &&
+ !ffStrbufIgnCaseEqualS(value, "N/A") &&
+ !ffStrbufIgnCaseEqualS(value, "Unknown") &&
+ !ffStrbufIgnCaseEqualS(value, "Standard") && ({
+ // Some SMBIOS implementations use "0x0000" to indicate an unset value, even for strings.
+ bool zero = ffStrbufStartsWithS(value, "0x0");
+ if (zero) {
+ for (size_t i = 2; i < value->length; i++) {
+ char c = value->chars[i];
+ if (c != '0') {
+ zero = false;
+ break;
+ }
+ }
+ }
+ !zero;
+ });
+}
+
+static bool smbiosTableInitialized = false;
+static FFSmbiosHeaderTable smbiosTable;
+
+const FFSmbiosHeader* ffSmbiosNextEntry(const FFSmbiosHeader* header) {
+ const char* p = ((const char*) header) + header->Length;
+ if (*p) {
+ do {
+ p += strlen(p) + 1;
+ } while (*p);
+ } else { // The terminator is always double 0 even if there is no string
+ p++;
+ }
+
+ return (const FFSmbiosHeader*) (p + 1);
+}
+
+static bool parseSmbiosTable(const uint8_t* data, uint32_t length) {
+ const FFSmbiosHeader* endOfTable = NULL;
+
+ FF_DEBUG("Parsing SMBIOS table structures with length %u bytes", length);
+ FF_A_UNUSED int structureCount = 0, totalCount = 0;
+ for (
+ const FFSmbiosHeader* header = (const FFSmbiosHeader*) data;
+ (const uint8_t*) header + sizeof(FFSmbiosHeader) < (const uint8_t*) data + length;
+ header = ffSmbiosNextEntry(header)) {
+ ++totalCount;
+ endOfTable = header;
+
+ if (header->Length < sizeof(FFSmbiosHeader)) {
+ FF_DEBUG("Invalid SMBIOS structure length %u at offset 0x%lx",
+ header->Length,
+ (unsigned long) ((const uint8_t*) header - data));
+ break;
+ }
+
+ if (header->Handle >= 0xFF00) {
+ FF_DEBUG("Invalid SMBIOS structure handle 0x%04x at offset 0x%lx",
+ header->Handle,
+ (unsigned long) ((const uint8_t*) header - data));
+ break;
+ }
+
+ // This doesn't verify the entire structure (e.g. string section can still be truncated),
+ // but at least ensures the formatted section is valid and prevents infinite loops
+ // when the table is severely malformed.
+ if (__builtin_expect((const uint8_t*) header + header->Length > (const uint8_t*) data + length, false)) {
+ FF_DEBUG("Truncated SMBIOS structure at offset 0x%lx: length %u is too small",
+ (unsigned long) ((const uint8_t*) header - data),
+ header->Length);
+ break;
+ }
+
+ if (header->Type < FF_SMBIOS_TYPE_END_OF_TABLE) {
+ if (!smbiosTable[header->Type]) {
+ smbiosTable[header->Type] = header;
+ FF_DEBUG("Found SMBIOS structure type %u, handle 0x%04X, length %u",
+ header->Type,
+ header->Handle,
+ header->Length);
+ structureCount++;
+ } else {
+ FF_DEBUG("Duplicate SMBIOS structure type %u, handle 0x%04X, length %u",
+ header->Type,
+ header->Handle,
+ header->Length);
+ }
+ } else if (header->Type == FF_SMBIOS_TYPE_END_OF_TABLE) {
+ FF_DEBUG("Reached SMBIOS end of type %u, handle 0x%04X, length %u",
+ header->Type,
+ header->Handle,
+ header->Length);
+ break;
+ } else {
+ FF_DEBUG("Found custom SMBIOS structure type %u, handle 0x%04X, length %u; ignoring",
+ header->Type,
+ header->Handle,
+ header->Length);
+ }
+ }
+
+ if (!endOfTable) {
+ FF_DEBUG("No SMBIOS structures found in table");
+ return false;
+ }
+
+ FF_DEBUG("Parsed %d/%d SMBIOS structures, end-of-table (Type 127) %s",
+ structureCount,
+ totalCount,
+ endOfTable->Type == FF_SMBIOS_TYPE_END_OF_TABLE ? "found." : "not found! SMBIOS data may be malformed.");
+ smbiosTable[FF_SMBIOS_TYPE_END_OF_TABLE] = endOfTable;
+
+ return true;
+}
+
+#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__sun) || defined(__HAIKU__) || defined(__OpenBSD__) || defined(__GNU__)
+ #include <fcntl.h>
+ #include <sys/stat.h>
+ #include <sys/types.h>
+ #include <sys/mman.h>
+ #include <stddef.h>
+
+ #ifdef __linux__
+ #include "common/properties.h"
+ #elif defined(__FreeBSD__)
+ #include "common/settings.h"
+ #elif defined(__NetBSD__)
+ #include "common/sysctl.h"
+ #endif
+
+ #ifdef __linux__
+bool ffGetSmbiosValue(const char* devicesPath, const char* classPath, FFstrbuf* buffer) {
+ // /sys/class/dmi/id/* are all pseudo-files with very small content
+ // so reading the whole file at once is efficient
+ ffStrbufEnsureFixedLengthFree(buffer, 127);
+
+ ssize_t len = ffReadFileData(devicesPath, buffer->allocated - 1, buffer->chars);
+ if (len > 0) {
+ assert(len < buffer->allocated);
+ buffer->chars[len] = '\0';
+ buffer->length = (uint32_t) len;
+ ffStrbufTrimRightSpace(buffer);
+ if (ffIsSmbiosValueSet(buffer)) {
+ return true;
+ }
+ }
+
+ len = ffReadFileData(classPath, buffer->allocated - 1, buffer->chars);
+ if (len > 0) {
+ assert(len < buffer->allocated);
+ buffer->chars[len] = '\0';
+ buffer->length = (uint32_t) len;
+ ffStrbufTrimRightSpace(buffer);
+ if (ffIsSmbiosValueSet(buffer)) {
+ return true;
+ }
+ }
+
+ ffStrbufClear(buffer);
+ return false;
+}
+ #endif
+
+static bool readPhysicalMemory(int fd, off_t address, size_t length, void* buffer) {
+ #if !defined(__FreeBSD__) // Either causes kernel panic or returns EFAULT
+ // -1: unknown, 0: failed before (stop trying), 1: succeeded before
+ static int preadState = -1;
+ if (preadState != 0) {
+ ssize_t bytesRead = pread(fd, buffer, length, address);
+ if (bytesRead == (ssize_t) length) {
+ preadState = 1;
+ return true;
+ }
+
+ FF_DEBUG("pread failed at address 0x%lx for %zu bytes: %s. Falling back to mmap%s",
+ (unsigned long) address,
+ length,
+ strerror(errno),
+ preadState < 0 ? " and caching failure" : "");
+ preadState = 0;
+ } else {
+ FF_DEBUG("Skipping pread due to cached failure; using mmap");
+ }
+ #endif
+
+ off_t alignedAddress = address & ~((off_t) instance.state.platform.sysinfo.pageSize - 1);
+ size_t pageOffset = (size_t) (address - alignedAddress);
+ size_t mapLength = pageOffset + length;
+
+ void* p = mmap(NULL, mapLength, PROT_READ, MAP_SHARED, fd, alignedAddress);
+ if (p == MAP_FAILED) {
+ FF_DEBUG("mmap failed at aligned address 0x%lx for %zu bytes: %s",
+ (unsigned long) alignedAddress,
+ mapLength,
+ strerror(errno));
+ return false;
+ }
+
+ memcpy(buffer, (const uint8_t*) p + pageOffset, length);
+ munmap(p, mapLength);
+ return true;
+}
+
+typedef struct FFSmbios20EntryPoint {
+ uint8_t AnchorString[4];
+ uint8_t EntryPointStructureChecksum;
+ uint8_t EntryPointLength;
+ uint8_t SmbiosMajorVersion;
+ uint8_t SmbiosMinorVersion;
+ uint16_t MaximumStructureSize;
+ uint8_t EntryPointRevision;
+ uint8_t FormattedArea[5];
+ uint8_t IntermediateAnchorString[5];
+ uint8_t IntermediateChecksum;
+ uint16_t StructureTableLength;
+ uint32_t StructureTableAddress;
+ uint16_t NumberOfSmbiosStructures;
+ uint8_t SmbiosBcdRevision;
+} FF_A_PACKED FFSmbios20EntryPoint;
+static_assert(offsetof(FFSmbios20EntryPoint, SmbiosBcdRevision) == 0x1E,
+ "FFSmbios20EntryPoint: Wrong struct alignment");
+
+typedef struct FFSmbios30EntryPoint {
+ uint8_t AnchorString[5];
+ uint8_t EntryPointStructureChecksum;
+ uint8_t EntryPointLength;
+ uint8_t SmbiosMajorVersion;
+ uint8_t SmbiosMinorVersion;
+ uint8_t SmbiosDocrev;
+ uint8_t EntryPointRevision;
+ uint8_t Reversed;
+ uint32_t StructureTableMaximumSize;
+ uint64_t StructureTableAddress;
+} FF_A_PACKED FFSmbios30EntryPoint;
+
+static_assert(offsetof(FFSmbios30EntryPoint, StructureTableAddress) == 0x10,
+ "FFSmbios30EntryPoint: Wrong struct alignment");
+
+typedef union FFSmbiosEntryPoint {
+ FFSmbios20EntryPoint Smbios20;
+ FFSmbios30EntryPoint Smbios30;
+} FFSmbiosEntryPoint;
+
+static bool fillTableBufferFallback(FFstrbuf* buffer) {
+ const char* devMem =
+ #if __HAIKU__
+ "/dev/misc/mem";
+ #else
+ "/dev/mem"; // kern.securelevel must be -1
+ #endif
+ FF_DEBUG("Using physical memory searching implementation: %s", devMem);
+
+ uint32_t tableLength = 0;
+ off_t tableAddress = 0;
+ FF_AUTO_CLOSE_FD int fd = open(devMem, O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ FF_DEBUG("Failed to open memory device: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("Memory device opened successfully with fd=%d", fd);
+
+ // Works on legacy BIOS only
+ // See: https://wiki.osdev.org/System_Management_BIOS#UEFI_systems
+ // On BSD systems, we can get EFI system resource table (ESRT) via EFIIOC_GET_TABLE
+ // However, to acquire SMBIOS entry point, we need EFI configuration table (provided by EFI system table)
+ // which is not available via EFIIOC_GET_TABLE.
+ FF_AUTO_FREE uint8_t* smBiosBase = malloc(0x10000);
+ if (!readPhysicalMemory(fd, 0xF0000, 0x10000, smBiosBase)) {
+ FF_DEBUG("Failed to read SMBIOS memory region");
+ return false;
+ }
+ FF_DEBUG("Successfully read 0x10000 bytes from physical address 0xF0000");
+
+ for (off_t offset = 0; offset <= 0xffe0; offset += 0x10) {
+ FFSmbiosEntryPoint* p = (void*) (smBiosBase + offset);
+ if (memcmp(p, "_SM3_", sizeof(p->Smbios30.AnchorString)) == 0) {
+ FF_DEBUG("Found SMBIOS 3.0 entry point at phyaddr 0x%05lX", (unsigned long) (0xF0000 + offset));
+ if (p->Smbios30.EntryPointLength != sizeof(p->Smbios30)) {
+ FF_DEBUG("Invalid SMBIOS 3.0 entry point length: %u (expected %zu)",
+ p->Smbios30.EntryPointLength,
+ sizeof(p->Smbios30));
+ return false;
+ }
+ tableLength = p->Smbios30.StructureTableMaximumSize;
+ tableAddress = (off_t) p->Smbios30.StructureTableAddress;
+ FF_DEBUG("SMBIOS 3.0: tableLength=0x%x, tableAddress=0x%lx, version=%u.%u.%u",
+ tableLength,
+ (unsigned long) tableAddress,
+ p->Smbios30.SmbiosMajorVersion,
+ p->Smbios30.SmbiosMinorVersion,
+ p->Smbios30.SmbiosDocrev);
+ break;
+ } else if (memcmp(p, "_SM_", sizeof(p->Smbios20.AnchorString)) == 0) {
+ FF_DEBUG("Found SMBIOS 2.0 entry point at phyaddr 0x%05lX", (unsigned long) (0xF0000 + offset));
+ if (p->Smbios20.EntryPointLength != sizeof(p->Smbios20)) {
+ FF_DEBUG("Invalid SMBIOS 2.0 entry point length: %u (expected %zu)",
+ p->Smbios20.EntryPointLength,
+ sizeof(p->Smbios20));
+ return false;
+ }
+ tableLength = p->Smbios20.StructureTableLength;
+ tableAddress = (off_t) p->Smbios20.StructureTableAddress;
+ FF_DEBUG("SMBIOS 2.0: tableLength=0x%x, tableAddress=0x%lx, version=%u.%u",
+ tableLength,
+ (unsigned long) tableAddress,
+ p->Smbios20.SmbiosMajorVersion,
+ p->Smbios20.SmbiosMinorVersion);
+ break;
+ }
+ }
+ if (tableLength == 0) {
+ FF_DEBUG("No valid SMBIOS entry point found in memory region");
+ return false;
+ }
+
+ ffStrbufClear(buffer);
+ ffStrbufEnsureFixedLengthFree(buffer, tableLength);
+ FF_DEBUG("Attempting to read SMBIOS table data: %u bytes at 0x%lx", tableLength, (unsigned long) tableAddress);
+ if (readPhysicalMemory(fd, (off_t) tableAddress, tableLength, buffer->chars)) {
+ buffer->length = tableLength;
+ buffer->chars[buffer->length] = '\0';
+ FF_DEBUG("Successfully read SMBIOS table data: %u bytes", tableLength);
+ } else {
+ FF_DEBUG("Failed to read SMBIOS table data");
+ return false;
+ }
+
+ return true;
+}
+
+ #ifdef __OpenBSD__
+static bool detectSmbiosTableLength(const uint8_t* data, uint32_t bufferLength, uint32_t* tableLength) {
+ const uint8_t* p = data;
+ const uint8_t* end = data + bufferLength;
+
+ while (p + sizeof(FFSmbiosHeader) <= end) {
+ const FFSmbiosHeader* header = (const FFSmbiosHeader*) p;
+ if (header->Length < sizeof(*header)) {
+ FF_DEBUG("Invalid SMBIOS structure length %u at offset 0x%lx",
+ header->Length,
+ (unsigned long) (p - data));
+ return false;
+ }
+
+ if (header->Handle >= 0xFF00) {
+ FF_DEBUG("Invalid SMBIOS structure handle 0x%04x at offset 0x%lx",
+ header->Handle,
+ (unsigned long) (p - data));
+ return false;
+ }
+
+ const uint8_t* formattedEnd = p + header->Length;
+ if (formattedEnd > end) {
+ FF_DEBUG("Truncated SMBIOS structure at offset 0x%lx: length %u is too small",
+ (unsigned long) (p - data),
+ header->Length);
+ return false;
+ }
+
+ const char* string = (const char*) formattedEnd;
+ const char* stringEnd = (const char*) end;
+ while (true) {
+ size_t remaining = (size_t) (stringEnd - string);
+ if (remaining == 0) {
+ return false;
+ }
+
+ const char* nul = memchr(string, '\0', remaining);
+ if (!nul) {
+ return false;
+ }
+
+ string = nul + 1;
+ if (string >= stringEnd) {
+ return false;
+ }
+
+ if (*string == '\0') {
+ ++string;
+ break;
+ }
+ }
+
+ if (header->Type == FF_SMBIOS_TYPE_END_OF_TABLE) {
+ *tableLength = (uint32_t) (string - (const char*) data);
+ return true;
+ }
+
+ p = (const uint8_t*) string;
+ }
+
+ return false;
+}
+
+static bool fillTableBufferPlatform(FFstrbuf* buffer) {
+ FF_DEBUG("Using OpenBSD /var/run/dmesg.boot implementation");
+
+ char dmesg[2048];
+ ssize_t size = ffReadFileData("/var/run/dmesg.boot", sizeof(dmesg) - 1, dmesg);
+ if (size <= 0) {
+ FF_DEBUG("Failed to read /var/run/dmesg.boot");
+ return false;
+ } else {
+ FF_DEBUG("Successfully read %zd bytes from /var/run/dmesg.boot", size);
+ }
+ dmesg[size] = '\0';
+
+ const char* const needle = "\nbios0 at mainbus0: SMBIOS rev. ";
+ size_t needleLen = strlen(needle);
+ char* line = memmem(dmesg, (size_t) size, needle, needleLen);
+ if (!line) {
+ FF_DEBUG("Failed to find SMBIOS line in /var/run/dmesg.boot");
+ return false;
+ }
+ line += needleLen;
+
+ char* lineEnd = memchr(line, '\n', (size_t) ((dmesg + size) - line));
+ if (!lineEnd) {
+ lineEnd = dmesg + size;
+ }
+
+ char* address = memchr(line, '@', (size_t) (lineEnd - line));
+ if (!address) {
+ FF_DEBUG("Failed to find SMBIOS table address in dmesg line");
+ return false;
+ }
+
+ do {
+ ++address;
+ } while (address < lineEnd && (*address == ' ' || *address == '\t'));
+
+ errno = 0;
+ char* addressEnd = NULL;
+ unsigned long long parsedAddress = strtoull(address, &addressEnd, 16);
+ if (errno != 0 || addressEnd == address || parsedAddress == 0) {
+ FF_DEBUG("Failed to parse OpenBSD SMBIOS table address from line: %.*s",
+ (int) (lineEnd - line),
+ line);
+ return false;
+ }
+
+ off_t tableAddress = (off_t) parsedAddress;
+ FF_DEBUG("Parsed OpenBSD SMBIOS table address: 0x%llx", parsedAddress);
+
+ FF_AUTO_CLOSE_FD int fd = open("/dev/mem", O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ FF_DEBUG("Failed to open /dev/mem: %s", strerror(errno));
+ return false;
+ }
+
+ uint32_t readLength = 0x10000;
+ ffStrbufClear(buffer);
+ ffStrbufEnsureFixedLengthFree(buffer, readLength);
+
+ FF_DEBUG("Attempting to read OpenBSD SMBIOS table data: %u bytes at 0x%lx",
+ readLength,
+ (unsigned long) tableAddress);
+
+ if (!readPhysicalMemory(fd, tableAddress, readLength, buffer->chars)) {
+ FF_DEBUG("Failed to read OpenBSD SMBIOS table data");
+ return false;
+ }
+
+ uint32_t detectedLength = 0;
+ if (detectSmbiosTableLength((const uint8_t*) buffer->chars, readLength, &detectedLength)) {
+ buffer->length = detectedLength;
+ buffer->chars[buffer->length] = '\0';
+ FF_DEBUG("Determined OpenBSD SMBIOS table length: %u bytes", detectedLength);
+ return true;
+ }
+
+ FF_DEBUG("SMBIOS end-of-table marker not found within first %u bytes; give up", readLength);
+ ffStrbufClear(buffer);
+ return false;
+}
+ #else
+static bool fillTableBufferPlatform(FFstrbuf* buffer) {
+ #if __HAIKU__ && __GNU__
+ return false;
+ #elif defined(__linux__)
+ FF_DEBUG("Using Linux implementation - trying /sys/firmware/dmi/tables/DMI");
+ if (ffAppendFileBuffer("/sys/firmware/dmi/tables/DMI", buffer)) {
+ return true;
+ }
+
+ FF_DEBUG("Failed to read /sys/firmware/dmi/tables/DMI, falling back to memory-mapped implementation");
+ #endif
+
+ {
+ #if !defined(__sun) && !defined(__NetBSD__)
+ FF_DEBUG("Using memory-mapped implementation");
+ FF_STRBUF_AUTO_DESTROY strEntryAddress = ffStrbufCreate();
+ #ifdef __FreeBSD__
+ FF_DEBUG("Using FreeBSD kenv implementation");
+ if (!ffSettingsGetFreeBSDKenv("hint.smbios.0.mem", &strEntryAddress)) {
+ FF_DEBUG("Failed to get SMBIOS address from FreeBSD kenv");
+ return false; // non-UEFI systems
+ }
+ FF_DEBUG("Got SMBIOS address from kenv: %s", strEntryAddress.chars);
+ #elif defined(__linux__)
+ {
+ FF_DEBUG("Using Linux EFI systab implementation");
+ FF_STRBUF_AUTO_DESTROY systab = ffStrbufCreate();
+ if (!ffAppendFileBuffer("/sys/firmware/efi/systab", &systab)) {
+ FF_DEBUG("Failed to read /sys/firmware/efi/systab");
+ return false;
+ }
+ if (!ffParsePropLines(systab.chars, "SMBIOS3=", &strEntryAddress) &&
+ !ffParsePropLines(systab.chars, "SMBIOS=", &strEntryAddress)) {
+ FF_DEBUG("Failed to find SMBIOS entry in systab");
+ return false;
+ }
+ FF_DEBUG("Found SMBIOS entry in systab: %s", strEntryAddress.chars);
+ }
+ #endif
+
+ off_t entryAddress = (off_t) strtol(strEntryAddress.chars, NULL, 16);
+ if (entryAddress == 0) {
+ FF_DEBUG("Invalid SMBIOS entry address: 0");
+ return false;
+ }
+ FF_DEBUG("Parsed SMBIOS entry address: 0x%lx", (unsigned long) entryAddress);
+
+ FF_AUTO_CLOSE_FD int fd = open("/dev/mem", O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ FF_DEBUG("Failed to open /dev/mem: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("/dev/mem opened successfully with fd=%d", fd);
+
+ FFSmbiosEntryPoint entryPoint;
+ FF_DEBUG("Attempting to read %zu bytes from physical address 0x%lx",
+ sizeof(entryPoint),
+ (unsigned long) entryAddress);
+ if (!readPhysicalMemory(fd, entryAddress, sizeof(entryPoint), &entryPoint)) {
+ return false;
+ }
+ FF_DEBUG("Successfully read SMBIOS entry point data");
+ #else
+ // Sun or NetBSD
+ FF_DEBUG("Using %s specific implementation",
+ #ifdef __NetBSD__
+ "NetBSD"
+ #else
+ "SunOS"
+ #endif
+ );
+
+ FF_AUTO_CLOSE_FD int fd = open("/dev/smbios", O_RDONLY | O_CLOEXEC);
+ if (fd < 0) {
+ FF_DEBUG("Failed to open /dev/smbios: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("/dev/smbios opened successfully with fd=%d", fd);
+
+ FFSmbiosEntryPoint entryPoint;
+ #ifdef __NetBSD__
+ off_t addr = (off_t) ffSysctlGetInt64("machdep.smbios", 0);
+ if (addr == 0) {
+ FF_DEBUG("Failed to get SMBIOS address from sysctl");
+ return false;
+ }
+ FF_DEBUG("Got SMBIOS address from sysctl: 0x%lx", (unsigned long) addr);
+
+ if (pread(fd, &entryPoint, sizeof(entryPoint), addr) < 1) {
+ FF_DEBUG("Failed to read SMBIOS entry point: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("Successfully read SMBIOS entry point");
+ #else
+ FF_DEBUG("Reading SMBIOS entry point from /dev/smbios");
+ if (ffReadFDData(fd, sizeof(entryPoint), &entryPoint) < 1) {
+ FF_DEBUG("Failed to read SMBIOS entry point: %s", strerror(errno));
+ return false;
+ }
+ FF_DEBUG("Successfully read SMBIOS entry point");
+ #endif
+ #endif
+
+ uint32_t tableLength = 0;
+ off_t tableAddress = 0;
+ if (memcmp(entryPoint.Smbios20.AnchorString, "_SM_", sizeof(entryPoint.Smbios20.AnchorString)) == 0) {
+ FF_DEBUG("Found SMBIOS 2.0 entry point");
+ if (entryPoint.Smbios20.EntryPointLength != sizeof(entryPoint.Smbios20)) {
+ FF_DEBUG("Invalid SMBIOS 2.0 entry point length: %u (expected %zu)",
+ entryPoint.Smbios20.EntryPointLength,
+ sizeof(entryPoint.Smbios20));
+ return false;
+ }
+ tableLength = entryPoint.Smbios20.StructureTableLength;
+ tableAddress = (off_t) entryPoint.Smbios20.StructureTableAddress;
+ FF_DEBUG("SMBIOS 2.0: tableLength=0x%x, tableAddress=0x%lx, version=%u.%u",
+ tableLength,
+ (unsigned long) tableAddress,
+ entryPoint.Smbios20.SmbiosMajorVersion,
+ entryPoint.Smbios20.SmbiosMinorVersion);
+ } else if (memcmp(entryPoint.Smbios30.AnchorString, "_SM3_", sizeof(entryPoint.Smbios30.AnchorString)) == 0) {
+ FF_DEBUG("Found SMBIOS 3.0 entry point");
+ if (entryPoint.Smbios30.EntryPointLength != sizeof(entryPoint.Smbios30)) {
+ FF_DEBUG("Invalid SMBIOS 3.0 entry point length: %u (expected %zu)",
+ entryPoint.Smbios30.EntryPointLength,
+ sizeof(entryPoint.Smbios30));
+ return false;
+ }
+ tableLength = entryPoint.Smbios30.StructureTableMaximumSize;
+ tableAddress = (off_t) entryPoint.Smbios30.StructureTableAddress;
+ FF_DEBUG("SMBIOS 3.0: tableLength=0x%x, tableAddress=0x%lx, version=%u.%u.%u",
+ tableLength,
+ (unsigned long) tableAddress,
+ entryPoint.Smbios30.SmbiosMajorVersion,
+ entryPoint.Smbios30.SmbiosMinorVersion,
+ entryPoint.Smbios30.SmbiosDocrev);
+ } else {
+ FF_DEBUG("Unknown SMBIOS entry point format");
+ return false; // Dragonfly goes here
+ }
+
+ ffStrbufClear(buffer);
+ ffStrbufEnsureFixedLengthFree(buffer, tableLength);
+ FF_DEBUG("Attempting to read SMBIOS table data: %u bytes at 0x%lx", tableLength, (unsigned long) tableAddress);
+ if (readPhysicalMemory(fd, tableAddress, tableLength, buffer->chars)) {
+ buffer->length = tableLength;
+ buffer->chars[buffer->length] = '\0';
+ FF_DEBUG("Successfully read SMBIOS table data: %u bytes", tableLength);
+ } else {
+ ffStrbufClear(buffer);
+ return false;
+ }
+ }
+
+ return true;
+}
+ #endif
+
+const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable() {
+ static FFstrbuf buffer;
+ if (!smbiosTableInitialized) {
+ smbiosTableInitialized = true;
+ FF_DEBUG("Initializing SMBIOS buffer");
+ ffStrbufInit(&buffer);
+
+ if (!fillTableBufferPlatform(&buffer)) {
+ FF_DEBUG("Platform specific SMBIOS retrieval failed, trying fallback method");
+ if (!fillTableBufferFallback(&buffer)) {
+ FF_DEBUG("Fallback SMBIOS retrieval also failed");
+ ffStrbufDestroy(&buffer);
+ return NULL;
+ }
+ }
+
+ if (!parseSmbiosTable((const uint8_t*) buffer.chars, buffer.length)) {
+ ffStrbufClear(&buffer);
+ }
+ }
+
+ if (buffer.length == 0) {
+ FF_DEBUG("No valid SMBIOS data available");
+ return NULL;
+ }
+
+ return &smbiosTable;
+}
+#elif defined(_WIN32)
+ #include "common/windows/nt.h"
+
+ #pragma GCC diagnostic ignored "-Wmultichar"
+
+typedef struct FFRawSmbiosData {
+ uint8_t Used20CallingMethod;
+ uint8_t SMBIOSMajorVersion;
+ uint8_t SMBIOSMinorVersion;
+ uint8_t DmiRevision;
+ uint32_t Length;
+ uint8_t SMBIOSTableData[];
+} FFRawSmbiosData;
+
+const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable() {
+ static SYSTEM_FIRMWARE_TABLE_INFORMATION* buffer;
+
+ if (!smbiosTableInitialized) {
+ smbiosTableInitialized = true;
+ FF_DEBUG("Initializing Windows SMBIOS buffer");
+
+ FF_DEBUG("Querying system firmware table size with signature 'RSMB'");
+ SYSTEM_FIRMWARE_TABLE_INFORMATION sfti = {
+ .ProviderSignature = 'RSMB',
+ .Action = SystemFirmwareTableGet,
+ };
+ ULONG bufSize = 0;
+ NtQuerySystemInformation(SystemFirmwareTableInformation, &sfti, sizeof(sfti), &bufSize);
+ if (bufSize <= sizeof(FFRawSmbiosData) + sizeof(sfti)) {
+ FF_DEBUG("Invalid firmware table size: %lu (must be > %zu)", bufSize, sizeof(FFRawSmbiosData) + sizeof(sfti));
+ return NULL;
+ }
+ if (bufSize != sfti.TableBufferLength + (ULONG) sizeof(sfti)) {
+ FF_DEBUG("Firmware table size mismatch: NtQuerySystemInformation returned %lu but expected %lu",
+ bufSize,
+ sfti.TableBufferLength + (ULONG) sizeof(sfti));
+ return NULL;
+ }
+ FF_DEBUG("Firmware table size: %lu bytes", bufSize);
+
+ buffer = malloc(bufSize);
+ *buffer = sfti;
+ FF_DEBUG("Allocated buffer for SMBIOS data");
+
+ if (!NT_SUCCESS(NtQuerySystemInformation(SystemFirmwareTableInformation, buffer, bufSize, &bufSize))) {
+ FF_DEBUG("NtQuerySystemInformation(SystemFirmwareTableInformation) failed");
+ free(buffer);
+ buffer = NULL;
+ return NULL;
+ }
+ FFRawSmbiosData* rawData = (FFRawSmbiosData*) buffer->TableBuffer;
+
+ FF_DEBUG("Successfully retrieved SMBIOS data: version %u.%u, length %u bytes",
+ rawData->SMBIOSMajorVersion,
+ rawData->SMBIOSMinorVersion,
+ rawData->Length);
+
+ if (!parseSmbiosTable(rawData->SMBIOSTableData, rawData->Length)) {
+ free(buffer);
+ buffer = NULL;
+ return NULL;
+ }
+ }
+
+ if (!buffer) {
+ FF_DEBUG("No valid SMBIOS data available");
+ return NULL;
+ }
+ return &smbiosTable;
+}
+#elif defined(__APPLE__)
+ #include "common/apple/cf_helpers.h"
+
+const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable() {
+ static CFDataRef smbiosDataBuffer;
+
+ if (!smbiosTableInitialized) {
+ smbiosTableInitialized = true;
+ FF_DEBUG("Initializing SMBIOS buffer on Apple platform");
+
+ FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t registryEntry = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("AppleSMBIOS"));
+
+ if (!registryEntry) {
+ FF_DEBUG("IOServiceGetMatchingService() failed to find AppleSMBIOS");
+ return NULL;
+ }
+
+ FF_DEBUG("AppleSMBIOS service found, retrieving SMBIOS data");
+ smbiosDataBuffer = IORegistryEntryCreateCFProperty(registryEntry, CFSTR("SMBIOS"), kCFAllocatorDefault, kNilOptions);
+ if (!smbiosDataBuffer) {
+ FF_DEBUG("IORegistryEntryCreateCFProperty() failed to get SMBIOS data");
+ return NULL;
+ }
+ if (CFGetTypeID(smbiosDataBuffer) != CFDataGetTypeID()) {
+ FF_DEBUG("Unexpected SMBIOS data type: expected CFData");
+ CFRelease(smbiosDataBuffer);
+ smbiosDataBuffer = NULL;
+ return NULL;
+ }
+
+ FF_DEBUG("Successfully retrieved SMBIOS data: %lu bytes", CFDataGetLength(smbiosDataBuffer));
+ if (!parseSmbiosTable((const uint8_t*) CFDataGetBytePtr(smbiosDataBuffer), (uint32_t) CFDataGetLength(smbiosDataBuffer))) {
+ CFRelease(smbiosDataBuffer);
+ smbiosDataBuffer = NULL;
+ return NULL;
+ }
+ }
+
+ if (!smbiosDataBuffer) {
+ FF_DEBUG("No valid SMBIOS data available");
+ return NULL;
+ }
+
+ return &smbiosTable;
+}
+#endif
diff --git a/src/common/impl/strutil.c b/src/common/impl/strutil.c
new file mode 100644
index 0000000..3a801f7
--- /dev/null
+++ b/src/common/impl/strutil.c
@@ -0,0 +1,80 @@
+#include "common/strutil.h"
+
+uint8_t ffUtf8CharLenWidth(const char* str, uint32_t length, uint8_t* width) {
+ if (__builtin_expect(length == 0 || *str == '\0', false)) {
+ if (width) {
+ *width = 0;
+ }
+ return 0;
+ }
+
+ unsigned char first = (unsigned char) *str;
+ if (__builtin_expect(first < 0x80, true)) {
+ if (width) {
+ *width = 1;
+ }
+ return 1;
+ }
+
+ uint8_t bytes;
+ if ((first & 0xE0) == 0xC0) {
+ bytes = 2;
+ } else if ((first & 0xF0) == 0xE0) {
+ bytes = 3;
+ } else if ((first & 0xF8) == 0xF0) {
+ bytes = 4;
+ } else {
+ if (width) {
+ *width = 1;
+ }
+ return 1;
+ }
+
+ if (length < bytes) {
+ if (width) {
+ *width = 1;
+ }
+ return 1;
+ }
+
+ for (uint8_t i = 1; i < bytes; ++i) {
+ unsigned char continuation = (unsigned char) str[i];
+ if (continuation == '\0' || (continuation & 0xC0) != 0x80) {
+ if (width) {
+ *width = 1;
+ }
+ return 1;
+ }
+ }
+
+ uint32_t ucs = (uint32_t) (first & ((1U << (8 - bytes)) - 1));
+ for (uint8_t i = 1; i < bytes; ++i) {
+ ucs <<= 6;
+ ucs |= (uint32_t) ((unsigned char) str[i] & 0x3F);
+ }
+
+ int wcWidth = mk_wcwidth(ucs);
+ if (width) {
+ *width = (uint8_t) (wcWidth < 0 ? 0 : wcWidth);
+ }
+ return bytes;
+}
+
+uint32_t ffUtf8StrWidth(const char* str, uint32_t length) {
+ uint32_t result = 0;
+ const char* ptr = str;
+
+ while (length > 0 && *ptr != '\0') {
+ uint8_t width = 0;
+ uint8_t bytes = ffUtf8CharLenWidth(ptr, length, &width);
+ if (__builtin_expect(bytes == 0, false)) {
+ break;
+ }
+
+ result += width;
+ ptr += bytes;
+ length -= bytes;
+ }
+
+ return result > 0 ? result : (uint32_t) (ptr - str);
+}
diff --git a/src/common/impl/sysctl.c b/src/common/impl/sysctl.c
new file mode 100644
index 0000000..5974698
--- /dev/null
+++ b/src/common/impl/sysctl.c
@@ -0,0 +1,93 @@
+#include "common/sysctl.h"
+
+#include <stdlib.h>
+
+#ifdef __OpenBSD__
+const char* ffSysctlGetString(int mib1, int mib2, FFstrbuf* result) {
+ size_t neededLength;
+ if (sysctl((int[]) { mib1, mib2 }, 2, NULL, &neededLength, NULL, 0) != 0 || neededLength == 1) { // neededLength is 1 for empty strings, because of the null terminator
+ return "sysctl() length query failed";
+ }
+
+ ffStrbufEnsureFree(result, (uint32_t) neededLength - 1);
+
+ if (sysctl((int[]) { mib1, mib2 }, 2, result->chars + result->length, &neededLength, NULL, 0) != 0) {
+ return "sysctl() failed to retrieve string data";
+ }
+
+ result->length += (uint32_t) neededLength - 1;
+ result->chars[result->length] = '\0';
+
+ return NULL;
+}
+
+int ffSysctlGetInt(int mib1, int mib2, int defaultValue) {
+ int result;
+ size_t neededLength = sizeof(result);
+ if (sysctl((int[]) { mib1, mib2 }, 2, &result, &neededLength, NULL, 0) != 0) {
+ return defaultValue;
+ }
+ return result;
+}
+
+int64_t ffSysctlGetInt64(int mib1, int mib2, int64_t defaultValue) {
+ int64_t result;
+ size_t neededLength = sizeof(result);
+ if (sysctl((int[]) { mib1, mib2 }, 2, &result, &neededLength, NULL, 0) != 0) {
+ return defaultValue;
+ }
+ return result;
+}
+#else
+const char* ffSysctlGetString(const char* propName, FFstrbuf* result) {
+ size_t neededLength;
+ if (sysctlbyname(propName, NULL, &neededLength, NULL, 0) != 0 || neededLength == 1) { // neededLength is 1 for empty strings, because of the null terminator
+ return "sysctlbyname() failed";
+ }
+
+ ffStrbufEnsureFree(result, (uint32_t) neededLength - 1);
+
+ if (sysctlbyname(propName, result->chars + result->length, &neededLength, NULL, 0) != 0) {
+ return "sysctlbyname() failed to retrieve string data";
+ }
+
+ result->length += (uint32_t) neededLength - 1;
+
+ result->chars[result->length] = '\0';
+
+ return NULL;
+}
+
+int ffSysctlGetInt(const char* propName, int defaultValue) {
+ int result;
+ size_t neededLength = sizeof(result);
+ if (sysctlbyname(propName, &result, &neededLength, NULL, 0) != 0) {
+ return defaultValue;
+ }
+ return result;
+}
+
+int64_t ffSysctlGetInt64(const char* propName, int64_t defaultValue) {
+ int64_t result;
+ size_t neededLength = sizeof(result);
+ if (sysctlbyname(propName, &result, &neededLength, NULL, 0) != 0) {
+ return defaultValue;
+ }
+ return result;
+}
+#endif // OpenBSD
+
+void* ffSysctlGetData(int* request, u_int requestLength, size_t* resultLength) {
+ if (sysctl(request, requestLength, NULL, resultLength, NULL, 0) != 0) {
+ return NULL;
+ }
+
+ void* data = malloc(*resultLength);
+
+ if (sysctl(request, requestLength, data, resultLength, NULL, 0) != 0) {
+ free(data);
+ return NULL;
+ }
+
+ return data;
+}
diff --git a/src/common/impl/temps.c b/src/common/impl/temps.c
new file mode 100644
index 0000000..652f3fb
--- /dev/null
+++ b/src/common/impl/temps.c
@@ -0,0 +1,165 @@
+#include "fastfetch.h"
+#include "common/temps.h"
+#include "common/textModifier.h"
+#include "common/strutil.h"
+
+void ffTempsAppendNum(double celsius, FFstrbuf* buffer, FFColorRangeConfig config, const FFModuleArgs* module) {
+ if (celsius == -DBL_MAX) { // ignores invalid value
+ return;
+ }
+
+ const FFOptionsDisplay* options = &instance.config.display;
+ const char* colorGreen = options->tempColorGreen.chars;
+ const char* colorYellow = options->tempColorYellow.chars;
+ const char* colorRed = options->tempColorRed.chars;
+
+ uint8_t green = config.green, yellow = config.yellow;
+
+ if (!options->pipe) {
+ if (green <= yellow) {
+ if (celsius > yellow) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorRed);
+ } else if (celsius > green) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorYellow);
+ } else {
+ ffStrbufAppendF(buffer, "\e[%sm", colorGreen);
+ }
+ } else {
+ if (celsius < yellow) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorRed);
+ } else if (celsius < green) {
+ ffStrbufAppendF(buffer, "\e[%sm", colorYellow);
+ } else {
+ ffStrbufAppendF(buffer, "\e[%sm", colorGreen);
+ }
+ }
+ }
+
+ switch (options->tempUnit) {
+ case FF_TEMPERATURE_UNIT_DEFAULT:
+ case FF_TEMPERATURE_UNIT_CELSIUS:
+ ffStrbufAppendF(buffer, "%.*f%s°C", options->tempNdigits, celsius, options->tempSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : "");
+ break;
+ case FF_TEMPERATURE_UNIT_FAHRENHEIT:
+ ffStrbufAppendF(buffer, "%.*f%s°F", options->tempNdigits, celsius * 1.8 + 32, options->tempSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : "");
+ break;
+ case FF_TEMPERATURE_UNIT_KELVIN:
+ ffStrbufAppendF(buffer, "%.*f%sK", options->tempNdigits, celsius + 273.15, options->tempSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_NEVER ? "" : " ");
+ break;
+ }
+
+ if (!options->pipe) {
+ ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET);
+ if (module->outputColor.length) {
+ ffStrbufAppendF(buffer, "\e[%sm", module->outputColor.chars);
+ } else if (instance.config.display.colorOutput.length) {
+ ffStrbufAppendF(buffer, "\e[%sm", instance.config.display.colorOutput.chars);
+ }
+ }
+}
+
+bool ffTempsParseCommandOptions(const char* key, const char* subkey, const char* value, bool* useTemp, FFColorRangeConfig* config) {
+ if (!ffStrStartsWithIgnCase(subkey, "temp")) {
+ return false;
+ }
+
+ if (subkey[strlen("temp")] == '\0') {
+ *useTemp = ffOptionParseBoolean(value);
+ return true;
+ }
+
+ if (subkey[strlen("temp")] != '-') {
+ return false;
+ }
+
+ subkey += strlen("temp-");
+
+ if (ffStrEqualsIgnCase(subkey, "green")) {
+ uint32_t num = ffOptionParseUInt32(key, value);
+ if (num > 100) {
+ fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key);
+ exit(480);
+ }
+ config->green = (uint8_t) num;
+ return true;
+ }
+
+ if (ffStrEqualsIgnCase(subkey, "yellow")) {
+ uint32_t num = ffOptionParseUInt32(key, value);
+ if (num > 100) {
+ fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key);
+ exit(480);
+ }
+ config->yellow = (uint8_t) num;
+ return true;
+ }
+
+ return false;
+}
+
+bool ffTempsParseJsonObject(yyjson_val* key, yyjson_val* value, bool* useTemp, FFColorRangeConfig* config) {
+ assert(key);
+
+ if (!unsafe_yyjson_equals_str(key, "temp")) {
+ return false;
+ }
+
+ if (yyjson_is_bool(value)) {
+ *useTemp = yyjson_get_bool(value);
+ return true;
+ }
+
+ if (yyjson_is_null(value)) {
+ *useTemp = false;
+ return true;
+ }
+
+ if (!yyjson_is_obj(value)) {
+ fprintf(stderr, "Error: usage: %s must be an object or a boolean\n", unsafe_yyjson_get_str(key));
+ exit(480);
+ }
+
+ *useTemp = true;
+
+ yyjson_val* greenVal = yyjson_obj_get(value, "green");
+ if (greenVal) {
+ if (!yyjson_is_int(greenVal)) {
+ fputs("Error: usage: temp.green must be an integer between 0 and 100\n", stderr);
+ exit(480);
+ }
+
+ int num = unsafe_yyjson_get_int(greenVal);
+ if (num < 0 || num > 100) {
+ fputs("Error: usage: temp.green must be between 0 and 100\n", stderr);
+ exit(480);
+ }
+ config->green = (uint8_t) num;
+ }
+
+ yyjson_val* yellowVal = yyjson_obj_get(value, "yellow");
+ if (yellowVal) {
+ if (!yyjson_is_int(yellowVal)) {
+ fputs("Error: usage: temp.yellow must be an integer between 0 and 100\n", stderr);
+ exit(480);
+ }
+
+ int num = unsafe_yyjson_get_int(yellowVal);
+ if (num < 0 || num > 100) {
+ fputs("Error: usage: temp.yellow must be between 0 and 100\n", stderr);
+ exit(480);
+ }
+ config->yellow = (uint8_t) num;
+ }
+
+ return true;
+}
+
+void ffTempsGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, bool temp, FFColorRangeConfig config) {
+ if (!temp) {
+ yyjson_mut_obj_add_bool(doc, module, "temp", false);
+ } else {
+ yyjson_mut_val* temp = yyjson_mut_obj_add_obj(doc, module, "temp");
+ yyjson_mut_obj_add_uint(doc, temp, "green", config.green);
+ yyjson_mut_obj_add_uint(doc, temp, "yellow", config.yellow);
+ }
+}
diff --git a/src/common/impl/time.c b/src/common/impl/time.c
new file mode 100644
index 0000000..c60d1fe
--- /dev/null
+++ b/src/common/impl/time.c
@@ -0,0 +1,97 @@
+#include "common/time.h"
+
+#include <stdio.h>
+
+char ffTimeInternalBuffer[64]; // Reduce memory usage and prevent redundant allocations
+
+#ifdef _WIN32
+ #pragma GCC diagnostic push
+ #pragma GCC diagnostic ignored "-Wformat"
+#endif
+
+const char* ffTimeToFullStr(uint64_t msec) {
+ if (msec == 0) {
+ return "";
+ }
+ time_t tsec = (time_t) (msec / 1000);
+ const struct tm* tm = localtime(&tsec);
+
+ uint32_t len = 0;
+ len += (uint32_t) strftime(ffTimeInternalBuffer, ARRAY_SIZE(ffTimeInternalBuffer) - len, "%FT%T", tm);
+ len += (uint32_t) snprintf(ffTimeInternalBuffer + len, ARRAY_SIZE(ffTimeInternalBuffer) - len, ".%03u", (unsigned) (msec % 1000));
+ len += (uint32_t) strftime(ffTimeInternalBuffer + len, ARRAY_SIZE(ffTimeInternalBuffer) - len, "%z", tm);
+ return ffTimeInternalBuffer;
+}
+
+const char* ffTimeToShortStr(uint64_t msec) {
+ if (msec == 0) {
+ return "";
+ }
+ time_t tsec = (time_t) (msec / 1000);
+
+ strftime(ffTimeInternalBuffer, ARRAY_SIZE(ffTimeInternalBuffer), "%F %T", localtime(&tsec));
+ return ffTimeInternalBuffer;
+}
+
+const char* ffTimeToTimeStr(uint64_t msec) {
+ if (msec == 0) {
+ return "";
+ }
+ time_t tsec = (time_t) (msec / 1000);
+
+ uint32_t len = (uint32_t) strftime(ffTimeInternalBuffer, ARRAY_SIZE(ffTimeInternalBuffer), "%T", localtime(&tsec));
+ sprintf(ffTimeInternalBuffer + len, ".%03u", (unsigned) (msec % 1000));
+ return ffTimeInternalBuffer;
+}
+
+#ifdef _WIN32
+ #pragma GCC diagnostic pop
+#endif
+
+FFTimeGetAgeResult ffTimeGetAge(uint64_t birthMs, uint64_t nowMs) {
+ FFTimeGetAgeResult result = {};
+ if (__builtin_expect(birthMs == 0 || nowMs < birthMs, 0)) {
+ return result;
+ }
+
+ time_t birth_s = (time_t) (birthMs / 1000);
+ struct tm birth_tm;
+#ifdef _WIN32
+ localtime_s(&birth_tm, &birth_s);
+#else
+ localtime_r(&birth_s, &birth_tm);
+#endif
+
+ time_t now_s = (time_t) (nowMs / 1000);
+ struct tm now_tm;
+#ifdef _WIN32
+ localtime_s(&now_tm, &now_s);
+#else
+ localtime_r(&now_s, &now_tm);
+#endif
+
+ result.years = (uint32_t) (now_tm.tm_year - birth_tm.tm_year);
+ if (now_tm.tm_yday < birth_tm.tm_yday) {
+ result.years--;
+ }
+
+ birth_tm.tm_year += (int) result.years;
+ birth_s = mktime(&birth_tm);
+ uint32_t diff_s = (uint32_t) (now_s - birth_s);
+ result.daysOfYear = diff_s / (24 * 60 * 60);
+
+ birth_tm.tm_year += 1;
+ result.yearsFraction = (double) diff_s / (double) (mktime(&birth_tm) - birth_s) + result.years;
+
+ return result;
+}
+
+#ifdef _WIN32
+double ffQpcMultiplier;
+
+__attribute__((constructor)) static void ffTimeInitQpcMultiplier(void) {
+ LARGE_INTEGER frequency;
+ RtlQueryPerformanceFrequency(&frequency);
+ ffQpcMultiplier = 1000. / (double) frequency.QuadPart;
+}
+#endif
diff --git a/src/common/impl/wcwidth.c b/src/common/impl/wcwidth.c
new file mode 100644
index 0000000..4453510
--- /dev/null
+++ b/src/common/impl/wcwidth.c
@@ -0,0 +1,31 @@
+#include "common/wcwidth.h"
+#include "3rdparty/widecharwidth/widechar_width_c.h"
+
+int mk_wcwidth(uint32_t wc) {
+ // // We render U+1F6E1 (🛡) with a width of 2,
+ // // but widechar_width says it has a width of 1 because Unicode classifies it as "neutral".
+ // //
+ // // So we simply decide the width ourselves
+ // if (wc == 0x1F6E1) return 2;
+ //
+ // Well terminals do show it as width 1 after all
+
+ int width = widechar_wcwidth(wc);
+
+ switch (width) {
+ case widechar_ambiguous:
+ case widechar_private_use:
+ return 1;
+ case widechar_widened_in_9:
+ // Our renderer supports Unicode 9
+ return 2;
+ case widechar_nonprint:
+ case widechar_combining:
+ case widechar_unassigned:
+ case widechar_non_character:
+ return 0;
+ default:
+ // Use the width widechar_width gave us.
+ return width;
+ }
+}