summaryrefslogtreecommitdiffstats
path: root/src/common
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/common
Add the files
Diffstat (limited to 'src/common')
-rw-r--r--src/common/FFPlatform.h37
-rw-r--r--src/common/FFlist.h141
-rw-r--r--src/common/FFstrbuf.h584
-rw-r--r--src/common/apple/Info.plist.in16
-rw-r--r--src/common/apple/cf_helpers.c225
-rw-r--r--src/common/apple/cf_helpers.h43
-rw-r--r--src/common/apple/osascript.h5
-rw-r--r--src/common/apple/osascript.m15
-rw-r--r--src/common/apple/smc_temps.c465
-rw-r--r--src/common/apple/smc_temps.h32
-rw-r--r--src/common/apple/version.h5
-rw-r--r--src/common/apple/version.m43
-rw-r--r--src/common/argType.h44
-rw-r--r--src/common/arrutil.h16
-rw-r--r--src/common/attributes.h19
-rw-r--r--src/common/base64.h21
-rw-r--r--src/common/binary.h18
-rw-r--r--src/common/color.h55
-rw-r--r--src/common/commandoption.h8
-rw-r--r--src/common/dbus.h46
-rw-r--r--src/common/debug.h37
-rw-r--r--src/common/duration.h5
-rw-r--r--src/common/edidHelper.h13
-rw-r--r--src/common/ffdata.h22
-rw-r--r--src/common/font.h25
-rw-r--r--src/common/format.h14
-rw-r--r--src/common/frequency.h5
-rw-r--r--src/common/haiku/version.cpp26
-rw-r--r--src/common/haiku/version.h3
-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
-rw-r--r--src/common/init.h7
-rw-r--r--src/common/io.h300
-rw-r--r--src/common/jsonconfig.h27
-rw-r--r--src/common/kmod.h5
-rw-r--r--src/common/library.h109
-rw-r--r--src/common/lua.h187
-rw-r--r--src/common/mallocHelper.h38
-rw-r--r--src/common/memrchr.h15
-rw-r--r--src/common/netif.h30
-rw-r--r--src/common/networking.h38
-rw-r--r--src/common/option.h96
-rw-r--r--src/common/parsing.h24
-rw-r--r--src/common/path.h21
-rw-r--r--src/common/percent.h43
-rw-r--r--src/common/printing.h21
-rw-r--r--src/common/processing.h55
-rw-r--r--src/common/properties.h47
-rw-r--r--src/common/settings.h47
-rw-r--r--src/common/size.h5
-rw-r--r--src/common/smbios.h103
-rw-r--r--src/common/strutil.h115
-rw-r--r--src/common/sysctl.h17
-rw-r--r--src/common/temps.h9
-rw-r--r--src/common/textModifier.h5
-rw-r--r--src/common/thread.h115
-rw-r--r--src/common/time.h80
-rw-r--r--src/common/unused.h6
-rw-r--r--src/common/wcwidth.h12
-rwxr-xr-xsrc/common/windows/c-logo.sh10
-rw-r--r--src/common/windows/com.c67
-rw-r--r--src/common/windows/com.h23
-rw-r--r--src/common/windows/getline.c58
-rw-r--r--src/common/windows/getline.h6
-rw-r--r--src/common/windows/logo.icobin0 -> 107906 bytes
l---------src/common/windows/logo.svg1
-rw-r--r--src/common/windows/manifest.xml18
-rw-r--r--src/common/windows/nt.h1325
-rw-r--r--src/common/windows/perflib_.h134
-rw-r--r--src/common/windows/registry.c388
-rw-r--r--src/common/windows/registry.h66
-rw-r--r--src/common/windows/unicode.c44
-rw-r--r--src/common/windows/unicode.h46
-rw-r--r--src/common/windows/unicode.hpp27
-rw-r--r--src/common/windows/util.hpp21
-rw-r--r--src/common/windows/variant.cpp20
-rw-r--r--src/common/windows/variant.hpp164
-rw-r--r--src/common/windows/version.c77
-rw-r--r--src/common/windows/version.h14
-rw-r--r--src/common/windows/version.rc37
-rw-r--r--src/common/windows/wmi.h54
137 files changed, 18935 insertions, 0 deletions
diff --git a/src/common/FFPlatform.h b/src/common/FFPlatform.h
new file mode 100644
index 0000000..cf8224d
--- /dev/null
+++ b/src/common/FFPlatform.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+#include "common/FFlist.h"
+
+typedef struct FFPlatformSysinfo {
+ FFstrbuf name;
+ FFstrbuf release;
+ FFstrbuf version;
+ FFstrbuf architecture;
+ uint32_t pageSize;
+} FFPlatformSysinfo;
+
+typedef struct FFPlatform {
+ FFstrbuf homeDir; // Trailing slash included
+ FFstrbuf cacheDir; // Trailing slash included
+ FFlist configDirs; // List of FFstrbuf, trailing slash included
+ FFlist dataDirs; // List of FFstrbuf, trailing slash included
+ FFstrbuf exePath; // The real path of current exe (empty if unavailable)
+ FFstrbuf cwd; // Trailing slash included
+
+ uint32_t pid;
+#ifndef _WIN32
+ uint32_t uid;
+#else
+ FFstrbuf sid;
+#endif
+ FFstrbuf userName;
+ FFstrbuf fullUserName;
+ FFstrbuf hostName;
+ FFstrbuf userShell;
+
+ FFPlatformSysinfo sysinfo;
+} FFPlatform;
+
+void ffPlatformInit(FFPlatform* platform);
+void ffPlatformDestroy(FFPlatform* platform);
diff --git a/src/common/FFlist.h b/src/common/FFlist.h
new file mode 100644
index 0000000..80cef0b
--- /dev/null
+++ b/src/common/FFlist.h
@@ -0,0 +1,141 @@
+#pragma once
+
+#include "common/attributes.h"
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <assert.h>
+#include <stdlib.h>
+
+#define FF_LIST_DEFAULT_ALLOC 16
+
+typedef struct FFlist {
+ uint8_t* data;
+ uint32_t length;
+ uint32_t capacity;
+} FFlist;
+
+void* ffListAdd(FFlist* list, uint32_t elementSize);
+
+// Removes the first element, and copy its value to `*result`
+bool ffListShift(FFlist* list, uint32_t elementSize, void* __restrict result);
+// Removes the last element, and copy its value to `*result`
+bool ffListPop(FFlist* list, uint32_t elementSize, void* __restrict result);
+
+static inline void ffListInit(FFlist* list) {
+ list->capacity = 0;
+ list->length = 0;
+ list->data = NULL;
+}
+
+static inline void ffListInitA(FFlist* list, uint32_t elementSize, uint32_t capacity) {
+ ffListInit(list);
+ list->capacity = capacity;
+ list->data = __builtin_expect(capacity == 0, 0) ? NULL : (uint8_t*) malloc((size_t) capacity * elementSize);
+}
+
+FF_A_NODISCARD static inline FFlist ffListCreate() {
+ FFlist result;
+ ffListInit(&result);
+ return result;
+}
+
+FF_A_NODISCARD static inline FFlist ffListCreateA(uint32_t elementSize, uint32_t capacity) {
+ FFlist result;
+ ffListInitA(&result, elementSize, capacity);
+ return result;
+}
+
+FF_A_NODISCARD static inline void* ffListGet(const FFlist* list, uint32_t elementSize, uint32_t index) {
+ assert(list->capacity > index);
+ return list->data + (index * elementSize);
+}
+
+FF_A_NODISCARD static inline uint32_t ffListFirstIndexComp(const FFlist* list, uint32_t elementSize, void* compElement, bool (*compFunc)(const void*, const void*)) {
+ for (uint32_t i = 0; i < list->length; i++) {
+ if (compFunc(ffListGet(list, elementSize, i), compElement)) {
+ return i;
+ }
+ }
+
+ return list->length;
+}
+
+FF_A_NODISCARD static inline bool ffListContains(const FFlist* list, uint32_t elementSize, void* compElement, bool (*compFunc)(const void*, const void*)) {
+ return ffListFirstIndexComp(list, elementSize, compElement, compFunc) != list->length;
+}
+
+static inline void ffListSort(FFlist* list, uint32_t elementSize, int (*compar)(const void*, const void*)) {
+ qsort(list->data, list->length, elementSize, compar);
+}
+
+// Move the contents of `src` into `list`, and left `src` empty
+static inline void ffListInitMove(FFlist* list, FFlist* src) {
+ if (src) {
+ list->capacity = src->capacity;
+ list->length = src->length;
+ list->data = src->data;
+ ffListInit(src);
+ } else {
+ ffListInit(list);
+ }
+}
+
+static inline void ffListDestroy(FFlist* list) {
+ if (!list->data) {
+ return;
+ }
+
+ // Avoid free-after-use. These 3 assignments are cheap so don't remove them
+ list->capacity = list->length = 0;
+ free(list->data);
+ list->data = NULL;
+}
+
+static inline void ffListClear(FFlist* list) {
+ list->length = 0;
+}
+
+static inline void ffListReserve(FFlist* list, uint32_t elementSize, uint32_t newCapacity) {
+ if (__builtin_expect(newCapacity <= list->capacity, false)) {
+ return;
+ }
+
+ list->data = (uint8_t*) realloc(list->data, (size_t) newCapacity * elementSize);
+ list->capacity = newCapacity;
+}
+
+#define FF_LIST_FOR_EACH(itemType, itemVarName, listVar) \
+ for (itemType* itemVarName = (itemType*) (listVar).data; \
+ itemVarName - (itemType*) (listVar).data < (intptr_t) (listVar).length; \
+ ++itemVarName)
+
+#define FF_LIST_AUTO_DESTROY FFlist FF_A_CLEANUP(ffListDestroy)
+
+#define FF_LIST_GET(itemType, listVar, index) \
+ ({ \
+ assert((listVar).capacity > (index)); \
+ (itemType*) (listVar).data + (index); \
+ })
+
+#define FF_LIST_ADD(itemType, listVar) (itemType*) ffListAdd(&(listVar), (uint32_t) sizeof(itemType))
+
+#define FF_LIST_FIRST(itemType, listVar) FF_LIST_GET(itemType, listVar, 0)
+#define FF_LIST_LAST(itemType, listVar) \
+ ({ \
+ assert((listVar).length > 0); \
+ FF_LIST_GET(itemType, listVar, ((listVar).length - 1)); \
+ })
+
+#define FF_LIST_CONTAINS(listVar, pCompElement, compFunc) \
+ ({ \
+ typedef __typeof__(*(pCompElement)) compElementType; \
+ typedef bool compFuncType(const compElementType*, const compElementType*); \
+ static_assert(__builtin_types_compatible_p(__typeof__(compFunc), compFuncType), "Incompatible callback function"); \
+ ffListContains(&(listVar), (uint32_t) sizeof(*(pCompElement)), (pCompElement), (bool (*)(const void*, const void*)) compFunc); \
+ })
+
+#define FF_LIST_SHIFT(listVar, pResult) \
+ ffListShift(&(listVar), (uint32_t) sizeof(*(pResult)), (pResult))
+#define FF_LIST_POP(listVar, pResult) \
+ ffListPop(&(listVar), (uint32_t) sizeof(*(pResult)), (pResult))
diff --git a/src/common/FFstrbuf.h b/src/common/FFstrbuf.h
new file mode 100644
index 0000000..2279152
--- /dev/null
+++ b/src/common/FFstrbuf.h
@@ -0,0 +1,584 @@
+#pragma once
+
+#include "common/attributes.h"
+
+#include <stdint.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <assert.h>
+#include "common/memrchr.h"
+
+#ifdef FF_USE_SYSTEM_YYJSON
+ #include <yyjson.h>
+#else
+ #include "3rdparty/yyjson/yyjson.h"
+#endif
+
+#ifdef _WIN32
+// #include <shlwapi.h>
+__stdcall char* StrStrIA(const char* lpFirst, const char* lpSrch);
+ #define strcasestr StrStrIA
+#endif
+
+#define FASTFETCH_STRBUF_DEFAULT_ALLOC 32
+
+// static string (allocated == 0), chars points to a string literal
+// dynamic string (allocated > 0), chars points to a heap allocated buffer
+typedef struct FFstrbuf {
+ uint32_t allocated;
+ uint32_t length;
+ char* chars;
+} FFstrbuf;
+
+static inline void ffStrbufInit(FFstrbuf* strbuf);
+void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate);
+void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments);
+void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr);
+
+void ffStrbufEnsureFree(FFstrbuf* strbuf, uint32_t free);
+void ffStrbufEnsureFixedLengthFree(FFstrbuf* strbuf, uint32_t free);
+
+void ffStrbufClear(FFstrbuf* strbuf);
+
+static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict value);
+void ffStrbufAppendC(FFstrbuf* strbuf, char c);
+void ffStrbufAppendNC(FFstrbuf* strbuf, uint32_t num, char c);
+void ffStrbufAppendNS(FFstrbuf* strbuf, uint32_t length, const char* value);
+void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transformFunc)(int));
+FF_A_PRINTF(2, 3) void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...);
+void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments);
+const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char until);
+
+void ffStrbufPrependNS(FFstrbuf* strbuf, uint32_t length, const char* value);
+void ffStrbufPrependC(FFstrbuf* strbuf, char c);
+
+void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c);
+
+// Clear the content of strbuf and set new value
+// NOTE: Unlike ffStrbufAppend*, ffStrbufSet* functions may NOT reserve extra space
+void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value);
+void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value);
+FF_A_PRINTF(2, 3) void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...);
+
+void ffStrbufTrimLeft(FFstrbuf* strbuf, char c);
+void ffStrbufTrimRight(FFstrbuf* strbuf, char c);
+void ffStrbufTrimLeftSpace(FFstrbuf* strbuf);
+void ffStrbufTrimRightSpace(FFstrbuf* strbuf);
+
+bool ffStrbufRemoveSubstr(FFstrbuf* strbuf, uint32_t startIndex, uint32_t endIndex);
+void ffStrbufRemoveS(FFstrbuf* strbuf, const char* str);
+void ffStrbufRemoveStrings(FFstrbuf* strbuf, uint32_t numStrings, const char* strings[]);
+
+void ffStrbufReplaceAllC(FFstrbuf* strbuf, char find, char replace);
+
+// Returns true if the strbuf is modified
+bool ffStrbufSubstrBefore(FFstrbuf* strbuf, uint32_t index);
+bool ffStrbufSubstrAfter(FFstrbuf* strbuf, uint32_t index); // Not including the index
+bool ffStrbufSubstrAfterFirstC(FFstrbuf* strbuf, char c);
+bool ffStrbufSubstrAfterFirstS(FFstrbuf* strbuf, const char* str);
+bool ffStrbufSubstrAfterLastC(FFstrbuf* strbuf, char c);
+bool ffStrbufSubstr(FFstrbuf* strbuf, uint32_t start, uint32_t end);
+
+FF_A_NODISCARD uint32_t ffStrbufCountC(const FFstrbuf* strbuf, char c);
+
+bool ffStrbufRemoveIgnCaseEndS(FFstrbuf* strbuf, const char* end);
+
+bool ffStrbufEnsureEndsWithC(FFstrbuf* strbuf, char c);
+
+void ffStrbufWriteTo(const FFstrbuf* strbuf, FILE* file);
+void ffStrbufPutTo(const FFstrbuf* strbuf, FILE* file);
+
+FF_A_NODISCARD double ffStrbufToDouble(const FFstrbuf* strbuf, double defaultValue);
+FF_A_NODISCARD int64_t ffStrbufToSInt(const FFstrbuf* strbuf, int64_t defaultValue);
+FF_A_NODISCARD uint64_t ffStrbufToUInt(const FFstrbuf* strbuf, uint64_t defaultValue);
+
+void ffStrbufUpperCase(FFstrbuf* strbuf);
+void ffStrbufLowerCase(FFstrbuf* strbuf);
+
+// Function alters the buffer to extract lines or delimited segments (replaces the delimiter with '\0')
+// so that buffer MUST be heap allocated (NOT a static string)
+// `lineptr` must be `NULL` and `n` MUST be `0` for the first call
+// Caller MUST NOT free `*lineptr`
+bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer);
+void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer);
+
+/**
+ * @brief Read a line from a FFstrbuf.
+ *
+ * @details Behaves like getline(3) but reads from a FFstrbuf.
+ *
+ * @param[in,out] lineptr The pointer to a pointer that will be set to the start of the line
+ (points to buffer's internal memory address to avoid memory allocation and copy).
+ MUST NOT be freed by the caller, unlike `getline(3)`.
+ * MUST be NULL for the first call.
+ * @param[in,out] n The pointer to the size of the buffer of lineptr.
+ MUST be 0 for the first call.
+ * @param[in] buffer The buffer to read from.
+ MUST be heap allocated (NOT a static string).
+ *
+ * @return true if a line has been read, false if the end of the buffer has been reached.
+ */
+static inline bool ffStrbufGetline(char** lineptr, size_t* n, FFstrbuf* buffer) {
+ return ffStrbufGetdelim(lineptr, n, '\n', buffer);
+}
+/**
+ * @brief Restore the end of a line that was modified by ffStrbufGetline.
+ * @warning This function should be called before breaking an ffStrbufGetline loop if `buffer` will be used later.
+ */
+static inline void ffStrbufGetlineRestore(char** lineptr, size_t* n, FFstrbuf* buffer) {
+ ffStrbufGetdelimRestore(lineptr, n, '\n', buffer);
+}
+bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf);
+bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
+bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
+bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
+bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator);
+
+int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint);
+
+void ffStrbufAppendSInt(FFstrbuf* strbuf, int64_t value);
+void ffStrbufAppendUInt(FFstrbuf* strbuf, uint64_t value);
+// Appends a double value to the string buffer with the specified precision (0~15).
+// if `precision < 0`, let yyjson decide the precision
+void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool trailingZeros);
+
+FF_A_NODISCARD static inline FFstrbuf ffStrbufCreateA(uint32_t allocate) {
+ FFstrbuf strbuf;
+ ffStrbufInitA(&strbuf, allocate);
+ return strbuf;
+}
+
+static inline void ffStrbufInitCopy(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict src) {
+ if (src->allocated == 0) { // static string
+ *strbuf = *src;
+ } else {
+ ffStrbufInitA(strbuf, src->allocated);
+ ffStrbufAppend(strbuf, src);
+ }
+}
+
+FF_A_NODISCARD static inline FFstrbuf ffStrbufCreateCopy(const FFstrbuf* src) {
+ FFstrbuf strbuf;
+ ffStrbufInitCopy(&strbuf, src);
+ return strbuf;
+}
+
+// Move the content of `src` into `strbuf`, and left `src` empty
+static inline void ffStrbufInitMove(FFstrbuf* strbuf, FFstrbuf* src) {
+ if (src) {
+ *strbuf = *src;
+ ffStrbufInit(src);
+ } else {
+ ffStrbufInit(strbuf);
+ }
+}
+
+FF_A_NODISCARD static inline FFstrbuf ffStrbufCreateMove(FFstrbuf* src) {
+ FFstrbuf strbuf;
+ ffStrbufInitMove(&strbuf, src);
+ return strbuf;
+}
+
+FF_A_NODISCARD static inline FFstrbuf ffStrbufCreateVF(const char* format, va_list arguments) {
+ FFstrbuf strbuf;
+ ffStrbufInitVF(&strbuf, format, arguments);
+ return strbuf;
+}
+
+FF_A_PRINTF(2, 3) static inline void ffStrbufInitF(FFstrbuf* strbuf, const char* format, ...) {
+ va_list arguments;
+ va_start(arguments, format);
+ ffStrbufInitVF(strbuf, format, arguments);
+ va_end(arguments);
+}
+
+FF_A_PRINTF(1, 2) FF_A_NODISCARD static inline FFstrbuf
+ffStrbufCreateF(const char* format, ...) {
+ FFstrbuf strbuf;
+
+ va_list arguments;
+ va_start(arguments, format);
+ ffStrbufInitVF(&strbuf, format, arguments);
+ va_end(arguments);
+
+ return strbuf;
+}
+
+static inline void ffStrbufInitMoveS(FFstrbuf* strbuf, char* heapStr) {
+ ffStrbufInitMoveNS(strbuf, (uint32_t) strlen(heapStr), heapStr);
+}
+
+// Despite the name, this function resets strbuf to the initial/unallocated state
+static inline void ffStrbufDestroy(FFstrbuf* strbuf) {
+ if (strbuf->allocated > 0) {
+ free(strbuf->chars);
+ }
+
+ ffStrbufInit(strbuf);
+}
+
+FF_A_NODISCARD static inline uint32_t ffStrbufGetFree(const FFstrbuf* strbuf) {
+ assert(strbuf != NULL);
+ if (strbuf->allocated == 0) {
+ return 0;
+ }
+
+ return strbuf->allocated - strbuf->length - 1; // - 1 for the null byte
+}
+
+static inline void ffStrbufRecalculateLength(FFstrbuf* strbuf) {
+ strbuf->length = (uint32_t) strlen(strbuf->chars);
+}
+
+static inline void ffStrbufSetS(FFstrbuf* strbuf, const char* value) {
+ assert(strbuf != NULL);
+
+ if (value == NULL) {
+ ffStrbufClear(strbuf);
+ } else {
+ ffStrbufSetNS(strbuf, (uint32_t) strlen(value), value);
+ }
+}
+
+static inline bool ffStrbufSetJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
+ assert(strbuf != NULL);
+
+ if (yyjson_is_str(jsonVal)) {
+ ffStrbufSetNS(strbuf, (uint32_t) unsafe_yyjson_get_len(jsonVal), unsafe_yyjson_get_str(jsonVal));
+ return true;
+ }
+
+ ffStrbufClear(strbuf);
+ return false;
+}
+
+static inline void ffStrbufAppendS(FFstrbuf* strbuf, const char* value) {
+ if (value == NULL) {
+ return;
+ }
+ ffStrbufAppendNS(strbuf, (uint32_t) strlen(value), value);
+}
+
+static inline bool ffStrbufAppendJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
+ if (yyjson_is_str(jsonVal)) {
+ ffStrbufAppendNS(strbuf, (uint32_t) unsafe_yyjson_get_len(jsonVal), unsafe_yyjson_get_str(jsonVal));
+ return true;
+ }
+ return false;
+}
+
+static inline void ffStrbufInit(FFstrbuf* strbuf) {
+ extern char* CHAR_NULL_PTR;
+ strbuf->allocated = strbuf->length = 0;
+ strbuf->chars = CHAR_NULL_PTR;
+}
+
+FF_A_NODISCARD static inline FFstrbuf ffStrbufCreate(void) {
+ FFstrbuf strbuf;
+ ffStrbufInit(&strbuf);
+ return strbuf;
+}
+
+static inline void ffStrbufInitStatic(FFstrbuf* strbuf, const char* str) {
+ ffStrbufInit(strbuf);
+ if (!str) {
+ return;
+ }
+
+ strbuf->allocated = 0;
+ strbuf->length = (uint32_t) strlen(str);
+ strbuf->chars = (char*) str;
+}
+
+FF_A_NODISCARD static inline FFstrbuf ffStrbufCreateStatic(const char* str) {
+ FFstrbuf strbuf;
+ ffStrbufInitStatic(&strbuf, str);
+ return strbuf;
+}
+
+static inline void ffStrbufSetStatic(FFstrbuf* strbuf, const char* value) {
+ if (strbuf->allocated > 0) {
+ free(strbuf->chars);
+ }
+
+ if (value != NULL) {
+ ffStrbufInitStatic(strbuf, value);
+ } else {
+ ffStrbufInit(strbuf);
+ }
+}
+
+static inline void ffStrbufInitNS(FFstrbuf* strbuf, uint32_t length, const char* str) {
+ ffStrbufInit(strbuf);
+ ffStrbufAppendNS(strbuf, length, str);
+}
+
+FF_A_NODISCARD static inline FFstrbuf ffStrbufCreateNS(uint32_t length, const char* str) {
+ FFstrbuf strbuf;
+ ffStrbufInitNS(&strbuf, length, str);
+ return strbuf;
+}
+
+static inline bool ffStrbufInitJsonVal(FFstrbuf* strbuf, yyjson_val* jsonVal) {
+ ffStrbufInit(strbuf);
+ return ffStrbufAppendJsonVal(strbuf, jsonVal);
+}
+
+static inline void ffStrbufInitS(FFstrbuf* strbuf, const char* str) {
+ ffStrbufInit(strbuf);
+ ffStrbufAppendS(strbuf, str);
+}
+
+FF_A_NODISCARD static inline FFstrbuf ffStrbufCreateS(const char* str) {
+ FFstrbuf strbuf;
+ ffStrbufInitS(&strbuf, str);
+ return strbuf;
+}
+
+static inline void ffStrbufAppend(FFstrbuf* __restrict strbuf, const FFstrbuf* __restrict value) {
+ assert(value != strbuf);
+ if (value == NULL) {
+ return;
+ }
+ ffStrbufAppendNS(strbuf, value->length, value->chars);
+}
+
+static inline void ffStrbufPrepend(FFstrbuf* strbuf, FFstrbuf* value) {
+ if (value == NULL) {
+ return;
+ }
+ ffStrbufPrependNS(strbuf, value->length, value->chars);
+}
+
+static inline void ffStrbufPrependS(FFstrbuf* strbuf, const char* value) {
+ if (value == NULL) {
+ return;
+ }
+ ffStrbufPrependNS(strbuf, (uint32_t) strlen(value), value);
+}
+
+static inline FF_A_NODISCARD int ffStrbufComp(const FFstrbuf* strbuf, const FFstrbuf* comp) {
+ uint32_t length = strbuf->length > comp->length ? comp->length : strbuf->length;
+ return memcmp(strbuf->chars, comp->chars, length + 1);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEqual(const FFstrbuf* strbuf, const FFstrbuf* comp) {
+ return ffStrbufComp(strbuf, comp) == 0;
+}
+
+static inline FF_A_NODISCARD int ffStrbufCompS(const FFstrbuf* strbuf, const char* comp) {
+ return strcmp(strbuf->chars, comp);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEqualS(const FFstrbuf* strbuf, const char* comp) {
+ return ffStrbufCompS(strbuf, comp) == 0;
+}
+
+static inline FF_A_NODISCARD int ffStrbufIgnCaseCompS(const FFstrbuf* strbuf, const char* comp) {
+ return strcasecmp(strbuf->chars, comp);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufIgnCaseEqualS(const FFstrbuf* strbuf, const char* comp) {
+ return ffStrbufIgnCaseCompS(strbuf, comp) == 0;
+}
+
+static inline FF_A_NODISCARD int ffStrbufIgnCaseComp(const FFstrbuf* strbuf, const FFstrbuf* comp) {
+ return ffStrbufIgnCaseCompS(strbuf, comp->chars);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufIgnCaseEqual(const FFstrbuf* strbuf, const FFstrbuf* comp) {
+ return ffStrbufIgnCaseComp(strbuf, comp) == 0;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufContainC(const FFstrbuf* strbuf, char c) {
+ return memchr(strbuf->chars, c, strbuf->length) != NULL;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufContainS(const FFstrbuf* strbuf, const char* str) {
+ return strstr(strbuf->chars, str) != NULL;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufContain(const FFstrbuf* strbuf, const FFstrbuf* str) {
+ return ffStrbufContainS(strbuf, str->chars);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufContainIgnCaseS(const FFstrbuf* strbuf, const char* str) {
+ return strcasestr(strbuf->chars, str) != NULL;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufContainIgnCase(const FFstrbuf* strbuf, const FFstrbuf* str) {
+ return ffStrbufContainIgnCaseS(strbuf, str->chars);
+}
+
+FF_A_NODISCARD static inline uint32_t ffStrbufNextIndexC(const FFstrbuf* strbuf, uint32_t start, char c) {
+ assert(start <= strbuf->length);
+
+ const char* ptr = (const char*) memchr(strbuf->chars + start, c, strbuf->length - start);
+ return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
+}
+
+FF_A_NODISCARD static inline uint32_t ffStrbufNextIndexS(const FFstrbuf* strbuf, uint32_t start, const char* str) {
+ assert(start <= strbuf->length);
+
+ const char* ptr = strstr(strbuf->chars + start, str);
+ return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
+}
+
+FF_A_NODISCARD static inline uint32_t ffStrbufPreviousIndexC(const FFstrbuf* strbuf, uint32_t start, char c) {
+ assert(start <= strbuf->length);
+
+ const char* ptr = (const char*) memrchr(strbuf->chars, c, start + 1);
+ return ptr ? (uint32_t) (ptr - strbuf->chars) : strbuf->length;
+}
+
+static inline FF_A_NODISCARD uint32_t ffStrbufFirstIndexC(const FFstrbuf* strbuf, char c) {
+ return ffStrbufNextIndexC(strbuf, 0, c);
+}
+
+static inline FF_A_NODISCARD uint32_t ffStrbufFirstIndex(const FFstrbuf* strbuf, const FFstrbuf* searched) {
+ return ffStrbufNextIndexS(strbuf, 0, searched->chars);
+}
+
+static inline FF_A_NODISCARD uint32_t ffStrbufFirstIndexS(const FFstrbuf* strbuf, const char* str) {
+ return ffStrbufNextIndexS(strbuf, 0, str);
+}
+
+static inline FF_A_NODISCARD uint32_t ffStrbufLastIndexC(const FFstrbuf* strbuf, char c) {
+ if (strbuf->length == 0) {
+ return 0;
+ }
+
+ return ffStrbufPreviousIndexC(strbuf, strbuf->length - 1, c);
+}
+
+static inline bool ffStrbufSubstrBeforeFirstC(FFstrbuf* strbuf, char c) {
+ return ffStrbufSubstrBefore(strbuf, ffStrbufFirstIndexC(strbuf, c));
+}
+
+static inline bool ffStrbufSubstrBeforeLastC(FFstrbuf* strbuf, char c) {
+ return ffStrbufSubstrBefore(strbuf, ffStrbufLastIndexC(strbuf, c));
+}
+
+static inline FF_A_NODISCARD bool ffStrbufStartsWithC(const FFstrbuf* strbuf, char c) {
+ return strbuf->chars[0] == c;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufStartsWithSN(const FFstrbuf* strbuf, const char* start, uint32_t length) {
+ if (length > strbuf->length) {
+ return false;
+ }
+
+ return memcmp(strbuf->chars, start, length) == 0;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufStartsWithS(const FFstrbuf* strbuf, const char* start) {
+ return ffStrbufStartsWithSN(strbuf, start, (uint32_t) strlen(start));
+}
+
+static inline FF_A_NODISCARD bool ffStrbufStartsWith(const FFstrbuf* strbuf, const FFstrbuf* start) {
+ return ffStrbufStartsWithSN(strbuf, start->chars, start->length);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufStartsWithIgnCaseNS(const FFstrbuf* strbuf, uint32_t length, const char* start) {
+ if (length > strbuf->length) {
+ return false;
+ }
+ return strncasecmp(strbuf->chars, start, length) == 0;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufStartsWithIgnCaseS(const FFstrbuf* strbuf, const char* start) {
+ return ffStrbufStartsWithIgnCaseNS(strbuf, (uint32_t) strlen(start), start);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufStartsWithIgnCase(const FFstrbuf* strbuf, const FFstrbuf* start) {
+ return ffStrbufStartsWithIgnCaseNS(strbuf, start->length, start->chars);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEndsWithC(const FFstrbuf* strbuf, char c) {
+ return strbuf->length == 0 ? false : strbuf->chars[strbuf->length - 1] == c;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEndsWithNS(const FFstrbuf* strbuf, uint32_t endLength, const char* end) {
+ if (endLength > strbuf->length) {
+ return false;
+ }
+
+ return memcmp(strbuf->chars + strbuf->length - endLength, end, endLength) == 0;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEndsWithS(const FFstrbuf* strbuf, const char* end) {
+ return ffStrbufEndsWithNS(strbuf, (uint32_t) strlen(end), end);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEndsWithFn(const FFstrbuf* strbuf, int (*const fn)(int)) {
+ return strbuf->length == 0 ? false : fn(strbuf->chars[strbuf->length - 1]);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEndsWith(const FFstrbuf* strbuf, const FFstrbuf* end) {
+ return ffStrbufEndsWithNS(strbuf, end->length, end->chars);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEndsWithIgnCaseNS(const FFstrbuf* strbuf, uint32_t endLength, const char* end) {
+ if (endLength > strbuf->length) {
+ return false;
+ }
+ return strcasecmp(strbuf->chars + strbuf->length - endLength, end) == 0;
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEndsWithIgnCaseS(const FFstrbuf* strbuf, const char* end) {
+ return ffStrbufEndsWithIgnCaseNS(strbuf, (uint32_t) strlen(end), end);
+}
+
+static inline FF_A_NODISCARD bool ffStrbufEndsWithIgnCase(const FFstrbuf* strbuf, const FFstrbuf* end) {
+ return ffStrbufEndsWithIgnCaseNS(strbuf, end->length, end->chars);
+}
+
+static inline void ffStrbufTrim(FFstrbuf* strbuf, char c) {
+ ffStrbufTrimRight(strbuf, c);
+ ffStrbufTrimLeft(strbuf, c);
+}
+
+static inline void ffStrbufTrimSpace(FFstrbuf* strbuf) {
+ ffStrbufTrimRightSpace(strbuf);
+ ffStrbufTrimLeftSpace(strbuf);
+}
+
+static inline bool ffStrbufMatchSeparatedS(const FFstrbuf* strbuf, const char* comp, char separator) {
+ return ffStrbufMatchSeparatedNS(strbuf, (uint32_t) strlen(comp), comp, separator);
+}
+
+static inline bool ffStrbufMatchSeparated(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
+ return ffStrbufMatchSeparatedNS(strbuf, comp->length, comp->chars, separator);
+}
+
+static inline bool ffStrbufMatchSeparatedIgnCaseS(const FFstrbuf* strbuf, const char* comp, char separator) {
+ return ffStrbufMatchSeparatedIgnCaseNS(strbuf, (uint32_t) strlen(comp), comp, separator);
+}
+
+static inline bool ffStrbufMatchSeparatedIgnCase(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
+ return ffStrbufMatchSeparatedIgnCaseNS(strbuf, comp->length, comp->chars, separator);
+}
+
+static inline bool ffStrbufSeparatedContainS(const FFstrbuf* strbuf, const char* comp, char separator) {
+ return ffStrbufSeparatedContainNS(strbuf, (uint32_t) strlen(comp), comp, separator);
+}
+
+static inline bool ffStrbufSeparatedContain(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
+ return ffStrbufSeparatedContainNS(strbuf, comp->length, comp->chars, separator);
+}
+
+static inline bool ffStrbufSeparatedContainIgnCaseS(const FFstrbuf* strbuf, const char* comp, char separator) {
+ return ffStrbufSeparatedContainIgnCaseNS(strbuf, (uint32_t) strlen(comp), comp, separator);
+}
+
+static inline bool ffStrbufSeparatedContainIgnCase(const FFstrbuf* strbuf, const FFstrbuf* comp, char separator) {
+ return ffStrbufSeparatedContainIgnCaseNS(strbuf, comp->length, comp->chars, separator);
+}
+
+// Returns true if the strbuf is modified
+bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf);
+
+#define FF_STRBUF_AUTO_DESTROY FFstrbuf FF_A_CLEANUP(ffStrbufDestroy)
diff --git a/src/common/apple/Info.plist.in b/src/common/apple/Info.plist.in
new file mode 100644
index 0000000..5220053
--- /dev/null
+++ b/src/common/apple/Info.plist.in
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>CFBundleIdentifier</key>
+ <string>fastfetch</string>
+ <key>CFBundleName</key>
+ <string>@PROJECT_NAME@</string>
+ <key>CFBundleShortVersionString</key>
+ <string>@PROJECT_VERSION@</string>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>NSBluetoothAlwaysUsageDescription</key>
+ <string>For detecting Bluetooth devices</string>
+ </dict>
+</plist>
diff --git a/src/common/apple/cf_helpers.c b/src/common/apple/cf_helpers.c
new file mode 100644
index 0000000..60e1a1c
--- /dev/null
+++ b/src/common/apple/cf_helpers.c
@@ -0,0 +1,225 @@
+#include "cf_helpers.h"
+
+const char* ffCfNumGetInt64(CFTypeRef cf, int64_t* result) {
+ if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
+ if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberSInt64Type, result)) {
+ return "Number type is not SInt64";
+ }
+ return NULL;
+ } else if (CFGetTypeID(cf) == CFDataGetTypeID()) {
+ if (CFDataGetLength((CFDataRef) cf) != sizeof(int64_t)) {
+ return "Data length is not sizeof(int64_t)";
+ }
+ CFDataGetBytes((CFDataRef) cf, CFRangeMake(0, sizeof(int64_t)), (uint8_t*) result);
+ return NULL;
+ }
+
+ return "TypeID is neither 'CFNumber' nor 'CFData'";
+}
+
+const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result) {
+ if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
+ if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberSInt32Type, result)) {
+ return "Number type is not SInt32";
+ }
+ return NULL;
+ } else if (CFGetTypeID(cf) == CFDataGetTypeID()) {
+ if (CFDataGetLength((CFDataRef) cf) != sizeof(*result)) {
+ return "Data length is not sizeof(int32_t)";
+ }
+ CFDataGetBytes((CFDataRef) cf, CFRangeMake(0, sizeof(*result)), (uint8_t*) result);
+ return NULL;
+ }
+
+ return "TypeID is neither 'CFNumber' nor 'CFData'";
+}
+
+const char* ffCfNumGetDouble(CFTypeRef cf, double* result) {
+ if (CFGetTypeID(cf) == CFNumberGetTypeID()) {
+ if (!CFNumberGetValue((CFNumberRef) cf, kCFNumberDoubleType, result) &&
+ !CFNumberGetValue((CFNumberRef) cf, kCFNumberFloatType, result)) {
+ return "Number type is not Double or Float";
+ }
+ return NULL;
+ }
+
+ return "TypeID is neither 'CFNumber'";
+}
+
+const char* ffCfDateGetEpoch(CFTypeRef cf, uint64_t* result) {
+ if (CFGetTypeID(cf) != CFDateGetTypeID()) {
+ return "TypeID is not 'CFDate'";
+ }
+
+ CFAbsoluteTime absTime = CFDateGetAbsoluteTime((CFDateRef) cf);
+ // Convert from seconds to milliseconds and add the difference between 1970 and 2001 in milliseconds
+ *result = (uint64_t) ((absTime + 978307200 /*kCFAbsoluteTimeIntervalSince1970*/) * 1000);
+ return NULL;
+}
+
+const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result) {
+ ffStrbufClear(result);
+ if (!cf) {
+ return NULL;
+ }
+
+ if (CFGetTypeID(cf) == CFStringGetTypeID()) {
+ CFStringRef cfStr = (CFStringRef) cf;
+
+ const char* cstr = CFStringGetCStringPtr(cfStr, kCFStringEncodingUTF8);
+ if (cstr) {
+ ffStrbufSetS(result, cstr);
+ } else {
+ uint32_t length = (uint32_t) CFStringGetLength(cfStr);
+ if (length == 0) {
+ return NULL;
+ }
+ ffStrbufEnsureFixedLengthFree(result, (uint32_t) CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8));
+ if (!CFStringGetCString(cfStr, result->chars, result->allocated, kCFStringEncodingUTF8)) {
+ return "CFStringGetCString() failed";
+ }
+ // CFStringGetCString ensures the buffer is NUL terminated
+ // https://developer.apple.com/documentation/corefoundation/1542721-cfstringgetcstring
+ result->length = (uint32_t) strnlen(result->chars, (uint32_t) result->allocated);
+ }
+ } else if (CFGetTypeID(cf) == CFDataGetTypeID()) {
+ CFDataRef cfData = (CFDataRef) cf;
+ uint32_t length = (uint32_t) CFDataGetLength(cfData);
+ if (length == 0) {
+ return NULL;
+ }
+ ffStrbufEnsureFixedLengthFree(result, length + 1);
+ CFDataGetBytes(cfData, CFRangeMake(0, length), (uint8_t*) result->chars);
+ result->length = (uint32_t) strnlen(result->chars, length);
+ result->chars[result->length] = '\0';
+ } else {
+ return "TypeID is neither 'CFString' nor 'CFData'";
+ }
+
+ return NULL;
+}
+
+const char* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result) {
+ ffStrbufClear(result);
+ if (!cf) {
+ return NULL;
+ }
+
+ if (CFGetTypeID(cf) == CFDataGetTypeID()) {
+ CFDataRef cfData = (CFDataRef) cf;
+ uint32_t length = (uint32_t) CFDataGetLength(cfData);
+ if (length == 0) {
+ return NULL;
+ }
+ ffStrbufEnsureFixedLengthFree(result, length + 1);
+ CFDataGetBytes(cfData, CFRangeMake(0, length), (uint8_t*) result->chars);
+ result->length = length;
+ result->chars[result->length] = '\0';
+ } else {
+ return "TypeID is not 'CFData'";
+ }
+
+ return NULL;
+}
+
+const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result) {
+ CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL) {
+ return "CFDictionaryGetValue() failed";
+ }
+
+ return ffCfStrGetString(cf, result);
+}
+
+const char* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result) {
+ CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL) {
+ return "CFDictionaryGetValue() failed";
+ }
+
+ return ffCfDataGetDataAsString(cf, result);
+}
+
+const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result) {
+ CFBooleanRef cf = (CFBooleanRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL) {
+ return "CFDictionaryGetValue() failed";
+ }
+
+ if (CFGetTypeID(cf) != CFBooleanGetTypeID()) {
+ return "TypeID is not 'CFBoolean'";
+ }
+
+ *result = CFBooleanGetValue(cf);
+ return NULL;
+}
+
+const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result) {
+ CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL) {
+ return "CFDictionaryGetValue() failed";
+ }
+
+ return ffCfNumGetInt(cf, result);
+}
+
+const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* result) {
+ CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL) {
+ return "CFDictionaryGetValue() failed";
+ }
+
+ return ffCfNumGetInt64(cf, result);
+}
+
+const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* result) {
+ CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL) {
+ return "CFDictionaryGetValue() failed";
+ }
+
+ return ffCfNumGetDouble(cf, result);
+}
+
+const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offset, uint32_t size, uint8_t* result, uint32_t* length) {
+ CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL) {
+ return "CFDictionaryGetValue() failed";
+ }
+
+ if (CFGetTypeID(cf) != CFDataGetTypeID()) {
+ return "TypeID is not 'CFData'";
+ }
+
+ CFIndex trueLength = CFDataGetLength((CFDataRef) cf);
+
+ if (trueLength < offset + size) {
+ return "Data length is less than offset + size";
+ }
+
+ if (length) {
+ *length = (uint32_t) trueLength;
+ }
+
+ CFDataGetBytes((CFDataRef) cf, CFRangeMake(offset, size), result);
+ return NULL;
+}
+
+const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryRef* result) {
+ CFDictionaryRef cf = (CFDictionaryRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL || CFGetTypeID(cf) != CFDictionaryGetTypeID()) {
+ return "TypeID is not 'CFDictionary'";
+ }
+
+ *result = cf;
+ return NULL;
+}
+
+const char* ffCfDictGetDateAsEpoch(CFDictionaryRef dict, CFStringRef key, uint64_t* result) {
+ CFTypeRef cf = (CFTypeRef) CFDictionaryGetValue(dict, key);
+ if (cf == NULL) {
+ return "CFDictionaryGetValue() failed";
+ }
+
+ return ffCfDateGetEpoch(cf, result);
+}
diff --git a/src/common/apple/cf_helpers.h b/src/common/apple/cf_helpers.h
new file mode 100644
index 0000000..a929f03
--- /dev/null
+++ b/src/common/apple/cf_helpers.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "fastfetch.h"
+#include <CoreFoundation/CoreFoundation.h>
+#include <IOKit/IOKitLib.h>
+
+// Return error info if failed, NULL otherwise
+const char* ffCfStrGetString(CFTypeRef cf, FFstrbuf* result);
+const char* ffCfNumGetInt(CFTypeRef cf, int32_t* result);
+const char* ffCfNumGetInt64(CFTypeRef cf, int64_t* result);
+const char* ffCfNumGetDouble(CFTypeRef cf, double* result);
+const char* ffCfDateGetEpoch(CFTypeRef cf, uint64_t* result);
+const char* ffCfDataGetDataAsString(CFTypeRef cf, FFstrbuf* result);
+const char* ffCfDictGetString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result);
+const char* ffCfDictGetBool(CFDictionaryRef dict, CFStringRef key, bool* result);
+const char* ffCfDictGetInt(CFDictionaryRef dict, CFStringRef key, int* result);
+const char* ffCfDictGetInt64(CFDictionaryRef dict, CFStringRef key, int64_t* result);
+const char* ffCfDictGetDouble(CFDictionaryRef dict, CFStringRef key, double* result);
+const char* ffCfDictGetData(CFDictionaryRef dict, CFStringRef key, uint32_t offset, uint32_t size, uint8_t* result, uint32_t* length);
+const char* ffCfDictGetDataAsString(CFDictionaryRef dict, CFStringRef key, FFstrbuf* result);
+const char* ffCfDictGetDict(CFDictionaryRef dict, CFStringRef key, CFDictionaryRef* result);
+const char* ffCfDictGetDateAsEpoch(CFDictionaryRef dict, CFStringRef key, uint64_t* result);
+
+static inline CFNumberRef ffCfCreateInt(int value) {
+ return CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &value);
+}
+
+static inline void cfReleaseWrapper(void* type) {
+ assert(type);
+ if (*(CFTypeRef*) type) {
+ CFRelease(*(CFTypeRef*) type);
+ }
+}
+
+#define FF_CFTYPE_AUTO_RELEASE FF_A_CLEANUP(cfReleaseWrapper)
+
+static inline void wrapIoObjectRelease(io_object_t* service) {
+ assert(service);
+ if (*service) {
+ IOObjectRelease(*service);
+ }
+}
+#define FF_IOOBJECT_AUTO_RELEASE FF_A_CLEANUP(wrapIoObjectRelease)
diff --git a/src/common/apple/osascript.h b/src/common/apple/osascript.h
new file mode 100644
index 0000000..164d8d7
--- /dev/null
+++ b/src/common/apple/osascript.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "fastfetch.h"
+
+bool ffOsascript(const char* input, FFstrbuf* result);
diff --git a/src/common/apple/osascript.m b/src/common/apple/osascript.m
new file mode 100644
index 0000000..f17af03
--- /dev/null
+++ b/src/common/apple/osascript.m
@@ -0,0 +1,15 @@
+#include "osascript.h"
+
+#import <Foundation/Foundation.h>
+
+bool ffOsascript(const char* input, FFstrbuf* result)
+{
+ NSAppleScript* script = [NSAppleScript.alloc initWithSource:@(input)];
+ NSDictionary* errInfo = nil;
+ NSAppleEventDescriptor* descriptor = [script executeAndReturnError:&errInfo];
+ if (errInfo)
+ return false;
+
+ ffStrbufSetS(result, descriptor.stringValue.UTF8String);
+ return true;
+}
diff --git a/src/common/apple/smc_temps.c b/src/common/apple/smc_temps.c
new file mode 100644
index 0000000..6669c31
--- /dev/null
+++ b/src/common/apple/smc_temps.c
@@ -0,0 +1,465 @@
+#include "smc_temps.h"
+#include "common/apple/cf_helpers.h"
+#include "common/strutil.h"
+
+#include <stdint.h>
+#include <IOKit/IOKitLib.h>
+
+static const char kSmcCmdReadBytes = 5;
+static const char kSmcCmdReadKeyInfo = 9;
+static const uint32_t kKernelIndexSmc = 2;
+
+typedef struct
+{
+ char major;
+ char minor;
+ char build;
+ char reserved[1];
+ uint16_t release;
+} SmcKeyData_vers_t;
+
+typedef struct
+{
+ uint16_t version;
+ uint16_t length;
+ uint32_t cpuPLimit;
+ uint32_t gpuPLimit;
+ uint32_t memPLimit;
+} SmcKeyData_pLimitData_t;
+
+typedef struct
+{
+ uint32_t dataSize;
+ uint32_t dataType;
+ char dataAttributes;
+} SmcKeyData_keyInfo_t;
+
+typedef unsigned char SmcBytes_t[32];
+
+typedef struct
+{
+ uint32_t key;
+ SmcKeyData_vers_t vers;
+ SmcKeyData_pLimitData_t pLimitData;
+ SmcKeyData_keyInfo_t keyInfo;
+ char result;
+ char status;
+ char data8;
+ uint32_t data32;
+ SmcBytes_t bytes;
+} SmcKeyData_t;
+
+typedef char UInt32Char_t[5];
+
+typedef struct
+{
+ UInt32Char_t key;
+ uint32_t dataSize;
+ UInt32Char_t dataType;
+ SmcBytes_t bytes;
+} SmcVal_t;
+
+static uint32_t smcStrtoul(const char* str, int size, int base) {
+ uint32_t total = 0;
+
+ for (int i = 0; i < size; i++) {
+ if (base == 16) {
+ total += (uint32_t) (str[i] << (size - 1 - i) * 8);
+ } else {
+ total += (uint32_t) ((unsigned char) (str[i]) << (size - 1 - i) * 8);
+ }
+ }
+ return total;
+}
+
+static void smcUltostr(char* str, uint32_t val) {
+ str[0] = (char) (val >> 24);
+ str[1] = (char) (val >> 16);
+ str[2] = (char) (val >> 8);
+ str[3] = (char) val;
+ str[4] = '\0';
+}
+
+static const char* smcCall(io_connect_t conn, uint32_t selector, SmcKeyData_t* inputStructure, SmcKeyData_t* outputStructure) {
+ size_t size = sizeof(SmcKeyData_t);
+
+ if (IOConnectCallStructMethod(conn, selector, inputStructure, size, outputStructure, &size) != kIOReturnSuccess) {
+ return "IOConnectCallStructMethod(conn) failed";
+ }
+ return NULL;
+}
+
+// Provides key info, using a cache to dramatically improve the energy impact of smcFanControl
+static const char* smcGetKeyInfo(io_connect_t conn, const uint32_t key, SmcKeyData_keyInfo_t* key_info) {
+ SmcKeyData_t inputStructure = { 0 };
+ SmcKeyData_t outputStructure = { 0 };
+
+ inputStructure.key = key;
+ inputStructure.data8 = kSmcCmdReadKeyInfo;
+
+ const char* error = smcCall(conn, kKernelIndexSmc, &inputStructure, &outputStructure);
+ if (error) {
+ return error;
+ }
+
+ *key_info = outputStructure.keyInfo;
+ return NULL;
+}
+
+static const char* smcReadSmcVal(io_connect_t conn, const UInt32Char_t key, SmcVal_t* val) {
+ SmcKeyData_t inputStructure = { 0 };
+ SmcKeyData_t outputStructure = { 0 };
+
+ inputStructure.key = smcStrtoul(key, 4, 16);
+ strcpy(val->key, key);
+
+ const char* error = smcGetKeyInfo(conn, inputStructure.key, &outputStructure.keyInfo);
+ if (error) {
+ return error;
+ }
+
+ val->dataSize = outputStructure.keyInfo.dataSize;
+ smcUltostr(val->dataType, outputStructure.keyInfo.dataType);
+ inputStructure.keyInfo.dataSize = val->dataSize;
+ inputStructure.data8 = kSmcCmdReadBytes;
+
+ error = smcCall(conn, kKernelIndexSmc, &inputStructure, &outputStructure);
+ if (error) {
+ return error;
+ }
+
+ memcpy(val->bytes, outputStructure.bytes, sizeof(outputStructure.bytes));
+
+ return NULL;
+}
+
+static const char* smcOpen(io_connect_t* conn) {
+ FF_IOOBJECT_AUTO_RELEASE io_object_t device = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("AppleSMC"));
+ if (!device) {
+ return "No SMC device found";
+ }
+
+ if (IOServiceOpen(device, mach_task_self(), 0, conn) != kIOReturnSuccess) {
+ return "IOServiceOpen() failed";
+ }
+
+ return NULL;
+}
+
+static const char* smcReadValue(io_connect_t conn, const UInt32Char_t key, double* value) {
+ SmcVal_t val = { 0 };
+ const char* error = smcReadSmcVal(conn, key, &val);
+ if (error != NULL) {
+ return error;
+ }
+ if (val.dataSize == 0) {
+ return "Empty SMC result";
+ }
+
+ switch (val.dataType[0]) {
+ case 'u': // unsigned integer types
+ if (val.dataType[1] == 'i') {
+ switch (val.dataSize) {
+ case 1:
+ *value = *(uint8_t*) (val.bytes);
+ break;
+ case 2:
+ *value = ntohs(*(uint16_t*) (val.bytes));
+ break;
+ case 4:
+ *value = ntohl(*(uint32_t*) (val.bytes));
+ break;
+ case 8:
+ *value = (double) ntohll(*(uint64_t*) (val.bytes));
+ break;
+ default:
+ return "Unsupported SMC unsigned integer data size";
+ }
+ } else {
+ return "Unsupported SMC unsigned data type";
+ }
+ break;
+
+ case 'f': // floating point types
+ if (ffStrEquals(val.dataType, "flt ") && val.dataSize == 4) {
+ *value = *(float*) (val.bytes);
+ } else if (val.dataType[1] == 'p' && val.dataSize == 2) // fixed point types
+ {
+ if (ffStrEquals(val.dataType, "fp1f")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 32768.0;
+ } else if (ffStrEquals(val.dataType, "fp4c")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 4096.0;
+ } else if (ffStrEquals(val.dataType, "fp5b")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 2048.0;
+ } else if (ffStrEquals(val.dataType, "fp6a")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 1024.0;
+ } else if (ffStrEquals(val.dataType, "fp79")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 512.0;
+ } else if (ffStrEquals(val.dataType, "fp88")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 256.0;
+ } else if (ffStrEquals(val.dataType, "fpa6")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 64.0;
+ } else if (ffStrEquals(val.dataType, "fpc4")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 16.0;
+ } else if (ffStrEquals(val.dataType, "fpe2")) {
+ *value = ntohs(*(uint16_t*) (val.bytes)) / 4.0;
+ } else {
+ return "Unsupported SMC floating point data type";
+ }
+ } else {
+ return "Unsupported SMC floating point data type";
+ }
+ break;
+
+ case 's': // signed integer types
+ if (val.dataType[1] == 'i') {
+ switch (val.dataSize) {
+ case 1:
+ *value = *(int8_t*) (val.bytes);
+ break;
+ case 2:
+ *value = ntohs(*(int16_t*) (val.bytes));
+ break;
+ case 4:
+ *value = ntohl(*(int32_t*) (val.bytes));
+ break;
+ case 8:
+ *value = (double) ntohll(*(int64_t*) (val.bytes));
+ break;
+ default:
+ return "Unsupported SMC signed integer data size";
+ }
+ } else if (val.dataType[1] == 'p' && val.dataSize == 2) // signed fixed point types
+ {
+ if (ffStrEquals(val.dataType, "sp1e")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 16384.0;
+ } else if (ffStrEquals(val.dataType, "sp3c")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 4096.0;
+ } else if (ffStrEquals(val.dataType, "sp4b")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 2048.0;
+ } else if (ffStrEquals(val.dataType, "sp5a")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 1024.0;
+ } else if (ffStrEquals(val.dataType, "sp69")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 512.0;
+ } else if (ffStrEquals(val.dataType, "sp78")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 256.0;
+ } else if (ffStrEquals(val.dataType, "sp87")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 128.0;
+ } else if (ffStrEquals(val.dataType, "sp96")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 64.0;
+ } else if (ffStrEquals(val.dataType, "spb4")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 16.0;
+ } else if (ffStrEquals(val.dataType, "spf0")) {
+ *value = (int16_t) ntohs(*(int16_t*) (val.bytes)) / 1.0;
+ } else {
+ return "Unsupported SMC signed integer data type";
+ }
+ } else {
+ return "Unsupported SMC signed data type";
+ }
+ break;
+
+ case '{': // special types like pwm
+ if (ffStrEquals(val.dataType, "{pwm") && val.dataSize == 2) {
+ *value = (double) ntohs(*(uint16_t*) (val.bytes)) * 100 / 65536.0;
+ } else {
+ return "Unsupported SMC special data type";
+ }
+ break;
+
+ default:
+ return "Unsupported SMC data type";
+ }
+ return NULL;
+}
+
+static bool detectTemp(io_connect_t conn, const char* sensor, double* sum) {
+ double temp = 0;
+ const char* error = smcReadValue(conn, sensor, &temp);
+ if (error) {
+ return false;
+ }
+ // https://github.com/exelban/stats/blob/14e29c4d60229c363cca9c9d25c30c87b7870830/Modules/Sensors/readers.swift#L124
+ if (temp < 10 || temp > 120) {
+ return false;
+ }
+ *sum += temp;
+ return true;
+}
+
+static io_connect_t conn;
+
+const char* ffDetectSmcSpecificTemp(const char* sensor, double* result) {
+ if (!conn) {
+ if (smcOpen(&conn) != NULL) {
+ conn = (io_connect_t) -1;
+ }
+ }
+ if (conn == (io_connect_t) -1) {
+ return "Could not open SMC connection";
+ }
+
+ if (!detectTemp(conn, sensor, result)) {
+ return "Could not read SMC temperature";
+ }
+
+ return NULL;
+}
+
+const char* ffDetectSmcTemps(enum FFTempType type, double* result) {
+ if (!conn) {
+ if (smcOpen(&conn) != NULL) {
+ conn = (io_connect_t) -1;
+ }
+ }
+ if (conn == (io_connect_t) -1) {
+ return "Could not open SMC connection";
+ }
+
+ uint32_t count = 0;
+ *result = 0;
+
+ // https://github.com/exelban/stats/blob/master/Modules/Sensors/values.swift
+ switch (type) {
+ case FF_TEMP_CPU_X64:
+ count += detectTemp(conn, "TC0D", result); // CPU diode
+ count += detectTemp(conn, "TC0E", result); // CPU diode virtual
+ count += detectTemp(conn, "TC0F", result); // CPU diode filtered
+ count += detectTemp(conn, "TC0P", result); // CPU proximity
+ break;
+
+ case FF_TEMP_CPU_M1X:
+ count += detectTemp(conn, "Tp09", result); // CPU efficient core 1
+ count += detectTemp(conn, "Tp0T", result); // CPU efficient core 2
+
+ count += detectTemp(conn, "Tp01", result); // CPU performance core 1
+ count += detectTemp(conn, "Tp05", result); // CPU performance core 2
+ count += detectTemp(conn, "Tp0D", result); // CPU performance core 3
+ count += detectTemp(conn, "Tp0H", result); // CPU performance core 4
+ count += detectTemp(conn, "Tp0L", result); // CPU performance core 5
+ count += detectTemp(conn, "Tp0P", result); // CPU performance core 6
+ count += detectTemp(conn, "Tp0X", result); // CPU performance core 7
+ count += detectTemp(conn, "Tp0b", result); // CPU performance core 8
+ break;
+
+ case FF_TEMP_CPU_M2X:
+ count += detectTemp(conn, "Tp1h", result); // CPU efficiency core 1
+ count += detectTemp(conn, "Tp1t", result); // CPU efficiency core 2
+ count += detectTemp(conn, "Tp1p", result); // CPU efficiency core 3
+ count += detectTemp(conn, "Tp1l", result); // CPU efficiency core 4
+
+ count += detectTemp(conn, "Tp01", result); // CPU performance core 1
+ count += detectTemp(conn, "Tp05", result); // CPU performance core 2
+ count += detectTemp(conn, "Tp09", result); // CPU performance core 3
+ count += detectTemp(conn, "Tp0D", result); // CPU performance core 4
+ count += detectTemp(conn, "Tp0X", result); // CPU performance core 5
+ count += detectTemp(conn, "Tp0b", result); // CPU performance core 6
+ count += detectTemp(conn, "Tp0f", result); // CPU performance core 7
+ count += detectTemp(conn, "Tp0j", result); // CPU performance core 8
+ break;
+
+ case FF_TEMP_CPU_M3X:
+ count += detectTemp(conn, "Te05", result); // CPU efficiency core 1
+ count += detectTemp(conn, "Te0L", result); // CPU efficiency core 2
+ count += detectTemp(conn, "Te0P", result); // CPU efficiency core 3
+ count += detectTemp(conn, "Te0S", result); // CPU efficiency core 4
+ count += detectTemp(conn, "Tf04", result); // CPU performance core 1
+ count += detectTemp(conn, "Tf09", result); // CPU performance core 2
+ count += detectTemp(conn, "Tf0A", result); // CPU performance core 3
+ count += detectTemp(conn, "Tf0B", result); // CPU performance core 4
+ count += detectTemp(conn, "Tf0D", result); // CPU performance core 5
+ count += detectTemp(conn, "Tf0E", result); // CPU performance core 6
+ count += detectTemp(conn, "Tf44", result); // CPU performance core 7
+ count += detectTemp(conn, "Tf49", result); // CPU performance core 8
+ count += detectTemp(conn, "Tf4A", result); // CPU performance core 9
+ count += detectTemp(conn, "Tf4B", result); // CPU performance core 10
+ count += detectTemp(conn, "Tf4D", result); // CPU performance core 11
+ count += detectTemp(conn, "Tf4E", result); // CPU performance core 12
+ break;
+
+ case FF_TEMP_CPU_M4X:
+ count += detectTemp(conn, "Te05", result); // CPU efficiency core 1
+ count += detectTemp(conn, "Te0S", result); // CPU efficiency core 2
+ count += detectTemp(conn, "Te09", result); // CPU efficiency core 3
+ count += detectTemp(conn, "Te0H", result); // CPU efficiency core 4
+ count += detectTemp(conn, "Tp01", result); // CPU performance core 1
+ count += detectTemp(conn, "Tp05", result); // CPU performance core 2
+ count += detectTemp(conn, "Tp09", result); // CPU performance core 3
+ count += detectTemp(conn, "Tp0D", result); // CPU performance core 4
+ count += detectTemp(conn, "Tp0V", result); // CPU performance core 5
+ count += detectTemp(conn, "Tp0Y", result); // CPU performance core 6
+ count += detectTemp(conn, "Tp0b", result); // CPU performance core 7
+ count += detectTemp(conn, "Tp0e", result); // CPU performance core 8
+ break;
+
+ case FF_TEMP_GPU_INTEL:
+ count += detectTemp(conn, "TCGC", result); // GPU Intel Graphics
+ goto gpu_unknown;
+
+ case FF_TEMP_GPU_AMD:
+ count += detectTemp(conn, "TGDD", result); // GPU AMD Radeon
+ goto gpu_unknown;
+
+ case FF_TEMP_GPU_UNKNOWN: // Nvidia?
+ gpu_unknown:
+ count += detectTemp(conn, "TG0D", result); // GPU diode
+ count += detectTemp(conn, "TG0P", result); // GPU proximity
+ break;
+
+ case FF_TEMP_GPU_M1X:
+ count += detectTemp(conn, "Tg05", result); // GPU 1
+ count += detectTemp(conn, "Tg0D", result); // GPU 2
+ count += detectTemp(conn, "Tg0L", result); // GPU 3
+ count += detectTemp(conn, "Tg0T", result); // GPU 4
+ break;
+
+ case FF_TEMP_GPU_M2X:
+ count += detectTemp(conn, "Tg0f", result); // GPU 1
+ count += detectTemp(conn, "Tg0j", result); // GPU 2
+ break;
+
+ case FF_TEMP_GPU_M3X:
+ count += detectTemp(conn, "Tf14", result); // GPU 1
+ count += detectTemp(conn, "Tf18", result); // GPU 2
+ count += detectTemp(conn, "Tf19", result); // GPU 3
+ count += detectTemp(conn, "Tf1A", result); // GPU 4
+ count += detectTemp(conn, "Tf24", result); // GPU 5
+ count += detectTemp(conn, "Tf28", result); // GPU 6
+ count += detectTemp(conn, "Tf29", result); // GPU 7
+ count += detectTemp(conn, "Tf2A", result); // GPU 8
+ break;
+
+ case FF_TEMP_GPU_M4X:
+ count += detectTemp(conn, "Tg0G", result); // GPU 1 (Basic)
+ count += detectTemp(conn, "Tg0H", result); // GPU 2 (Basic)
+ count += detectTemp(conn, "Tg1U", result); // GPU 1 (Pro / Max)
+ count += detectTemp(conn, "Tg1k", result); // GPU 2 (Pro / Max)
+ count += detectTemp(conn, "Tg0K", result); // GPU 3
+ count += detectTemp(conn, "Tg0L", result); // GPU 4
+ count += detectTemp(conn, "Tg0d", result); // GPU 5
+ count += detectTemp(conn, "Tg0e", result); // GPU 6
+ count += detectTemp(conn, "Tg0j", result); // GPU 7
+ count += detectTemp(conn, "Tg0k", result); // GPU 8
+ break;
+
+ case FF_TEMP_BATTERY:
+ count += detectTemp(conn, "TB1T", result); // Battery
+ count += detectTemp(conn, "TB2T", result); // Battery
+ break;
+
+ case FF_TEMP_MEMORY:
+ count += detectTemp(conn, "Tm02", result); // Memory 1
+ count += detectTemp(conn, "Tm06", result); // Memory 2
+ count += detectTemp(conn, "Tm08", result); // Memory 3
+ count += detectTemp(conn, "Tm09", result); // Memory 4
+ break;
+ }
+
+ if (count == 0) {
+ return "No temperatures detected";
+ }
+
+ *result /= count;
+
+ return NULL;
+}
diff --git a/src/common/apple/smc_temps.h b/src/common/apple/smc_temps.h
new file mode 100644
index 0000000..6783bb9
--- /dev/null
+++ b/src/common/apple/smc_temps.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "fastfetch.h"
+
+typedef struct FFTempValue {
+ FFstrbuf name;
+ FFstrbuf deviceClass;
+ double value;
+} FFTempValue;
+
+enum FFTempType {
+ FF_TEMP_CPU_X64,
+ FF_TEMP_CPU_M1X,
+ FF_TEMP_CPU_M2X,
+ FF_TEMP_CPU_M3X,
+ FF_TEMP_CPU_M4X,
+
+ FF_TEMP_GPU_INTEL,
+ FF_TEMP_GPU_AMD,
+ FF_TEMP_GPU_UNKNOWN,
+ FF_TEMP_GPU_M1X,
+ FF_TEMP_GPU_M2X,
+ FF_TEMP_GPU_M3X,
+ FF_TEMP_GPU_M4X,
+
+ FF_TEMP_BATTERY,
+
+ FF_TEMP_MEMORY,
+};
+
+const char* ffDetectSmcSpecificTemp(const char* sensor, double* result);
+const char* ffDetectSmcTemps(enum FFTempType type, double* result);
diff --git a/src/common/apple/version.h b/src/common/apple/version.h
new file mode 100644
index 0000000..d15acb4
--- /dev/null
+++ b/src/common/apple/version.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "fastfetch.h"
+
+bool ffGetAppNameAndVersion(const char* exePath, FFstrbuf* retName, FFstrbuf* retVersion);
diff --git a/src/common/apple/version.m b/src/common/apple/version.m
new file mode 100644
index 0000000..133ec5b
--- /dev/null
+++ b/src/common/apple/version.m
@@ -0,0 +1,43 @@
+#include "common/apple/version.h"
+#include "common/strutil.h"
+
+#import <Foundation/Foundation.h>
+
+#define APP_CONTENTS_MACOS ".app/Contents/MacOS"
+
+bool ffGetAppNameAndVersion(const char* exePath, FFstrbuf* retName, FFstrbuf* retVersion) {
+ char* lastSlash = strrchr(exePath, '/');
+ if (!lastSlash) {
+ return false;
+ }
+ if ((size_t) (lastSlash - exePath) > strlen("X" APP_CONTENTS_MACOS) && memcmp(lastSlash - strlen(APP_CONTENTS_MACOS), APP_CONTENTS_MACOS, strlen(APP_CONTENTS_MACOS)) != 0) {
+ return false;
+ }
+
+ lastSlash -= strlen("MacOS");
+ char infoPlistPath[PATH_MAX];
+ memcpy(infoPlistPath, exePath, lastSlash - exePath);
+ memcpy(infoPlistPath + (lastSlash - exePath), "Info.plist", sizeof("Info.plist")); // X.app/Contents/Info.plist
+ NSError* error;
+ NSDictionary* dict = [NSDictionary dictionaryWithContentsOfURL:[NSURL fileURLWithPath:@(infoPlistPath)]
+ error:&error];
+ if (!dict) {
+ return false;
+ }
+
+ if (retName) {
+ NSString* bundleName = dict[@"CFBundleDisplayName"] ?: dict[@"CFBundleName"];
+ if (bundleName) {
+ ffStrbufSetS(retName, bundleName.UTF8String);
+ }
+ }
+
+ if (retVersion) {
+ NSString* bundleVersion = dict[@"CFBundleShortVersionString"];
+ if (bundleVersion) {
+ ffStrbufSetS(retVersion, bundleVersion.UTF8String);
+ }
+ }
+
+ return true;
+}
diff --git a/src/common/argType.h b/src/common/argType.h
new file mode 100644
index 0000000..81217c7
--- /dev/null
+++ b/src/common/argType.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+
+// For ffRegReadValue(s)
+// If length is 0, data will be allocated with malloc() and must be freed by the caller
+// Otherwise, data must point to a buffer of at least length bytes, and the actual length of the data will be written to length.
+typedef struct FFArgBuffer {
+ void* data;
+ uint32_t length;
+} FFArgBuffer;
+
+typedef enum FF_A_PACKED FFArgType {
+ FF_ARG_TYPE_NULL = 0,
+ FF_ARG_TYPE_UINT,
+ FF_ARG_TYPE_UINT64,
+ FF_ARG_TYPE_UINT16,
+ FF_ARG_TYPE_UINT8,
+ FF_ARG_TYPE_INT,
+ FF_ARG_TYPE_STRING,
+ FF_ARG_TYPE_STRBUF,
+ FF_ARG_TYPE_FLOAT,
+ FF_ARG_TYPE_DOUBLE,
+ FF_ARG_TYPE_LIST,
+ FF_ARG_TYPE_BOOL,
+ FF_ARG_TYPE_BUFFER,
+} FFArgType;
+
+#define FF_ARG(variable, var_name) { _Generic((variable), \
+ uint32_t: FF_ARG_TYPE_UINT, \
+ uint64_t: FF_ARG_TYPE_UINT64, \
+ uint16_t: FF_ARG_TYPE_UINT16, \
+ uint8_t: FF_ARG_TYPE_UINT8, \
+ int32_t: FF_ARG_TYPE_INT, \
+ char*: FF_ARG_TYPE_STRING, \
+ const char*: FF_ARG_TYPE_STRING, \
+ FFstrbuf: FF_ARG_TYPE_STRBUF, \
+ float: FF_ARG_TYPE_FLOAT, \
+ double: FF_ARG_TYPE_DOUBLE, \
+ FFlist: FF_ARG_TYPE_LIST, \
+ bool: FF_ARG_TYPE_BOOL, \
+ FFArgBuffer: FF_ARG_TYPE_BUFFER), \
+ _Generic((variable), char*: (variable), const char*: (variable), default: &(variable)), \
+ (var_name) }
diff --git a/src/common/arrutil.h b/src/common/arrutil.h
new file mode 100644
index 0000000..83a49d6
--- /dev/null
+++ b/src/common/arrutil.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <assert.h>
+
+#ifdef __has_builtin
+ #if !__cplusplus && FF_SUPPORTS_COUNT_OF
+ #define ARRAY_SIZE(x) _Countof(x)
+ #elif __has_builtin(__is_array)
+ #define ARRAY_SIZE(x) ({ static_assert(__is_array(__typeof__(x)), "Must be an array"); (uint32_t) (sizeof(x) / sizeof(*(x))); })
+ #elif __has_builtin(__builtin_types_compatible_p)
+ #define ARRAY_SIZE(x) ({ static_assert(!__builtin_types_compatible_p(__typeof__(x), __typeof__(&*(x))), "Must not be a pointer"); (uint32_t) (sizeof(x) / sizeof(*(x))); })
+ #endif
+#endif
+#ifndef ARRAY_SIZE
+ #define ARRAY_SIZE(x) ((uint32_t) (sizeof(x) / sizeof(*(x))))
+#endif
diff --git a/src/common/attributes.h b/src/common/attributes.h
new file mode 100644
index 0000000..ff1e660
--- /dev/null
+++ b/src/common/attributes.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#ifndef __has_attribute
+ #error Unsupported compiler
+ #define __attribute__(x)
+#endif
+
+#define FF_A_FALLTHROUGH __attribute__((__fallthrough__))
+#define FF_A_DEPRECATED __attribute__((__deprecated__))
+#define FF_A_CLEANUP(func) __attribute__((__cleanup__(func)))
+#define FF_A_NODISCARD __attribute__((__warn_unused_result__))
+#define FF_A_PRINTF(formatStrIndex, argsStartIndex) __attribute__((__format__(printf, formatStrIndex, argsStartIndex)))
+#define FF_A_SCANF(formatStrIndex, argsStartIndex) __attribute__((__format__(scanf, formatStrIndex, argsStartIndex)))
+#define FF_A_NONNULL(argIndex, ...) __attribute__((__nonnull__(argIndex, ##__VA_ARGS__)))
+#define FF_A_RETURNS_NONNULL __attribute__((__returns_nonnull__))
+#define FF_A_UNUSED __attribute__((__unused__))
+#define FF_A_PACKED __attribute__((__packed__))
+#define FF_A_WEAK_IMPORT __attribute__((__weak_import__))
+#define FF_A_ALWAYS_INLINE __attribute__((__always_inline__))
diff --git a/src/common/base64.h b/src/common/base64.h
new file mode 100644
index 0000000..d17da5c
--- /dev/null
+++ b/src/common/base64.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "fastfetch.h"
+
+void ffBase64EncodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output);
+static inline FFstrbuf ffBase64EncodeStrbuf(const FFstrbuf* in) {
+ FFstrbuf out = ffStrbufCreateA(10 + in->length * 4 / 3);
+ ffBase64EncodeRaw(in->length, in->chars, &out.length, out.chars);
+ assert(out.length < out.allocated);
+
+ return out;
+}
+
+bool ffBase64DecodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output);
+static inline FFstrbuf ffBase64DecodeStrbuf(const FFstrbuf* in) {
+ FFstrbuf out = ffStrbufCreateA(10 + in->length * 3 / 4);
+ ffBase64DecodeRaw(in->length, in->chars, &out.length, out.chars);
+ assert(out.length < out.allocated);
+
+ return out;
+}
diff --git a/src/common/binary.h b/src/common/binary.h
new file mode 100644
index 0000000..2d2d784
--- /dev/null
+++ b/src/common/binary.h
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "fastfetch.h"
+
+/**
+ * Extracts string literals from a binary file
+ *
+ * @param file Path to the binary file to extract strings from
+ * @param cb Callback function that will be called for each string found
+ * Return false from callback to stop extraction
+ * @param userdata User-provided data passed to the callback function
+ * @param minLength Minimum length of strings to extract
+ *
+ * @return NULL on success, error message on failure.
+ * @note This function won't return an error if no strings are found.
+ * Always check if strings are correctly extracted after this function all.
+ */
+const char* ffBinaryExtractStrings(const char* file, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength);
diff --git a/src/common/color.h b/src/common/color.h
new file mode 100644
index 0000000..7f8c539
--- /dev/null
+++ b/src/common/color.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#define FF_COLOR_MODE_RESET "0;"
+#define FF_COLOR_MODE_BOLD "1;"
+#define FF_COLOR_MODE_DIM "2;"
+#define FF_COLOR_MODE_ITALIC "3;"
+#define FF_COLOR_MODE_UNDERLINE "4;"
+#define FF_COLOR_MODE_BLINK "5;"
+#define FF_COLOR_MODE_INVERSE "7;"
+#define FF_COLOR_MODE_HIDDEN "8;"
+#define FF_COLOR_MODE_STRIKETHROUGH "9;"
+
+#define FF_COLOR_FG_BLACK "30"
+#define FF_COLOR_FG_RED "31"
+#define FF_COLOR_FG_GREEN "32"
+#define FF_COLOR_FG_YELLOW "33"
+#define FF_COLOR_FG_BLUE "34"
+#define FF_COLOR_FG_MAGENTA "35"
+#define FF_COLOR_FG_CYAN "36"
+#define FF_COLOR_FG_WHITE "37"
+#define FF_COLOR_FG_DEFAULT "39"
+
+#define FF_COLOR_FG_LIGHT_BLACK "90"
+#define FF_COLOR_FG_LIGHT_RED "91"
+#define FF_COLOR_FG_LIGHT_GREEN "92"
+#define FF_COLOR_FG_LIGHT_YELLOW "93"
+#define FF_COLOR_FG_LIGHT_BLUE "94"
+#define FF_COLOR_FG_LIGHT_MAGENTA "95"
+#define FF_COLOR_FG_LIGHT_CYAN "96"
+#define FF_COLOR_FG_LIGHT_WHITE "97"
+
+#define FF_COLOR_BG_BLACK "40"
+#define FF_COLOR_BG_RED "41"
+#define FF_COLOR_BG_GREEN "42"
+#define FF_COLOR_BG_YELLOW "43"
+#define FF_COLOR_BG_BLUE "44"
+#define FF_COLOR_BG_MAGENTA "45"
+#define FF_COLOR_BG_CYAN "46"
+#define FF_COLOR_BG_WHITE "47"
+#define FF_COLOR_BG_DEFAULT "49"
+
+#define FF_COLOR_BG_LIGHT_BLACK "100"
+#define FF_COLOR_BG_LIGHT_RED "101"
+#define FF_COLOR_BG_LIGHT_GREEN "102"
+#define FF_COLOR_BG_LIGHT_YELLOW "103"
+#define FF_COLOR_BG_LIGHT_BLUE "104"
+#define FF_COLOR_BG_LIGHT_MAGENTA "105"
+#define FF_COLOR_BG_LIGHT_CYAN "106"
+#define FF_COLOR_BG_LIGHT_WHITE "107"
+
+#define FF_COLOR_FG_256 "38;5;"
+#define FF_COLOR_BG_256 "48;5;"
+
+#define FF_COLOR_FG_RGB "38;2;"
+#define FF_COLOR_BG_RGB "48;2;"
diff --git a/src/common/commandoption.h b/src/common/commandoption.h
new file mode 100644
index 0000000..5f64701
--- /dev/null
+++ b/src/common/commandoption.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include "common/ffdata.h"
+
+void ffPrepareCommandOption(FFdata* data);
+void ffPrintCommandOption(FFdata* data);
+void ffMigrateCommandOptionToJsonc(FFdata* data);
+bool ffParseModuleOptions(const char* key, const char* value);
diff --git a/src/common/dbus.h b/src/common/dbus.h
new file mode 100644
index 0000000..e0689c3
--- /dev/null
+++ b/src/common/dbus.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#ifdef FF_HAVE_DBUS
+ #include <dbus/dbus.h>
+
+ #include "common/FFstrbuf.h"
+ #include "common/library.h"
+
+typedef struct FFDBusLibrary {
+ FF_LIBRARY_SYMBOL(dbus_bus_get)
+ FF_LIBRARY_SYMBOL(dbus_message_new_method_call)
+ FF_LIBRARY_SYMBOL(dbus_message_append_args)
+ FF_LIBRARY_SYMBOL(dbus_message_iter_init)
+ FF_LIBRARY_SYMBOL(dbus_message_iter_get_arg_type)
+ FF_LIBRARY_SYMBOL(dbus_message_iter_get_basic)
+ FF_LIBRARY_SYMBOL(dbus_message_iter_recurse)
+ FF_LIBRARY_SYMBOL(dbus_message_iter_has_next)
+ FF_LIBRARY_SYMBOL(dbus_message_iter_next)
+ FF_LIBRARY_SYMBOL(dbus_message_unref)
+ FF_LIBRARY_SYMBOL(dbus_connection_send_with_reply_and_block)
+ FF_LIBRARY_SYMBOL(dbus_connection_unref)
+} FFDBusLibrary;
+
+typedef struct FFDBusData {
+ const FFDBusLibrary* lib;
+ DBusConnection* connection;
+} FFDBusData;
+
+const char* ffDBusLoadData(DBusBusType busType, FFDBusData* data); // Returns an error message or NULL on success
+bool ffDBusGetString(FFDBusData* dbus, DBusMessageIter* iter, FFstrbuf* result);
+bool ffDBusGetBool(FFDBusData* dbus, DBusMessageIter* iter, bool* result);
+bool ffDBusGetUint(FFDBusData* dbus, DBusMessageIter* iter, uint64_t* result);
+DBusMessage* ffDBusGetMethodReply(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* method, const char* arg1, const char* arg2);
+DBusMessage* ffDBusGetProperty(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property);
+bool ffDBusGetPropertyString(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property, FFstrbuf* result);
+bool ffDBusGetInt(FFDBusData* dbus, DBusMessageIter* iter, int64_t* result);
+bool ffDBusGetPropertyUint(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property, uint64_t* result);
+void ffDBusDestroyData(FFDBusData* data);
+
+static inline DBusMessage* ffDBusGetAllProperties(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface) {
+ return ffDBusGetMethodReply(dbus, busName, objectPath, "org.freedesktop.DBus.Properties", "GetAll", interface, NULL);
+}
+
+ #define FF_DBUS_AUTO_DESTROY_DATA FF_A_CLEANUP(ffDBusDestroyData)
+
+#endif // FF_HAVE_DBUS
diff --git a/src/common/debug.h b/src/common/debug.h
new file mode 100644
index 0000000..a378fc8
--- /dev/null
+++ b/src/common/debug.h
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "fastfetch.h"
+#include "common/time.h"
+
+static inline const char* ffFindFileName(const char* file) {
+ const char* lastSlash = __builtin_strrchr(file, '/');
+#ifdef _WIN32
+ if (lastSlash == NULL) {
+ lastSlash = __builtin_strrchr(file, '\\');
+ }
+#endif
+ if (lastSlash != NULL) {
+ return lastSlash + 1;
+ }
+ return file;
+}
+
+#ifndef NDEBUG
+ #define FF_DEBUG_PRINT(file_, line_, format_, ...) \
+ do { \
+ if (instance.config.display.debugMode) \
+ fprintf(stderr, "[%s%4d, %s] " format_ "\n", ffFindFileName(file_), line_, ffTimeToTimeStr(ffTimeGetNow()), ##__VA_ARGS__); \
+ } while (0)
+#else
+ #define FF_DEBUG_PRINT(file_, line_, format_, ...) \
+ do { \
+ } while (0)
+#endif
+
+#define FF_DEBUG(format, ...) FF_DEBUG_PRINT(__FILE__, __LINE__, format, ##__VA_ARGS__)
+
+#if _WIN32
+const char* ffDebugWin32Error(DWORD errorCode);
+const char* ffDebugNtStatus(NTSTATUS status);
+const char* ffDebugHResult(HRESULT hr);
+#endif
diff --git a/src/common/duration.h b/src/common/duration.h
new file mode 100644
index 0000000..30733c9
--- /dev/null
+++ b/src/common/duration.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "fastfetch.h"
+
+void ffDurationAppendNum(uint64_t totalSeconds, FFstrbuf* result);
diff --git a/src/common/edidHelper.h b/src/common/edidHelper.h
new file mode 100644
index 0000000..dc4e4fb
--- /dev/null
+++ b/src/common/edidHelper.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <stdint.h>
+#include "common/FFstrbuf.h"
+
+void ffEdidGetVendorAndModel(const uint8_t edid[128], FFstrbuf* result);
+bool ffEdidGetName(const uint8_t edid[128], FFstrbuf* name);
+void ffEdidGetPreferredResolutionAndRefreshRate(const uint8_t edid[128], uint32_t* width, uint32_t* height, double* refreshRate);
+void ffEdidGetPhysicalResolution(const uint8_t edid[128], uint32_t* width, uint32_t* height);
+void ffEdidGetPhysicalSize(const uint8_t edid[128], uint32_t* width, uint32_t* height); // in mm
+void ffEdidGetSerialAndManufactureDate(const uint8_t edid[128], uint32_t* serial, uint16_t* year, uint16_t* week);
+bool ffEdidGetHdrCompatible(const uint8_t* edid, uint32_t length);
+bool ffEdidIsValid(const uint8_t edid[128], uint32_t length);
diff --git a/src/common/ffdata.h b/src/common/ffdata.h
new file mode 100644
index 0000000..189c0fa
--- /dev/null
+++ b/src/common/ffdata.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+
+typedef enum FF_A_PACKED FFDataResultDocType {
+ FF_RESULT_DOC_TYPE_DEFAULT = 0,
+ FF_RESULT_DOC_TYPE_JSON,
+ FF_RESULT_DOC_TYPE_CONFIG,
+ FF_RESULT_DOC_TYPE_CONFIG_FULL,
+} FFDataResultDocType;
+
+// FFdata aggregates configuration, generation parameters, and output state used by fastfetch.
+// It holds the parsed configuration document, a mutable JSON document for results, and related metadata.
+typedef struct FFdata {
+ yyjson_doc* configDoc; // Parsed JSON configuration document
+ yyjson_mut_doc* resultDoc; // Mutable JSON document for storing results
+ FFstrbuf structure; // Custom output structure from command line
+ FFstrbuf structureDisabled; // Disabled modules in the output structure from command line
+ FFstrbuf genConfigPath; // Path to generate configuration file
+ FFDataResultDocType docType; // Type of result document
+ bool configLoaded;
+} FFdata;
diff --git a/src/common/font.h b/src/common/font.h
new file mode 100644
index 0000000..f9d25e7
--- /dev/null
+++ b/src/common/font.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+#include "common/FFlist.h"
+
+typedef struct FFfont {
+ FFstrbuf pretty;
+ FFstrbuf name;
+ FFstrbuf size;
+ FFlist styles;
+} FFfont;
+
+void ffFontInit(FFfont* font);
+void ffFontInitQt(FFfont* font, const char* data);
+void ffFontInitPango(FFfont* font, const char* data);
+void ffFontInitValues(FFfont* font, const char* name, const char* size);
+void ffFontInitXlfd(FFfont* font, const char* xlfd);
+void ffFontInitXft(FFfont* font, const char* xft);
+void ffFontInitMoveValues(FFfont* font, FFstrbuf* name, FFstrbuf* size, FFstrbuf* style);
+void ffFontInitWithSpace(FFfont* font, const char* rawName);
+void ffFontDestroy(FFfont* font);
+
+static inline void ffFontInitCopy(FFfont* font, const char* name) {
+ ffFontInitValues(font, name, NULL);
+}
diff --git a/src/common/format.h b/src/common/format.h
new file mode 100644
index 0000000..5be5dff
--- /dev/null
+++ b/src/common/format.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "common/argType.h"
+
+typedef struct FFformatarg {
+ FFArgType type;
+ const void* value;
+ const char* name; // argument name, must start with an alphabet
+} FFformatarg;
+
+void ffFormatAppendFormatArg(FFstrbuf* buffer, const FFformatarg* formatarg);
+bool ffParseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments);
+#define FF_PARSE_FORMAT_STRING_CHECKED(buffer, formatstr, arguments) \
+ ffParseFormatString((buffer), (formatstr), sizeof(arguments) / sizeof(*arguments), (arguments));
diff --git a/src/common/frequency.h b/src/common/frequency.h
new file mode 100644
index 0000000..7a3bdab
--- /dev/null
+++ b/src/common/frequency.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "fastfetch.h"
+
+bool ffFreqAppendNum(uint32_t mhz, FFstrbuf* result);
diff --git a/src/common/haiku/version.cpp b/src/common/haiku/version.cpp
new file mode 100644
index 0000000..f8e65c3
--- /dev/null
+++ b/src/common/haiku/version.cpp
@@ -0,0 +1,26 @@
+extern "C" {
+#include "version.h"
+}
+
+#include <File.h>
+#include <AppFileInfo.h>
+
+bool ffGetFileVersion(const char* filePath, FFstrbuf* version) {
+ BFile f(filePath, B_READ_ONLY);
+ if (f.InitCheck() != B_OK) {
+ return false;
+ }
+
+ BAppFileInfo fileInfo(&f);
+ if (f.InitCheck() != B_OK) {
+ return false;
+ }
+
+ version_info info;
+ if (fileInfo.GetVersionInfo(&info, B_SYSTEM_VERSION_KIND) != B_OK) {
+ return false;
+ }
+
+ ffStrbufSetF(version, "%d.%d.%d", (int) info.major, (int) info.middle, (int) info.minor);
+ return true;
+}
diff --git a/src/common/haiku/version.h b/src/common/haiku/version.h
new file mode 100644
index 0000000..4f047a2
--- /dev/null
+++ b/src/common/haiku/version.h
@@ -0,0 +1,3 @@
+#include "common/FFstrbuf.h"
+
+bool ffGetFileVersion(const char* filePath, FFstrbuf* version);
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;
+ }
+}
diff --git a/src/common/init.h b/src/common/init.h
new file mode 100644
index 0000000..c2ea4c0
--- /dev/null
+++ b/src/common/init.h
@@ -0,0 +1,7 @@
+#pragma once
+
+void ffInitInstance(void);
+void ffStart(void);
+void ffFinish(void);
+void ffDestroyInstance(void);
+void ffListFeatures(void);
diff --git a/src/common/io.h b/src/common/io.h
new file mode 100644
index 0000000..f7aec0b
--- /dev/null
+++ b/src/common/io.h
@@ -0,0 +1,300 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+#include "common/FFlist.h"
+
+#ifdef _WIN32
+ #include <fileapi.h>
+ #include <handleapi.h>
+ #include <io.h>
+ #include "common/windows/nt.h"
+typedef HANDLE FFNativeFD;
+ #define FF_INVALID_FD INVALID_HANDLE_VALUE
+#else
+ #include <unistd.h>
+ #include <dirent.h>
+ #include <sys/stat.h>
+ #include <errno.h>
+ #include <limits.h>
+ #include <fcntl.h>
+typedef int FFNativeFD;
+ #define FF_INVALID_FD (-1)
+ // procfs's file can be changed between read calls such as /proc/meminfo and /proc/uptime.
+ // one safe way to read correct data is reading the whole file in a single read syscall
+ #define PROC_FILE_BUFFSIZ (32 * 1024)
+#endif
+
+#ifdef _WIN32
+ #ifndef O_CLOEXEC
+ #define O_CLOEXEC 0
+ #endif
+ #ifndef O_RDONLY
+ #define O_RDONLY 0
+ #endif
+ #ifndef O_DIRECTORY
+ #define O_DIRECTORY 0200000
+ #endif
+
+// Only O_RDONLY is supported
+HANDLE openat(HANDLE dfd, const char* fileName, int oflag);
+HANDLE openatW(HANDLE dfd, const wchar_t* fileName, uint16_t fileNameLen, bool directory);
+#endif
+
+static inline bool ffIsValidNativeFD(FFNativeFD fd) {
+#ifndef _WIN32
+ return fd >= 0;
+#else
+ // https://devblogs.microsoft.com/oldnewthing/20040302-00/?p=40443
+ return fd != INVALID_HANDLE_VALUE && fd != NULL;
+#endif
+}
+
+FF_A_NONNULL(1) static inline bool wrapClose(FFNativeFD* pfd) {
+ assert(pfd);
+
+ if (!ffIsValidNativeFD(*pfd)) {
+ return false;
+ }
+
+#ifndef _WIN32
+ close(*pfd);
+#else
+ NtClose(*pfd);
+#endif
+
+ return true;
+}
+#define FF_AUTO_CLOSE_FD FF_A_CLEANUP(wrapClose)
+
+static inline FFNativeFD FFUnixFD2NativeFD(int unixfd) {
+#ifndef _WIN32
+ return unixfd;
+#else
+ return (FFNativeFD) _get_osfhandle(unixfd);
+#endif
+}
+
+FF_A_NONNULL(3) static inline bool ffWriteFDData(FFNativeFD fd, size_t dataSize, const void* data) {
+#ifndef _WIN32
+ return write(fd, data, dataSize) != -1;
+#else
+ DWORD written;
+ return WriteFile(fd, data, (DWORD) dataSize, &written, NULL) && written == dataSize;
+#endif
+}
+
+FF_A_NONNULL(2) static inline bool ffWriteFDBuffer(FFNativeFD fd, const FFstrbuf* content) {
+ return ffWriteFDData(fd, content->length, content->chars);
+}
+
+FF_A_NONNULL(1, 3) bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data);
+
+FF_A_NONNULL(1, 2) static inline bool ffWriteFileBuffer(const char* fileName, const FFstrbuf* buffer) {
+ return ffWriteFileData(fileName, buffer->length, buffer->chars);
+}
+
+FF_A_NONNULL(3) static inline ssize_t ffReadFDData(FFNativeFD fd, size_t dataSize, void* data) {
+#ifndef _WIN32
+ return read(fd, data, dataSize);
+#else
+ DWORD bytesRead;
+ if (!ReadFile(fd, data, (DWORD) dataSize, &bytesRead, NULL)) {
+ return -1;
+ }
+
+ return (ssize_t) bytesRead;
+#endif
+}
+
+FF_A_NONNULL(2) bool ffAppendFDBuffer(FFNativeFD fd, FFstrbuf* buffer);
+
+FF_A_NONNULL(1, 3) static inline ssize_t ffReadFileData(const char* fileName, size_t dataSize, void* data) {
+ FFNativeFD FF_AUTO_CLOSE_FD fd =
+#ifndef _WIN32
+ open(fileName, O_RDONLY | O_CLOEXEC);
+#else
+ CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+#endif
+
+ if (!ffIsValidNativeFD(fd)) {
+ return -1;
+ }
+
+ return ffReadFDData(fd, dataSize, data);
+}
+
+FF_A_NONNULL(2, 4) static inline ssize_t ffReadFileDataRelative(FFNativeFD dfd, const char* fileName, size_t dataSize, void* data) {
+ FFNativeFD FF_AUTO_CLOSE_FD fd = openat(dfd, fileName, O_RDONLY | O_CLOEXEC);
+ if (!ffIsValidNativeFD(fd)) {
+ return -1;
+ }
+
+ return ffReadFDData(fd, dataSize, data);
+}
+
+FF_A_NONNULL(1, 2) static inline bool ffAppendFileBuffer(const char* fileName, FFstrbuf* buffer) {
+ FFNativeFD FF_AUTO_CLOSE_FD fd =
+#ifndef _WIN32
+ open(fileName, O_RDONLY | O_CLOEXEC);
+#else
+ CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
+#endif
+
+ if (!ffIsValidNativeFD(fd)) {
+ return false;
+ }
+
+ return ffAppendFDBuffer(fd, buffer);
+}
+
+FF_A_NONNULL(2, 3) static inline bool ffAppendFileBufferRelative(FFNativeFD dfd, const char* fileName, FFstrbuf* buffer) {
+ FFNativeFD FF_AUTO_CLOSE_FD fd = openat(dfd, fileName, O_RDONLY | O_CLOEXEC);
+ if (!ffIsValidNativeFD(fd)) {
+ return false;
+ }
+
+ return ffAppendFDBuffer(fd, buffer);
+}
+
+FF_A_NONNULL(2) static inline bool ffReadFDBuffer(FFNativeFD fd, FFstrbuf* buffer) {
+ ffStrbufClear(buffer);
+ return ffAppendFDBuffer(fd, buffer);
+}
+
+FF_A_NONNULL(1, 2) static inline bool ffReadFileBuffer(const char* fileName, FFstrbuf* buffer) {
+ ffStrbufClear(buffer);
+ return ffAppendFileBuffer(fileName, buffer);
+}
+
+FF_A_NONNULL(2, 3) static inline bool ffReadFileBufferRelative(FFNativeFD dfd, const char* fileName, FFstrbuf* buffer) {
+ ffStrbufClear(buffer);
+ return ffAppendFileBufferRelative(dfd, fileName, buffer);
+}
+
+typedef enum FF_A_PACKED FFPathType {
+ FF_PATHTYPE_FILE = 1 << 0,
+ FF_PATHTYPE_DIRECTORY = 1 << 1,
+ FF_PATHTYPE_ANY = FF_PATHTYPE_FILE | FF_PATHTYPE_DIRECTORY,
+ FF_PATHTYPE_FORCE_UNSIGNED = UINT8_MAX,
+} FFPathType;
+
+FF_A_NONNULL(1) static inline bool ffPathExists(const char* path, FFPathType pathType) {
+#ifdef _WIN32
+
+ wchar_t wPath[MAX_PATH];
+ if (!NT_SUCCESS(RtlUTF8ToUnicodeN(wPath, (ULONG) sizeof(wPath), NULL, path, (ULONG) strlen(path) + 1))) {
+ return false;
+ }
+
+ DWORD attr = GetFileAttributesW(wPath);
+
+ if (attr == INVALID_FILE_ATTRIBUTES) {
+ return false;
+ }
+
+ if (pathType & FF_PATHTYPE_FILE && !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
+ return true;
+ }
+
+ if (pathType & FF_PATHTYPE_DIRECTORY && (attr & FILE_ATTRIBUTE_DIRECTORY)) {
+ return true;
+ }
+
+#else
+
+ if (pathType == FF_PATHTYPE_ANY) {
+ // Zero overhead
+ return access(path, F_OK) == 0;
+ } else {
+ struct stat fileStat;
+ if (stat(path, &fileStat) != 0) {
+ return false;
+ }
+
+ unsigned int mode = fileStat.st_mode & S_IFMT;
+
+ if (pathType & FF_PATHTYPE_FILE && mode != S_IFDIR) {
+ return true;
+ }
+
+ if (pathType & FF_PATHTYPE_DIRECTORY && mode == S_IFDIR) {
+ return true;
+ }
+ }
+
+#endif
+
+ return false;
+}
+
+FF_A_NONNULL(1, 2) bool ffPathExpandEnv(const char* in, FFstrbuf* out);
+
+#define FF_IO_TERM_RESP_WAIT_MS 100 // #554
+
+FF_A_SCANF(3, 4)
+FF_A_NONNULL(1, 3)
+const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...);
+
+// Not thread safe!
+bool ffSuppressIO(bool suppress);
+
+static inline void ffUnsuppressIO(bool* suppressed) {
+ if (!*suppressed) {
+ return;
+ }
+ ffSuppressIO(false);
+ *suppressed = false;
+}
+
+#define FF_SUPPRESS_IO() bool FF_A_CLEANUP(ffUnsuppressIO) FF_A_UNUSED io_suppressed__ = ffSuppressIO(true)
+
+void ffListFilesRecursively(const char* path, bool pretty);
+
+FF_A_NONNULL(1) static inline bool wrapFclose(FILE** pfile) {
+ assert(pfile);
+ if (!*pfile) {
+ return false;
+ }
+ fclose(*pfile);
+ return true;
+}
+#define FF_AUTO_CLOSE_FILE FF_A_CLEANUP(wrapFclose)
+
+FF_A_NONNULL(1)
+#ifndef _WIN32
+static inline bool wrapClosedir(DIR** pdir) {
+ assert(pdir);
+ if (!*pdir) {
+ return false;
+ }
+ closedir(*pdir);
+ return true;
+}
+#else
+static inline bool wrapClosedir(HANDLE* pdir) {
+ assert(pdir);
+ if (!*pdir) {
+ return false;
+ }
+ FindClose(*pdir);
+ return true;
+}
+#endif
+#define FF_AUTO_CLOSE_DIR FF_A_CLEANUP(wrapClosedir)
+
+FF_A_NONNULL(1, 2, 3) static inline bool ffSearchUserConfigFile(const FFlist* configDirs, const char* fileSubpath, FFstrbuf* result) {
+ // configDirs is a list of FFstrbufs include the trailing slash
+ FF_LIST_FOR_EACH (FFstrbuf, dir, *configDirs) {
+ ffStrbufClear(result);
+ ffStrbufAppend(result, dir);
+ ffStrbufAppendS(result, fileSubpath);
+ if (ffPathExists(result->chars, FF_PATHTYPE_FILE)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+FFNativeFD ffGetNullFD(void);
+bool ffRemoveFile(const char* fileName);
diff --git a/src/common/jsonconfig.h b/src/common/jsonconfig.h
new file mode 100644
index 0000000..a78aafd
--- /dev/null
+++ b/src/common/jsonconfig.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "common/ffdata.h"
+#include "common/option.h"
+
+bool ffJsonConfigParseModuleArgs(yyjson_val* key, yyjson_val* val, FFModuleArgs* moduleArgs);
+const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]);
+
+yyjson_api_inline yyjson_mut_val* yyjson_mut_strbuf(yyjson_mut_doc* doc, const FFstrbuf* buf) {
+ return yyjson_mut_strncpy(doc, buf->chars, buf->length);
+}
+
+yyjson_api_inline bool yyjson_mut_obj_add_strbuf(yyjson_mut_doc* doc,
+ yyjson_mut_val* obj,
+ const char* _key,
+ const FFstrbuf* buf) {
+ return yyjson_mut_obj_add_strncpy(doc, obj, _key, buf->chars, buf->length);
+}
+
+yyjson_api_inline bool yyjson_mut_arr_add_strbuf(yyjson_mut_doc* doc,
+ yyjson_mut_val* obj,
+ const FFstrbuf* buf) {
+ return yyjson_mut_arr_add_strncpy(doc, obj, buf->chars, buf->length);
+}
+
+void ffPrintJsonConfig(FFdata* data, bool prepare);
+void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFModuleArgs* moduleArgs);
diff --git a/src/common/kmod.h b/src/common/kmod.h
new file mode 100644
index 0000000..013340b
--- /dev/null
+++ b/src/common/kmod.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "fastfetch.h"
+
+bool ffKmodLoaded(const char* modName);
diff --git a/src/common/library.h b/src/common/library.h
new file mode 100644
index 0000000..97500ac
--- /dev/null
+++ b/src/common/library.h
@@ -0,0 +1,109 @@
+#pragma once
+
+#include "fastfetch.h"
+
+#ifndef FF_DISABLE_DLOPEN
+
+ #if defined(_WIN32)
+ #define FF_DLOPEN_FLAGS 0
+FF_A_NODISCARD void* dlopen(const char* path, int mode);
+FF_A_NODISCARD void* dlsym(void* handle, const char* symbol);
+int dlclose(void* handle);
+ #else
+ #include <dlfcn.h>
+ #endif
+
+ #ifdef _WIN32
+ #define FF_LIBRARY_EXTENSION ".dll"
+ #elif defined(__APPLE__)
+ #define FF_LIBRARY_EXTENSION ".dylib"
+ #else
+ #define FF_LIBRARY_EXTENSION ".so"
+ #endif
+
+static inline void ffLibraryUnload(void** handle) {
+ assert(handle);
+ if (*handle) {
+ dlclose(*handle);
+ }
+}
+
+ #if __cplusplus
+ #define __auto_type auto
+ #endif
+
+ #define FF_LIBRARY_SYMBOL(symbolName) \
+ __typeof__(&symbolName) ff##symbolName;
+
+ #define FF_LIBRARY_LOAD(libraryObjectName, returnValue, ...) \
+ void* FF_A_CLEANUP(ffLibraryUnload) libraryObjectName = ffLibraryLoad(__VA_ARGS__, NULL); \
+ if (libraryObjectName == NULL) \
+ return returnValue;
+
+ #define FF_LIBRARY_LOAD_MESSAGE(libraryObjectName, libraryFileName, maxVersion, ...) \
+ FF_LIBRARY_LOAD(libraryObjectName, "dlopen(" libraryFileName ") failed", libraryFileName, maxVersion, ##__VA_ARGS__)
+
+ #define FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, symbolMapping, symbolName, returnValue) \
+ symbolMapping = (__typeof__(&symbolName)) dlsym(library, #symbolName); \
+ if (symbolMapping == NULL) \
+ return returnValue;
+
+ #define FF_LIBRARY_LOAD_SYMBOL(library, symbolName, returnValue) \
+ __auto_type FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, ff##symbolName, symbolName, returnValue);
+
+ #define FF_LIBRARY_LOAD_SYMBOL_LAZY(library, symbolName) \
+ __auto_type ff##symbolName = (__typeof__(&symbolName)) dlsym(library, #symbolName);
+
+ #define FF_LIBRARY_LOAD_SYMBOL_MESSAGE(library, symbolName) \
+ __auto_type FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, ff##symbolName, symbolName, "dlsym " #symbolName " failed");
+
+ #define FF_LIBRARY_LOAD_SYMBOL_VAR(library, varName, symbolName, returnValue) \
+ FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, (varName).ff##symbolName, symbolName, returnValue);
+
+ #define FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(library, varName, symbolName) \
+ FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, (varName).ff##symbolName, symbolName, "dlsym " #symbolName " failed");
+
+ #define FF_LIBRARY_LOAD_SYMBOL_PTR(library, varName, symbolName, returnValue) \
+ FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, (varName)->ff##symbolName, symbolName, returnValue);
+
+void* ffLibraryLoad(const char* path, int maxVersion, ...);
+
+#else
+
+ #define FF_LIBRARY_EXTENSION ""
+
+ #define FF_LIBRARY_SYMBOL(symbolName) \
+ __typeof__(&symbolName) ff##symbolName;
+
+ #define FF_LIBRARY_LOAD(libraryObjectName, returnValue, ...) \
+ FF_A_UNUSED void* libraryObjectName = NULL; // Placeholder
+
+ #define FF_LIBRARY_LOAD_MESSAGE(libraryObjectName, libraryFileName, maxVersion, ...) \
+ FF_LIBRARY_LOAD(libraryObjectName, , libraryFileName, maxVersion, ##__VA_ARGS__)
+
+ #define FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, symbolMapping, symbolName, returnValue) \
+ symbolMapping = (__typeof__(&symbolName)) &symbolName;
+
+ #define FF_LIBRARY_LOAD_SYMBOL(library, symbolName, returnValue) \
+ FF_A_UNUSED __auto_type FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, ff##symbolName, symbolName, returnValue);
+
+ #define FF_LIBRARY_LOAD_SYMBOL_LAZY(library, symbolName) \
+ FF_A_UNUSED __auto_type ff##symbolName = (__typeof__(&symbolName)) &symbolName;
+
+ #define FF_LIBRARY_LOAD_SYMBOL_MESSAGE(library, symbolName) \
+ FF_A_UNUSED __auto_type FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, ff##symbolName, symbolName, "dlsym " #symbolName " failed");
+
+ #define FF_LIBRARY_LOAD_SYMBOL_VAR(library, varName, symbolName, returnValue) \
+ FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, (varName).ff##symbolName, symbolName, returnValue);
+
+ #define FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(library, varName, symbolName) \
+ FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, (varName).ff##symbolName, symbolName, "dlsym " #symbolName " failed");
+
+ #define FF_LIBRARY_LOAD_SYMBOL_PTR(library, varName, symbolName, returnValue) \
+ FF_LIBRARY_LOAD_SYMBOL_ADDRESS(library, (varName)->ff##symbolName, symbolName, returnValue);
+
+#endif
+
+#if _WIN32
+void* ffLibraryGetModule(const wchar_t* libraryFileName);
+#endif
diff --git a/src/common/lua.h b/src/common/lua.h
new file mode 100644
index 0000000..06343c4
--- /dev/null
+++ b/src/common/lua.h
@@ -0,0 +1,187 @@
+#pragma once
+
+#include "fastfetch.h"
+
+#if FF_HAVE_LUA
+ #if !FF_DISABLE_DLOPEN
+ // Hack. LUA_API is defined as extern which prevents us from implementing the functions ourselves.
+ #include <luaconf.h>
+ #undef LUA_API
+ #undef LUALIB_API
+ #undef LUAMOD_API
+ #define LUA_API static inline
+ #define LUALIB_API LUA_API
+ #define LUAMOD_API LUA_API
+
+ #pragma GCC diagnostic ignored "-Wunused-function"
+ #endif
+
+ #include <lua.h>
+ #include <lauxlib.h>
+ #include <lualib.h>
+
+ #ifndef LUA_GNAME
+ #define LUA_GNAME "_G"
+ #endif
+
+ #include "common/library.h"
+
+extern struct FFLuaData {
+ #if !FF_DISABLE_DLOPEN
+ FF_LIBRARY_SYMBOL(luaL_checkany)
+ FF_LIBRARY_SYMBOL(luaL_loadbufferx)
+ FF_LIBRARY_SYMBOL(luaL_tolstring)
+ FF_LIBRARY_SYMBOL(lua_callk)
+ FF_LIBRARY_SYMBOL(lua_createtable)
+ FF_LIBRARY_SYMBOL(lua_error)
+ FF_LIBRARY_SYMBOL(lua_gettop)
+ FF_LIBRARY_SYMBOL(lua_isinteger)
+ FF_LIBRARY_SYMBOL(lua_next)
+ FF_LIBRARY_SYMBOL(lua_pcallk)
+ FF_LIBRARY_SYMBOL(lua_pushboolean)
+ FF_LIBRARY_SYMBOL(lua_pushcclosure)
+ FF_LIBRARY_SYMBOL(lua_pushinteger)
+ FF_LIBRARY_SYMBOL(lua_pushlstring)
+ FF_LIBRARY_SYMBOL(lua_pushnil)
+ FF_LIBRARY_SYMBOL(lua_pushnumber)
+ FF_LIBRARY_SYMBOL(lua_pushvalue)
+ FF_LIBRARY_SYMBOL(lua_rawgeti)
+ FF_LIBRARY_SYMBOL(lua_rawlen)
+ FF_LIBRARY_SYMBOL(lua_setfield)
+ FF_LIBRARY_SYMBOL(lua_setglobal)
+ FF_LIBRARY_SYMBOL(lua_seti)
+ FF_LIBRARY_SYMBOL(lua_settop)
+ FF_LIBRARY_SYMBOL(lua_toboolean)
+ FF_LIBRARY_SYMBOL(lua_tointegerx)
+ FF_LIBRARY_SYMBOL(lua_tolstring)
+ FF_LIBRARY_SYMBOL(lua_tonumberx)
+ FF_LIBRARY_SYMBOL(lua_type)
+ #endif
+
+ lua_State* L;
+ bool inited;
+} luaData;
+
+ #if !FF_DISABLE_DLOPEN
+FF_A_ALWAYS_INLINE void(lua_settop)(lua_State* L, int idx) {
+ return luaData.fflua_settop(L, idx);
+}
+
+FF_A_ALWAYS_INLINE int(luaL_loadbufferx)(lua_State* L, const char* buff, size_t sz, const char* name, const char* mode) {
+ return luaData.ffluaL_loadbufferx(L, buff, sz, name, mode);
+}
+
+FF_A_ALWAYS_INLINE const char*(lua_tolstring) (lua_State * L, int idx, size_t* len) {
+ return luaData.fflua_tolstring(L, idx, len);
+}
+
+FF_A_ALWAYS_INLINE void(lua_createtable)(lua_State* L, int narr, int nrec) {
+ return luaData.fflua_createtable(L, narr, nrec);
+}
+
+FF_A_ALWAYS_INLINE void(lua_pushinteger)(lua_State* L, lua_Integer n) {
+ return luaData.fflua_pushinteger(L, n);
+}
+
+FF_A_ALWAYS_INLINE void(lua_pushnumber)(lua_State* L, lua_Number n) {
+ return luaData.fflua_pushnumber(L, n);
+}
+
+FF_A_ALWAYS_INLINE void(lua_pushboolean)(lua_State* L, int b) {
+ return luaData.fflua_pushboolean(L, b);
+}
+
+FF_A_ALWAYS_INLINE const char*(lua_pushlstring) (lua_State * L, const char* s, size_t len) {
+ return luaData.fflua_pushlstring(L, s, len);
+}
+
+FF_A_ALWAYS_INLINE void(lua_pushvalue)(lua_State* L, int idx) {
+ return luaData.fflua_pushvalue(L, idx);
+}
+
+FF_A_ALWAYS_INLINE void(lua_seti)(lua_State* L, int idx, lua_Integer n) {
+ return luaData.fflua_seti(L, idx, n);
+}
+
+FF_A_ALWAYS_INLINE void(lua_pushnil)(lua_State* L) {
+ return luaData.fflua_pushnil(L);
+}
+
+FF_A_ALWAYS_INLINE void(lua_setfield)(lua_State* L, int idx, const char* k) {
+ return luaData.fflua_setfield(L, idx, k);
+}
+
+FF_A_ALWAYS_INLINE int(lua_pcallk)(lua_State* L, int nargs, int nresults, int errfunc, lua_KContext ctx, lua_KFunction k) {
+ return luaData.fflua_pcallk(L, nargs, nresults, errfunc, ctx, k);
+}
+
+FF_A_ALWAYS_INLINE int(lua_gettop)(lua_State* L) {
+ return luaData.fflua_gettop(L);
+}
+
+FF_A_ALWAYS_INLINE const char*(luaL_tolstring) (lua_State * L, int idx, size_t* len) {
+ return luaData.ffluaL_tolstring(L, idx, len);
+}
+
+FF_A_ALWAYS_INLINE int(lua_error)(lua_State* L) {
+ return luaData.fflua_error(L);
+}
+
+FF_A_ALWAYS_INLINE void(lua_pushcclosure)(lua_State* L, lua_CFunction fn, int n) {
+ return luaData.fflua_pushcclosure(L, fn, n);
+}
+
+FF_A_ALWAYS_INLINE void(luaL_checkany)(lua_State* L, int idx) {
+ return luaData.ffluaL_checkany(L, idx);
+}
+
+FF_A_ALWAYS_INLINE void(lua_callk)(lua_State* L, int nargs, int nresults, lua_KContext ctx, lua_KFunction k) {
+ return luaData.fflua_callk(L, nargs, nresults, ctx, k);
+}
+
+FF_A_ALWAYS_INLINE int(lua_isinteger)(lua_State* L, int idx) {
+ return luaData.fflua_isinteger(L, idx);
+}
+
+FF_A_ALWAYS_INLINE int(lua_next)(lua_State* L, int idx) {
+ return luaData.fflua_next(L, idx);
+}
+
+FF_A_ALWAYS_INLINE int(lua_rawgeti)(lua_State* L, int idx, lua_Integer n) {
+ return luaData.fflua_rawgeti(L, idx, n);
+}
+
+FF_A_ALWAYS_INLINE
+ #if LUA_VERSION_NUM > 503
+lua_Unsigned
+ #else
+size_t
+ #endif
+ (lua_rawlen)(lua_State* L, int idx) {
+ return luaData.fflua_rawlen(L, idx);
+}
+
+FF_A_ALWAYS_INLINE void(lua_setglobal)(lua_State* L, const char* name) {
+ return luaData.fflua_setglobal(L, name);
+}
+
+FF_A_ALWAYS_INLINE int(lua_toboolean)(lua_State* L, int idx) {
+ return luaData.fflua_toboolean(L, idx);
+}
+
+FF_A_ALWAYS_INLINE lua_Integer(lua_tointegerx)(lua_State* L, int idx, int* isnum) {
+ return luaData.fflua_tointegerx(L, idx, isnum);
+}
+
+FF_A_ALWAYS_INLINE lua_Number(lua_tonumberx)(lua_State* L, int idx, int* isnum) {
+ return luaData.fflua_tonumberx(L, idx, isnum);
+}
+
+FF_A_ALWAYS_INLINE int(lua_type)(lua_State* L, int idx) {
+ return luaData.fflua_type(L, idx);
+}
+ #endif
+
+const char* ffLuaLoadState(void);
+
+#endif
diff --git a/src/common/mallocHelper.h b/src/common/mallocHelper.h
new file mode 100644
index 0000000..5b24df4
--- /dev/null
+++ b/src/common/mallocHelper.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <stdlib.h>
+#include <assert.h>
+
+#if FF_HAVE_MALLOC_USABLE_SIZE || FF_HAVE_MSVC_MSIZE
+ #if __has_include(<malloc.h>)
+ #include <malloc.h>
+ #else
+ #include <malloc_np.h> // For DragonFly BSD
+ #endif
+#elif FF_HAVE_MALLOC_SIZE
+ #include <malloc/malloc.h>
+#endif
+
+static inline void ffWrapFree(const void* pPtr) {
+ assert(pPtr);
+ if (*(void**) pPtr) {
+ free(*(void**) pPtr);
+ }
+}
+
+#define FF_AUTO_FREE FF_A_CLEANUP(ffWrapFree)
+
+// ptr MUST be a malloc'ed pointer
+static inline size_t ffMallocUsableSize(const void* ptr) {
+ assert(ptr);
+#if FF_HAVE_MALLOC_USABLE_SIZE
+ return malloc_usable_size((void*) ptr);
+#elif FF_HAVE_MALLOC_SIZE
+ return malloc_size((void*) ptr);
+#elif FF_HAVE_MSVC_MSIZE
+ return _msize((void*) ptr);
+#else
+ (void) ptr;
+ return 0; // Not supported
+#endif
+}
diff --git a/src/common/memrchr.h b/src/common/memrchr.h
new file mode 100644
index 0000000..6905a33
--- /dev/null
+++ b/src/common/memrchr.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// `memrchr` is a GNU extension and may not be declared by system headers even when the symbol exists.
+// Declare it unconditionally; the build system provides a fallback implementation when missing.
+void* memrchr(const void* s, int c, size_t n);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/common/netif.h b/src/common/netif.h
new file mode 100644
index 0000000..d3984df
--- /dev/null
+++ b/src/common/netif.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include "fastfetch.h"
+
+#ifndef _WIN32
+ #include <net/if.h>
+ #include <netinet/in.h>
+#endif
+
+typedef enum FF_A_PACKED FFNetifDefaultRouteResultStatus {
+ FF_NETIF_UNINITIALIZED,
+ FF_NETIF_INVALID,
+ FF_NETIF_OK
+} FFNetifDefaultRouteResultStatus;
+
+typedef struct FFNetifDefaultRouteResult {
+ uint32_t ifIndex;
+
+#ifndef _WIN32
+ char ifName[IF_NAMESIZE + 1];
+ uint32_t preferredSourceAddrV4;
+#endif
+ enum FFNetifDefaultRouteResultStatus status;
+} FFNetifDefaultRouteResult;
+
+bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result);
+bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result);
+
+const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV4(void);
+const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV6(void);
diff --git a/src/common/networking.h b/src/common/networking.h
new file mode 100644
index 0000000..2554e22
--- /dev/null
+++ b/src/common/networking.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "common/thread.h"
+#include "common/FFstrbuf.h"
+
+#ifdef _WIN32
+ #include <minwindef.h>
+#endif
+
+struct addrinfo;
+
+typedef struct FFNetworkingState {
+#ifdef _WIN32
+ uintptr_t sockfd;
+ OVERLAPPED overlapped;
+#else
+ int sockfd;
+ struct addrinfo* addr;
+
+ #ifdef FF_HAVE_THREADS
+ FFThreadType thread;
+ #endif
+#endif
+
+ FFstrbuf command;
+ uint32_t timeout;
+ bool ipv6;
+ bool compression; // if true, HTTP content compression will be enabled if supported
+ bool tfo; // if true, TCP Fast Open will be attempted first, and fallback to traditional connection if it fails
+} FFNetworkingState;
+
+const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers);
+const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer);
+
+#ifdef FF_HAVE_ZLIB
+const char* ffNetworkingLoadZlibLibrary(void);
+bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd);
+#endif
diff --git a/src/common/option.h b/src/common/option.h
new file mode 100644
index 0000000..ce0a0ab
--- /dev/null
+++ b/src/common/option.h
@@ -0,0 +1,96 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+
+struct yyjson_val;
+struct yyjson_mut_doc;
+struct yyjson_mut_val;
+
+typedef struct FFModuleFormatArg {
+ const char* desc;
+ const char* name;
+} FFModuleFormatArg;
+
+typedef struct FFModuleFormatArgList {
+ FFModuleFormatArg* args;
+ uint32_t count;
+} FFModuleFormatArgList;
+
+#define FF_FORMAT_ARG_LIST(list) { .args = list, .count = sizeof(list) / sizeof(FFModuleFormatArg) }
+
+// Must be the first field of FFModuleOptions
+typedef struct FFModuleBaseInfo {
+ const char* name;
+ const char* description;
+ // A dirty polymorphic implementation in C.
+ // This is UB, because `void*` is not compatible with `FF*Options*`.
+ // However we can't do it better unless we move to C++, so that `option` becomes a `this` pointer
+ // https://stackoverflow.com/questions/559581/casting-a-function-pointer-to-another-type
+
+ void (*initOptions)(void* options);
+ void (*destroyOptions)(void* options);
+ void (*parseJsonObject)(void* options, struct yyjson_val* module);
+ bool (*printModule)(void* options); // true on success
+ bool (*generateJsonResult)(void* options, struct yyjson_mut_doc* doc, struct yyjson_mut_val* module); // true on success
+ void (*generateJsonConfig)(void* options, struct yyjson_mut_doc* doc, struct yyjson_mut_val* obj);
+ FFModuleFormatArgList formatArgs;
+} FFModuleBaseInfo;
+
+typedef enum FF_A_PACKED FFModuleKeyType {
+ FF_MODULE_KEY_TYPE_NONE = 0,
+ FF_MODULE_KEY_TYPE_STRING = 1 << 0,
+ FF_MODULE_KEY_TYPE_ICON = 1 << 1,
+ FF_MODULE_KEY_TYPE_SPACE_SHIFT = 4,
+ FF_MODULE_KEY_TYPE_BOTH_0 = FF_MODULE_KEY_TYPE_STRING | FF_MODULE_KEY_TYPE_ICON,
+ FF_MODULE_KEY_TYPE_BOTH_1 = FF_MODULE_KEY_TYPE_BOTH_0 | (1 << FF_MODULE_KEY_TYPE_SPACE_SHIFT),
+ FF_MODULE_KEY_TYPE_BOTH = FF_MODULE_KEY_TYPE_BOTH_1, // alias
+ FF_MODULE_KEY_TYPE_BOTH_2 = FF_MODULE_KEY_TYPE_BOTH_0 | (2 << FF_MODULE_KEY_TYPE_SPACE_SHIFT),
+ FF_MODULE_KEY_TYPE_BOTH_3 = FF_MODULE_KEY_TYPE_BOTH_0 | (3 << FF_MODULE_KEY_TYPE_SPACE_SHIFT),
+ FF_MODULE_KEY_TYPE_BOTH_4 = FF_MODULE_KEY_TYPE_BOTH_0 | (4 << FF_MODULE_KEY_TYPE_SPACE_SHIFT),
+ FF_MODULE_KEY_TYPE_FORCE_UNSIGNED = UINT8_MAX,
+} FFModuleKeyType;
+
+typedef struct FFModuleArgs {
+ FFstrbuf key;
+ FFstrbuf keyColor;
+ FFstrbuf keyIcon;
+ FFstrbuf outputFormat;
+ FFstrbuf outputColor;
+ uint32_t keyWidth;
+} FFModuleArgs;
+
+typedef struct FFKeyValuePair {
+ const char* key;
+ int value;
+} FFKeyValuePair;
+
+const char* ffOptionTestPrefix(const char* argumentKey, const char* moduleName);
+void ffOptionParseString(const char* argumentKey, const char* value, FFstrbuf* buffer);
+FF_A_NODISCARD uint32_t ffOptionParseUInt32(const char* argumentKey, const char* value);
+FF_A_NODISCARD int32_t ffOptionParseInt32(const char* argumentKey, const char* value);
+FF_A_NODISCARD int ffOptionParseEnum(const char* argumentKey, const char* requestedKey, FFKeyValuePair pairs[]);
+FF_A_NODISCARD bool ffOptionParseBoolean(const char* str);
+void ffOptionParseColorNoClear(const char* value, FFstrbuf* buffer);
+static inline void ffOptionParseColor(const char* value, FFstrbuf* buffer) {
+ ffStrbufClear(buffer);
+ ffOptionParseColorNoClear(value, buffer);
+}
+
+static inline void ffOptionInitModuleArg(FFModuleArgs* args, const char* icon) {
+ ffStrbufInit(&args->key);
+ ffStrbufInit(&args->keyColor);
+ ffStrbufInitStatic(&args->keyIcon, icon);
+ ffStrbufInit(&args->outputFormat);
+ ffStrbufInit(&args->outputColor);
+ args->keyWidth = 0;
+}
+
+static inline void ffOptionDestroyModuleArg(FFModuleArgs* args) {
+ ffStrbufDestroy(&args->key);
+ ffStrbufDestroy(&args->keyColor);
+ ffStrbufDestroy(&args->keyIcon);
+ ffStrbufDestroy(&args->outputFormat);
+ ffStrbufDestroy(&args->outputColor);
+}
+
+enum { FF_OPTION_MAX_SIZE = 1 << 8 }; // Maximum size of a single option value, used for static allocation
diff --git a/src/common/parsing.h b/src/common/parsing.h
new file mode 100644
index 0000000..c3a4bee
--- /dev/null
+++ b/src/common/parsing.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+
+#include <stdint.h>
+
+typedef struct FFVersion {
+ uint32_t major;
+ uint32_t minor;
+ uint32_t patch;
+} FFVersion;
+
+typedef struct FFColorRangeConfig {
+ uint8_t green;
+ uint8_t yellow;
+} FFColorRangeConfig;
+
+#define FF_VERSION_INIT ((FFVersion) { 0 })
+
+void ffParseSemver(FFstrbuf* buffer, const FFstrbuf* major, const FFstrbuf* minor, const FFstrbuf* patch);
+void ffParseGTK(FFstrbuf* buffer, const FFstrbuf* gtk2, const FFstrbuf* gtk3, const FFstrbuf* gtk4);
+
+void ffVersionToPretty(const FFVersion* version, FFstrbuf* pretty);
+int8_t ffVersionCompare(const FFVersion* version1, const FFVersion* version2);
diff --git a/src/common/path.h b/src/common/path.h
new file mode 100644
index 0000000..705b1c0
--- /dev/null
+++ b/src/common/path.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+#include "common/strutil.h"
+
+const char* ffFindExecutableInPath(const char* name, FFstrbuf* result);
+static inline bool ffIsAbsolutePath(const char* path) {
+#ifdef _WIN32
+ return (ffCharIsEnglishAlphabet(path[0]) && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) // drive letter path
+ || (path[0] == '\\' && path[1] == '\\'); // UNC path
+#else
+ return path[0] == '/';
+#endif
+}
+
+#if _WIN32
+char* frealpath(void* __restrict hFile, char* __restrict resolved_name /*MAX_PATH*/);
+char* realpath(const char* __restrict file_name, char* __restrict resolved_name /*MAX_PATH*/);
+ssize_t freadlink(void* hFile, char* buf, size_t bufsiz);
+ssize_t readlink(const char* path, char* buf, size_t bufsiz);
+#endif
diff --git a/src/common/percent.h b/src/common/percent.h
new file mode 100644
index 0000000..c2eeba6
--- /dev/null
+++ b/src/common/percent.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+#include "common/parsing.h"
+#include "common/option.h"
+
+typedef enum FF_A_PACKED FFPercentageTypeFlags {
+ FF_PERCENTAGE_TYPE_NONE = 0,
+ FF_PERCENTAGE_TYPE_NUM_BIT = 1 << 0,
+ FF_PERCENTAGE_TYPE_BAR_BIT = 1 << 1,
+ FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT = 1 << 2,
+ FF_PERCENTAGE_TYPE_NUM_COLOR_BIT = 1 << 3,
+ FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT = 1 << 4,
+ FF_PERCENTAGE_TYPE_FORCE_UNSIGNED_ = UINT8_MAX,
+} FFPercentageTypeFlags;
+static_assert(sizeof(FFPercentageTypeFlags) == 1, "");
+
+typedef struct FFPercentageModuleConfig {
+ uint8_t green;
+ uint8_t yellow;
+ FFPercentageTypeFlags type;
+} FFPercentageModuleConfig;
+
+// if (green <= yellow)
+// [0, green]: print green
+// (green, yellow]: print yellow
+// (yellow, 100]: print red
+//
+// if (green > yellow)
+// [green, 100]: print green
+// [yellow, green): print yellow
+// [0, yellow): print red
+
+void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, const FFModuleArgs* module);
+void ffPercentAppendNum(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, bool parentheses, const FFModuleArgs* module);
+
+typedef struct yyjson_val yyjson_val;
+typedef struct yyjson_mut_doc yyjson_mut_doc;
+typedef struct yyjson_mut_val yyjson_mut_val;
+bool ffPercentParseCommandOptions(const char* key, const char* subkey, const char* value, FFPercentageModuleConfig* config);
+bool ffPercentParseJsonObject(yyjson_val* key, yyjson_val* value, FFPercentageModuleConfig* config);
+void ffPercentGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFPercentageModuleConfig config);
+const char* ffPercentParseTypeJsonConfig(yyjson_val* value, FFPercentageTypeFlags* result);
diff --git a/src/common/printing.h b/src/common/printing.h
new file mode 100644
index 0000000..f20a244
--- /dev/null
+++ b/src/common/printing.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "fastfetch.h"
+#include "common/format.h"
+
+typedef enum FF_A_PACKED FFPrintType {
+ FF_PRINT_TYPE_DEFAULT = 0,
+ FF_PRINT_TYPE_NO_CUSTOM_KEY = 1 << 0, // key has been formatted outside
+ FF_PRINT_TYPE_NO_CUSTOM_KEY_COLOR = 1 << 1,
+ FF_PRINT_TYPE_NO_CUSTOM_KEY_WIDTH = 1 << 2,
+ FF_PRINT_TYPE_NO_CUSTOM_OUTPUT_FORMAT = 1 << 3, // reserved
+ FF_PRINT_TYPE_FORCE_UNSIGNED = UINT8_MAX,
+} FFPrintType;
+
+void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType);
+bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments);
+#define FF_PRINT_FORMAT_CHECKED(moduleName, moduleIndex, moduleArgs, printType, arguments) \
+ ffPrintFormat((moduleName), (moduleIndex), (moduleArgs), (printType), (sizeof(arguments) / sizeof(*arguments)), (arguments));
+FF_A_PRINTF(5, 6) void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...);
+void ffPrintColor(const FFstrbuf* colorValue);
+void ffPrintCharTimes(char c, uint32_t times);
diff --git a/src/common/processing.h b/src/common/processing.h
new file mode 100644
index 0000000..51105c8
--- /dev/null
+++ b/src/common/processing.h
@@ -0,0 +1,55 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+
+#ifndef _WIN32
+ #include <sys/types.h> // pid_t
+#endif
+
+typedef struct FFProcessHandle {
+#if _WIN32
+ void* pid; // HANDLE
+ void* pipeRead; // HANDLE
+#else
+ pid_t pid;
+ int pipeRead;
+#endif
+} FFProcessHandle;
+
+const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle);
+const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer); // Destroys handle internally
+
+static inline const char* ffProcessAppendStdOut(FFstrbuf* buffer, char* const argv[]) {
+ FFProcessHandle handle;
+ const char* error = ffProcessSpawn(argv, false, &handle);
+ if (error) {
+ return error;
+ }
+
+ error = ffProcessReadOutput(&handle, buffer);
+ if (!error) {
+ ffStrbufTrimRightSpace(buffer);
+ }
+ return error;
+}
+
+static inline const char* ffProcessAppendStdErr(FFstrbuf* buffer, char* const argv[]) {
+ FFProcessHandle handle;
+ const char* error = ffProcessSpawn(argv, true, &handle);
+ if (error) {
+ return error;
+ }
+
+ error = ffProcessReadOutput(&handle, buffer);
+ if (!error) {
+ ffStrbufTrimRightSpace(buffer);
+ }
+ return error;
+}
+
+#ifdef _WIN32
+bool ffProcessGetInfoWindows(uint32_t pid, uint32_t* ppid, FFstrbuf* pname, FFstrbuf* exe, const char** exeName, FFstrbuf* exePath, bool* gui);
+#else
+void ffProcessGetInfoLinux(pid_t pid, FFstrbuf* processName, FFstrbuf* exe, const char** exeName, FFstrbuf* exePath);
+const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, int32_t* tty);
+#endif
diff --git a/src/common/properties.h b/src/common/properties.h
new file mode 100644
index 0000000..789a757
--- /dev/null
+++ b/src/common/properties.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "fastfetch.h"
+
+typedef struct FFpropquery {
+ const char* start;
+ FFstrbuf* buffer;
+} FFpropquery;
+
+bool ffParsePropLines(const char* lines, const char* start, FFstrbuf* buffer);
+bool ffParsePropFileValues(const char* filename, uint32_t numQueries, FFpropquery* queries);
+bool ffParsePropFileHomeValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries);
+bool ffParsePropFileListValues(const FFlist* list, const char* relativeFile, uint32_t numQueries, FFpropquery* queries);
+
+bool ffParsePropLinePointer(const char** line, const char* start, FFstrbuf* buffer);
+
+static inline bool ffParsePropLine(const char* line, const char* start, FFstrbuf* buffer) {
+ return ffParsePropLinePointer(&line, start, buffer);
+}
+
+static inline bool ffParsePropFile(const char* filename, const char* start, FFstrbuf* buffer) {
+ return ffParsePropFileValues(filename, 1, (FFpropquery[]) { { start, buffer } });
+}
+
+static inline bool ffParsePropFileHome(const char* relativeFile, const char* start, FFstrbuf* buffer) {
+ return ffParsePropFileHomeValues(relativeFile, 1, (FFpropquery[]) { { start, buffer } });
+}
+
+static inline bool ffParsePropFileList(const FFlist* list, const char* relativeFile, const char* start, FFstrbuf* buffer) {
+ return ffParsePropFileListValues(list, relativeFile, 1, (FFpropquery[]) { { start, buffer } });
+}
+
+static inline bool ffParsePropFileConfigValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries) {
+ return ffParsePropFileListValues(&instance.state.platform.configDirs, relativeFile, numQueries, queries);
+}
+
+static inline bool ffParsePropFileConfig(const char* relativeFile, const char* start, FFstrbuf* buffer) {
+ return ffParsePropFileConfigValues(relativeFile, 1, (FFpropquery[]) { { start, buffer } });
+}
+
+static inline bool ffParsePropFileDataValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries) {
+ return ffParsePropFileListValues(&instance.state.platform.dataDirs, relativeFile, numQueries, queries);
+}
+
+static inline bool ffParsePropFileData(const char* relativeFile, const char* start, FFstrbuf* buffer) {
+ return ffParsePropFileDataValues(relativeFile, 1, (FFpropquery[]) { { start, buffer } });
+}
diff --git a/src/common/settings.h b/src/common/settings.h
new file mode 100644
index 0000000..f2143cf
--- /dev/null
+++ b/src/common/settings.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "fastfetch.h"
+
+typedef enum FF_A_PACKED FFvarianttype {
+ FF_VARIANT_TYPE_STRING,
+ FF_VARIANT_TYPE_BOOL,
+ FF_VARIANT_TYPE_INT
+} FFvarianttype;
+
+typedef union FFvariant {
+ const char* strValue;
+ int32_t intValue;
+ struct
+ {
+ bool boolValueSet;
+ bool boolValue;
+ };
+} FFvariant;
+
+#define FF_VARIANT_NULL ((FFvariant) { .strValue = NULL })
+
+FFvariant ffSettingsGetDConf(const char* key, FFvarianttype type);
+FFvariant ffSettingsGetGSettings(const char* schemaName, const char* path, const char* key, FFvarianttype type);
+FFvariant ffSettingsGetGnome(const char* dconfKey, const char* gsettingsSchemaName, const char* gsettingsPath, const char* gsettingsKey, FFvarianttype type);
+FFvariant ffSettingsGetXFConf(const char* channelName, const char* propertyName, FFvarianttype type);
+typedef bool FFTestXfconfPropCallback(void* data, const char* propertyName); // Return false to break loop
+FFvariant ffSettingsGetXFConfFirstMatch(const char* channelName, const char* propertyPrefix, FFvarianttype type, void* data, FFTestXfconfPropCallback* cb);
+
+int ffSettingsGetSQLite3Int(const char* dbPath, const char* query);
+bool ffSettingsGetSQLite3String(const char* dbPath, const char* query, FFstrbuf* result);
+
+typedef struct {
+ char* theme;
+ char* icon_theme;
+ bool use_e_cursor;
+ int cursor_size;
+ char* desktop_default_background;
+ char* font;
+} ffEnlightenmentSettings;
+bool ffSettingsGetEnlightenmentProperty(ffEnlightenmentSettings* result);
+
+#ifdef __ANDROID__
+bool ffSettingsGetAndroidProperty(const char* propName, FFstrbuf* result);
+#elif defined(__FreeBSD__)
+bool ffSettingsGetFreeBSDKenv(const char* propName, FFstrbuf* result);
+#endif
diff --git a/src/common/size.h b/src/common/size.h
new file mode 100644
index 0000000..7f1da02
--- /dev/null
+++ b/src/common/size.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include "fastfetch.h"
+
+void ffSizeAppendNum(uint64_t bytes, FFstrbuf* result);
diff --git a/src/common/smbios.h b/src/common/smbios.h
new file mode 100644
index 0000000..24edbda
--- /dev/null
+++ b/src/common/smbios.h
@@ -0,0 +1,103 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+
+bool ffIsSmbiosValueSet(FFstrbuf* value);
+static inline void ffCleanUpSmbiosValue(FFstrbuf* value) {
+ if (!ffIsSmbiosValueSet(value)) {
+ ffStrbufClear(value);
+ }
+}
+
+// https://github.com/KunYi/DumpSMBIOS
+// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.9.0.pdf
+
+typedef enum FF_A_PACKED FFSmbiosType // : uint8_t
+{
+ FF_SMBIOS_TYPE_BIOS = 0,
+ FF_SMBIOS_TYPE_SYSTEM_INFO = 1,
+ FF_SMBIOS_TYPE_BASEBOARD_INFO = 2,
+ FF_SMBIOS_TYPE_SYSTEM_ENCLOSURE = 3,
+ FF_SMBIOS_TYPE_PROCESSOR_INFO = 4,
+ FF_SMBIOS_TYPE_MEMORY_CONTROLLER_INFO = 5, // obsolete
+ FF_SMBIOS_TYPE_MEMORY_MODULE_INFO = 6, // obsolete
+ FF_SMBIOS_TYPE_CACHE_INFO = 7,
+ FF_SMBIOS_TYPE_PORT_CONNECTOR_INFO = 8,
+ FF_SMBIOS_TYPE_SYSTEM_SLOTS = 9,
+ FF_SMBIOS_TYPE_ON_BOARD_DEVICES_INFO = 10, // obsolete
+ FF_SMBIOS_TYPE_OEM_STRING = 11,
+ FF_SMBIOS_TYPE_SYSTEM_CONFIGURATION_OPTIONS = 12,
+ FF_SMBIOS_TYPE_BIOS_LANGUAGE_INFO = 13,
+ FF_SMBIOS_TYPE_GROUP_ASSOCIATIONS = 14,
+ FF_SMBIOS_TYPE_SYSTEM_EVENT_LOG = 15,
+ FF_SMBIOS_TYPE_PHYSICAL_MEMORY_ARRAY = 16,
+ FF_SMBIOS_TYPE_MEMORY_DEVICE = 17,
+ FF_SMBIOS_TYPE_32BIT_MEMORY_ERROR_INFO = 18,
+ FF_SMBIOS_TYPE_MEMORY_ARRAY_MAPPED_ADDRESS = 19,
+ FF_SMBIOS_TYPE_MEMORY_DEVICE_MAPPED_ADDRESS = 20,
+ FF_SMBIOS_TYPE_BUILTIN_POINTING_DEVICE = 21,
+ FF_SMBIOS_TYPE_PORTABLE_BATTERY = 22,
+ FF_SMBIOS_TYPE_SYSTEM_RESET = 23,
+ FF_SMBIOS_TYPE_HARDWARE_SECURITY = 24,
+ FF_SMBIOS_TYPE_SYSTEM_POWER_CONTROLS = 25,
+ FF_SMBIOS_TYPE_VOLTAGE_PROBE = 26,
+ FF_SMBIOS_TYPE_COOLING_DEVICE = 27,
+ FF_SMBIOS_TYPE_TEMPERATURE_PROBE = 28,
+ FF_SMBIOS_TYPE_ELECTRICAL_CURRENT_PROBE = 29,
+ FF_SMBIOS_TYPE_OUT_OF_BAND_REMOTE_ACCESS = 30,
+ FF_SMBIOS_TYPE_BOOT_INTEGRITY_SERVICES_ENTRY_POINT = 31, // reserved
+ FF_SMBIOS_TYPE_SYSTEM_BOOT_INFO = 32,
+ FF_SMBIOS_TYPE_64BIT_MEMORY_ERROR_INFO = 33,
+ FF_SMBIOS_TYPE_MANAGEMENT_DEVICE = 34,
+ FF_SMBIOS_TYPE_MANAGEMENT_DEVICE_COMPONENT = 35,
+ FF_SMBIOS_TYPE_MANAGEMENT_DEVICE_THRESHOLD_DATA = 36,
+ FF_SMBIOS_TYPE_MEMORY_CHANNEL = 37,
+ FF_SMBIOS_TYPE_IPMI_DEVICE_INFO = 38,
+ FF_SMBIOS_TYPE_SYSTEM_POWER_SUPPLY = 39,
+ FF_SMBIOS_TYPE_ADDITIONAL_INFO = 40,
+ FF_SMBIOS_TYPE_ONBOARD_DEVICE_EXTENDED_INFO = 41,
+ FF_SMBIOS_TYPE_MANAGEMENT_CONTROLLER_HOST_INTERFACE = 42,
+ FF_SMBIOS_TYPE_TPM_DEVICE = 43,
+ FF_SMBIOS_TYPE_PROCESSOR_ADDITIONAL_INFO = 44,
+ FF_SMBIOS_TYPE_FIRMWARE_INVENTORY_INFO = 45,
+ FF_SMBIOS_TYPE_STRING_PROPERTY = 46,
+ FF_SMBIOS_TYPE_INACTIVE = 126,
+ FF_SMBIOS_TYPE_END_OF_TABLE = 127,
+ FF_SMBIOS_TYPE__MAX,
+ // system- and OEM-specific information 128~256
+} FFSmbiosType;
+static_assert(sizeof(FFSmbiosType) == 1, "FFSmbiosType should be 1 byte");
+
+typedef struct FFSmbiosHeader {
+ // Type of SMBIOS structure. Do NOT test `Type == FF_SMBIOS_END_OF_TABLE` to determine the end of the table,
+ // as malformed tables may be missing the end-of-table marker.
+ // **Use FFSmbiosHeaderTable[FF_SMBIOS_TYPE_END_OF_TABLE] pointer instead.**
+ FFSmbiosType Type;
+ // Length of formatted section, excluding unformatted string section
+ // Must be at least 4 (sizeof(FFSmbiosHeader)) to be valid
+ uint8_t Length;
+ // Unique handle, used to reference this structure from other structures.
+ // Not guaranteed to be consistent across reboots or even multiple reads of the same table.
+ // Must be less than 0xFF00
+ uint16_t Handle;
+} FF_A_PACKED FFSmbiosHeader;
+static_assert(sizeof(FFSmbiosHeader) == 4, "FFSmbiosHeader should be 4 bytes");
+
+static inline const char* ffSmbiosLocateString(const char* start, uint8_t index /* start from 1 */) {
+ if (index == 0 || *start == '\0') {
+ return NULL;
+ }
+ while (--index) {
+ start += strlen(start) + 1;
+ }
+ return start;
+}
+
+typedef const FFSmbiosHeader* FFSmbiosHeaderTable[FF_SMBIOS_TYPE__MAX];
+
+const FFSmbiosHeader* ffSmbiosNextEntry(const FFSmbiosHeader* header);
+const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable(void);
+
+#ifdef __linux__
+bool ffGetSmbiosValue(const char* devicesPath, const char* classPath, FFstrbuf* buffer);
+#endif
diff --git a/src/common/strutil.h b/src/common/strutil.h
new file mode 100644
index 0000000..00a8a08
--- /dev/null
+++ b/src/common/strutil.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "common/wcwidth.h"
+
+#ifdef _WIN32
+// #include <shlwapi.h>
+__stdcall char* StrStrIA(const char* lpFirst, const char* lpSrch);
+ #define strcasestr StrStrIA
+#endif
+
+static inline bool ffStrSet(const char* str) {
+ if (str == NULL) {
+ return false;
+ }
+
+ while (isspace(*str)) {
+ str++;
+ }
+
+ return *str != '\0';
+}
+
+static inline bool ffStrStartsWithIgnCase(const char* str, const char* compareTo) {
+ return strncasecmp(str, compareTo, strlen(compareTo)) == 0;
+}
+
+static inline bool ffStrEqualsIgnCase(const char* str, const char* compareTo) {
+ return strcasecmp(str, compareTo) == 0;
+}
+
+static inline bool ffStrStartsWith(const char* str, const char* compareTo) {
+ return strncmp(str, compareTo, strlen(compareTo)) == 0;
+}
+
+static inline bool ffStrEndsWith(const char* str, const char* compareTo) {
+ size_t strLength = strlen(str);
+ size_t compareToLength = strlen(compareTo);
+ if (strLength < compareToLength) {
+ return false;
+ }
+ return memcmp(str + strLength - compareToLength, compareTo, compareToLength) == 0;
+}
+
+static inline bool ffStrEndsWithIgnCase(const char* str, const char* compareTo) {
+ size_t strLength = strlen(str);
+ size_t compareToLength = strlen(compareTo);
+ if (strLength < compareToLength) {
+ return false;
+ }
+ return strncasecmp(str + strLength - compareToLength, compareTo, compareToLength) == 0;
+}
+
+static inline bool ffStrEquals(const char* str, const char* compareTo) {
+ return strcmp(str, compareTo) == 0;
+}
+
+static inline bool ffStrContains(const char* str, const char* compareTo) {
+ return strstr(str, compareTo) != NULL;
+}
+
+static inline bool ffStrContainsIgnCase(const char* str, const char* compareTo) {
+ return strcasestr(str, compareTo) != NULL;
+}
+
+static inline bool ffStrContainsC(const char* str, char compareTo) {
+ return strchr(str, compareTo) != NULL;
+}
+
+static inline bool ffCharIsEnglishAlphabet(char c) {
+ return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z');
+}
+
+static inline bool ffCharIsDigit(char c) {
+ return '0' <= c && c <= '9';
+}
+
+// Parse one UTF-8 character, returning consumed byte count and display width.
+// Invalid / incomplete sequence falls back to one-byte width=1.
+// If the Unicode codepoint is non-printable, width becomes 0.
+uint8_t ffUtf8CharLenWidth(const char* str, uint32_t length, uint8_t* width);
+
+uint32_t ffUtf8StrWidth(const char* str, uint32_t length);
+
+static inline bool ffCharIsHexDigit(char c) {
+ return ffCharIsDigit(c) || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F');
+}
+
+static inline int8_t ffHexCharToInt(char c) {
+ if (ffCharIsDigit(c)) {
+ return (int8_t) (c - '0');
+ } else if ('a' <= c && c <= 'f') {
+ return (int8_t) (c - 'a' + 10);
+ } else if ('A' <= c && c <= 'F') {
+ return (int8_t) (c - 'A' + 10);
+ } else {
+ return -1;
+ }
+}
+
+// Copies at most (dstBufSiz - 1) bytes from src to dst; dst is always null-terminated
+static inline char* ffStrCopy(char* __restrict__ dst, const char* __restrict__ src, size_t dstBufSiz) {
+ if (__builtin_expect(dst == NULL, false) || dstBufSiz == 0) {
+ return dst;
+ }
+
+ size_t len = strnlen(src, dstBufSiz - 1);
+ memcpy(dst, src, len);
+ dst[len] = '\0';
+ return dst + len;
+}
diff --git a/src/common/sysctl.h b/src/common/sysctl.h
new file mode 100644
index 0000000..1baabe6
--- /dev/null
+++ b/src/common/sysctl.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "fastfetch.h"
+
+#include <sys/types.h>
+#include <sys/sysctl.h>
+
+#ifdef __OpenBSD__
+const char* ffSysctlGetString(int mib1, int mib2, FFstrbuf* result);
+FF_A_NODISCARD int ffSysctlGetInt(int mib1, int mib2, int defaultValue);
+FF_A_NODISCARD int64_t ffSysctlGetInt64(int mib1, int mib2, int64_t defaultValue);
+#else
+const char* ffSysctlGetString(const char* propName, FFstrbuf* result);
+FF_A_NODISCARD int ffSysctlGetInt(const char* propName, int defaultValue);
+FF_A_NODISCARD int64_t ffSysctlGetInt64(const char* propName, int64_t defaultValue);
+#endif
+FF_A_NODISCARD void* ffSysctlGetData(int* request, u_int requestLength, size_t* resultLength);
diff --git a/src/common/temps.h b/src/common/temps.h
new file mode 100644
index 0000000..f2f8eda
--- /dev/null
+++ b/src/common/temps.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "common/parsing.h"
+#include "common/option.h"
+
+void ffTempsAppendNum(double celsius, FFstrbuf* buffer, FFColorRangeConfig config, const FFModuleArgs* module);
+bool ffTempsParseCommandOptions(const char* key, const char* subkey, const char* value, bool* useTemp, FFColorRangeConfig* config);
+bool ffTempsParseJsonObject(yyjson_val* key, yyjson_val* value, bool* useTemp, FFColorRangeConfig* config);
+void ffTempsGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, bool temp, FFColorRangeConfig config);
diff --git a/src/common/textModifier.h b/src/common/textModifier.h
new file mode 100644
index 0000000..77d6e55
--- /dev/null
+++ b/src/common/textModifier.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#define FASTFETCH_TEXT_MODIFIER_BOLT "\033[1m"
+#define FASTFETCH_TEXT_MODIFIER_ERROR "\033[1;31m"
+#define FASTFETCH_TEXT_MODIFIER_RESET "\033[m"
diff --git a/src/common/thread.h b/src/common/thread.h
new file mode 100644
index 0000000..64d8478
--- /dev/null
+++ b/src/common/thread.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include "fastfetch.h"
+
+#ifdef FF_HAVE_THREADS
+ #if defined(_WIN32)
+ #include <winternl.h>
+ #include <synchapi.h>
+ #include <process.h>
+ #include <processthreadsapi.h>
+ #define FF_THREAD_MUTEX_INITIALIZER SRWLOCK_INIT
+typedef SRWLOCK FFThreadMutex;
+typedef HANDLE FFThreadType;
+static inline void ffThreadMutexLock(FFThreadMutex* mutex) {
+ AcquireSRWLockExclusive(mutex);
+}
+static inline void ffThreadMutexUnlock(FFThreadMutex* mutex) {
+ ReleaseSRWLockExclusive(mutex);
+}
+static inline FFThreadType ffThreadCreate(unsigned(__stdcall* func)(void*), void* data) {
+ return (FFThreadType) _beginthreadex(NULL, 0, func, data, 0, NULL);
+}
+ #define FF_THREAD_ENTRY_DECL_WRAPPER(fn, paramType) \
+ static __stdcall unsigned fn##ThreadMain(void* data) { \
+ fn((paramType) data); \
+ return 0; \
+ }
+ #define FF_THREAD_ENTRY_DECL_WRAPPER_NOPARAM(fn) \
+ static __stdcall unsigned fn##ThreadMain() { \
+ fn(); \
+ return 0; \
+ }
+static inline void ffThreadDetach(FFThreadType thread) {
+ NtClose(thread);
+}
+static inline bool ffThreadJoin(FFThreadType thread, uint32_t timeout) {
+ if (NtWaitForSingleObject(thread, FALSE, timeout == 0 ? NULL : &(LARGE_INTEGER) { .QuadPart = (int64_t) timeout * -10000 }) != STATUS_WAIT_0) {
+ TerminateThread(thread, (DWORD) -1);
+ NtClose(thread);
+ return false;
+ }
+ NtClose(thread);
+ return true;
+}
+ #else
+ #include <pthread.h>
+ #include <signal.h>
+ #if FF_HAVE_PTHREAD_NP
+ #include <pthread_np.h>
+ #endif
+typedef pthread_t FFThreadType;
+ #if __APPLE__
+ #include <os/lock.h>
+ #define FF_THREAD_MUTEX_INITIALIZER OS_UNFAIR_LOCK_INIT
+typedef os_unfair_lock FFThreadMutex;
+static inline void ffThreadMutexLock(os_unfair_lock* mutex) {
+ os_unfair_lock_lock(mutex);
+}
+static inline void ffThreadMutexUnlock(os_unfair_lock* mutex) {
+ os_unfair_lock_unlock(mutex);
+}
+ #else
+ #define FF_THREAD_MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER
+typedef pthread_mutex_t FFThreadMutex;
+static inline void ffThreadMutexLock(FFThreadMutex* mutex) {
+ pthread_mutex_lock(mutex);
+}
+static inline void ffThreadMutexUnlock(FFThreadMutex* mutex) {
+ pthread_mutex_unlock(mutex);
+}
+ #endif
+static inline FFThreadType ffThreadCreate(void* (*func)(void*), void* data) {
+ FFThreadType newThread = 0;
+ pthread_create(&newThread, NULL, func, data);
+ return newThread;
+}
+ #define FF_THREAD_ENTRY_DECL_WRAPPER(fn, paramType) \
+ static void* fn##ThreadMain(void* data) { \
+ fn((paramType) data); \
+ return NULL; \
+ }
+ #define FF_THREAD_ENTRY_DECL_WRAPPER_NOPARAM(fn) \
+ static void* fn##ThreadMain() { \
+ fn(); \
+ return NULL; \
+ }
+static inline void ffThreadDetach(FFThreadType thread) {
+ pthread_detach(thread);
+}
+static inline bool ffThreadJoin(FFThreadType thread, FF_A_UNUSED uint32_t timeout) {
+ #if HAVE_TIMEDJOIN_NP
+ if (timeout > 0) {
+ struct timespec ts;
+ if (clock_gettime(CLOCK_REALTIME, &ts) == 0) {
+ ts.tv_sec += timeout / 1000;
+ ts.tv_nsec += (timeout % 1000) * 1000000;
+ if (pthread_timedjoin_np(thread, NULL, &ts) != 0) {
+ pthread_kill(thread, SIGTERM);
+ return false;
+ }
+ return true;
+ }
+ }
+ #endif
+ pthread_join(thread, NULL);
+ return true;
+}
+ #endif
+#else // FF_HAVE_THREADS
+ #define FF_THREAD_MUTEX_INITIALIZER 0
+typedef char FFThreadMutex;
+static inline void ffThreadMutexLock(FF_A_UNUSED FFThreadMutex* mutex) {}
+static inline void ffThreadMutexUnlock(FF_A_UNUSED FFThreadMutex* mutex) {}
+ #define FF_THREAD_ENTRY_DECL_WRAPPER(fn, paramType)
+#endif // FF_HAVE_THREADS
diff --git a/src/common/time.h b/src/common/time.h
new file mode 100644
index 0000000..e2d86ab
--- /dev/null
+++ b/src/common/time.h
@@ -0,0 +1,80 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <time.h>
+#ifdef _WIN32
+ #include <ntstatus.h>
+ #include "common/windows/nt.h"
+ #include <profileapi.h>
+#elif defined(__HAIKU__)
+ #include <OS.h>
+#endif
+
+#include "common/arrutil.h"
+
+static inline double ffTimeGetTick(void) // In msec
+{
+#ifdef _WIN32
+ extern double ffQpcMultiplier;
+ LARGE_INTEGER start;
+ RtlQueryPerformanceCounter(&start);
+ return (double) start.QuadPart * ffQpcMultiplier;
+#elif defined(__HAIKU__)
+ return (double) system_time() / 1000.;
+#else
+ struct timespec timeNow;
+ clock_gettime(CLOCK_MONOTONIC, &timeNow);
+ return (double) timeNow.tv_sec * 1000. + (double) timeNow.tv_nsec / 1000000.;
+#endif
+}
+
+#if _WIN32
+static inline uint64_t ffFileTimeToUnixMs(uint64_t value) {
+ if (__builtin_expect(__builtin_usubll_overflow(value, 116444736000000000ull, &value), false)) {
+ return 0;
+ }
+ return value / 10000ull;
+}
+#endif
+
+static inline uint64_t ffTimeGetNow(void) {
+#ifdef _WIN32
+ uint64_t timeNow = ffKSystemTimeToUInt64(&SharedUserData->SystemTime);
+ return ffFileTimeToUnixMs((uint64_t) timeNow);
+#elif defined(__HAIKU__)
+ return (uint64_t) real_time_clock_usecs() / 1000u;
+#else
+ struct timespec timeNow;
+ clock_gettime(CLOCK_REALTIME, &timeNow);
+ return (uint64_t) (((uint64_t) timeNow.tv_sec * 1000u) + ((uint64_t) timeNow.tv_nsec / 1000000u));
+#endif
+}
+
+// Returns true if not interrupted
+static inline bool ffTimeSleep(uint32_t msec) {
+#ifdef _WIN32
+ LARGE_INTEGER interval;
+ interval.QuadPart = -(int64_t) msec * 10000; // Relative time in 100-nanosecond intervals
+ return NT_SUCCESS(NtDelayExecution(TRUE, &interval));
+#else
+ return nanosleep(&(struct timespec) { msec / 1000, (long) (msec % 1000) * 1000000 }, NULL) == 0;
+#endif
+}
+
+// Not thread-safe
+const char* ffTimeToFullStr(uint64_t msec);
+
+// Not thread-safe
+const char* ffTimeToShortStr(uint64_t msec);
+
+// Not thread-safe
+const char* ffTimeToTimeStr(uint64_t msec);
+
+typedef struct FFTimeGetAgeResult {
+ uint32_t years;
+ uint32_t daysOfYear;
+ double yearsFraction;
+} FFTimeGetAgeResult;
+
+FFTimeGetAgeResult ffTimeGetAge(uint64_t birthMs, uint64_t nowMs);
diff --git a/src/common/unused.h b/src/common/unused.h
new file mode 100644
index 0000000..98e21a2
--- /dev/null
+++ b/src/common/unused.h
@@ -0,0 +1,6 @@
+#pragma once
+
+static inline void ffUnused(int dummy, ...) {
+ (void) dummy;
+}
+#define FF_UNUSED(...) ffUnused(0, __VA_ARGS__);
diff --git a/src/common/wcwidth.h b/src/common/wcwidth.h
new file mode 100644
index 0000000..8d093da
--- /dev/null
+++ b/src/common/wcwidth.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <stdint.h>
+
+#if FF_ENABLE_WCWIDTH
+int mk_wcwidth(uint32_t wc);
+#else
+static inline int mk_wcwidth(uint32_t wc) {
+ (void) wc;
+ return 1;
+}
+#endif
diff --git a/src/common/windows/c-logo.sh b/src/common/windows/c-logo.sh
new file mode 100755
index 0000000..e4d7863
--- /dev/null
+++ b/src/common/windows/c-logo.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# Convert logo.svg to logo.ico
+rsvg-convert -w 16 -h 16 logo.svg > logo16.png
+rsvg-convert -w 32 -h 32 logo.svg > logo32.png
+rsvg-convert -w 48 -h 48 logo.svg > logo48.png
+rsvg-convert -w 64 -h 64 logo.svg > logo64.png
+rsvg-convert -w 128 -h 128 logo.svg > logo128.png
+rsvg-convert -w 256 -h 256 logo.svg > logo256.png
+convert logo16.png logo32.png logo48.png logo64.png logo128.png logo256.png logo.ico
+rm logo16.png logo32.png logo48.png logo64.png logo128.png logo256.png
diff --git a/src/common/windows/com.c b/src/common/windows/com.c
new file mode 100644
index 0000000..a400ca9
--- /dev/null
+++ b/src/common/windows/com.c
@@ -0,0 +1,67 @@
+#include "com.h"
+
+#include <stdlib.h>
+
+#if FF_HAVE_WINRT
+ #include <roapi.h>
+
+static void RoUninitializeWrap(void) {
+ RoUninitialize();
+}
+
+static const char* doInitCom() {
+ HRESULT res = RoInitialize(RO_INIT_MULTITHREADED);
+ if (FAILED(res)) {
+ switch (res) {
+ case E_INVALIDARG:
+ return "RoInitialize() failed: invalid argument";
+ case E_OUTOFMEMORY:
+ return "RoInitialize() failed: out of memory";
+ case E_UNEXPECTED:
+ return "RoInitialize() failed: unexpected error";
+ case RPC_E_CHANGED_MODE:
+ // COM was already initialized with a different concurrency model
+ return NULL;
+ default:
+ return "RoInitialize() failed: unknown error";
+ }
+ }
+
+ atexit(RoUninitializeWrap);
+ return NULL;
+}
+#else
+ #include <combaseapi.h>
+
+static void CoUninitializeWrap(void) {
+ CoUninitialize();
+}
+
+static const char* doInitCom() {
+ HRESULT res = CoInitializeEx(NULL, COINIT_MULTITHREADED);
+ if (FAILED(res)) {
+ switch (res) {
+ case E_INVALIDARG:
+ return "CoInitializeEx() failed: invalid argument";
+ case E_OUTOFMEMORY:
+ return "CoInitializeEx() failed: out of memory";
+ case RPC_E_CHANGED_MODE:
+ // COM was already initialized with a different concurrency model
+ return NULL;
+ default:
+ return "CoInitializeEx() failed: unknown error";
+ }
+ }
+
+ atexit(CoUninitializeWrap);
+ return NULL;
+}
+#endif
+
+const char* ffInitCom(void) {
+ static const char* error = "";
+ if (error && error[0] == '\0') {
+ error = doInitCom();
+ }
+ return error;
+}
diff --git a/src/common/windows/com.h b/src/common/windows/com.h
new file mode 100644
index 0000000..57b8c6d
--- /dev/null
+++ b/src/common/windows/com.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "common/attributes.h"
+#include <assert.h>
+#include <unknwn.h>
+
+// Initialize COM & WinRT
+const char* ffInitCom(void);
+
+static inline void ffReleaseComObject(void* ppUnknown) {
+ assert(ppUnknown);
+ IUnknown* pUnknown = *(IUnknown**) ppUnknown;
+ if (pUnknown) {
+#ifdef __cplusplus
+ pUnknown->Release();
+#else
+ pUnknown->lpVtbl->Release(pUnknown);
+#endif
+ *(IUnknown**) ppUnknown = NULL;
+ }
+}
+
+#define FF_AUTO_RELEASE_COM_OBJECT FF_A_CLEANUP(ffReleaseComObject)
diff --git a/src/common/windows/getline.c b/src/common/windows/getline.c
new file mode 100644
index 0000000..3d43d55
--- /dev/null
+++ b/src/common/windows/getline.c
@@ -0,0 +1,58 @@
+#include "getline.h"
+
+#include <stdlib.h>
+#include <errno.h>
+
+ssize_t getline(char** lineptr, size_t* n, FILE* stream) {
+ ssize_t pos = -1;
+ int c;
+
+ if (lineptr == NULL || stream == NULL || n == NULL) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ _lock_file(stream);
+
+ c = _getc_nolock(stream);
+ if (c == EOF) {
+ goto exit;
+ }
+
+ if (*lineptr == NULL) {
+ *lineptr = malloc(128);
+ if (*lineptr == NULL) {
+ goto exit;
+ }
+ *n = 128;
+ }
+
+ pos = 0;
+ while (c != EOF) {
+ if ((size_t) (pos + 1) >= *n) {
+ size_t new_size = *n + (*n >> 2);
+ if (new_size < 128) {
+ new_size = 128;
+ }
+ char* new_ptr = realloc(*lineptr, new_size);
+ if (new_ptr == NULL) {
+ pos = -1;
+ goto exit;
+ }
+ *n = new_size;
+ *lineptr = new_ptr;
+ }
+
+ ((char*) (*lineptr))[pos++] = (char) c;
+ if (c == '\n') {
+ break;
+ }
+ c = _getc_nolock(stream);
+ }
+
+ (*lineptr)[pos] = '\0';
+
+exit:
+ _unlock_file(stream);
+ return pos;
+}
diff --git a/src/common/windows/getline.h b/src/common/windows/getline.h
new file mode 100644
index 0000000..49ba5db
--- /dev/null
+++ b/src/common/windows/getline.h
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdio.h>
+
+ssize_t getline(char** lineptr, size_t* n, FILE* stream);
diff --git a/src/common/windows/logo.ico b/src/common/windows/logo.ico
new file mode 100644
index 0000000..cdd89ad
--- /dev/null
+++ b/src/common/windows/logo.ico
Binary files differ
diff --git a/src/common/windows/logo.svg b/src/common/windows/logo.svg
new file mode 120000
index 0000000..068d3f6
--- /dev/null
+++ b/src/common/windows/logo.svg
@@ -0,0 +1 @@
+../../logo/logo.svg \ No newline at end of file
diff --git a/src/common/windows/manifest.xml b/src/common/windows/manifest.xml
new file mode 100644
index 0000000..2113c07
--- /dev/null
+++ b/src/common/windows/manifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/><!-- 8.1 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/><!-- 10/11 -->
+ </application>
+ </compatibility>
+ <assemblyIdentity type="win32" name="com.github.fastfetch" version="0.0.0.0"/>
+ <asmv3:application>
+ <asmv3:windowsSettings>
+ <activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
+ <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
+ <heapType xmlns="http://schemas.microsoft.com/SMI/2020/WindowsSettings">SegmentHeap</heapType>
+ </asmv3:windowsSettings>
+ </asmv3:application>
+</assembly>
diff --git a/src/common/windows/nt.h b/src/common/windows/nt.h
new file mode 100644
index 0000000..2f476b8
--- /dev/null
+++ b/src/common/windows/nt.h
@@ -0,0 +1,1325 @@
+#pragma once
+
+#include <ntdef.h>
+#include <winternl.h>
+#include <winnt.h>
+#include <stdint.h>
+#include <assert.h>
+
+enum {
+ SystemModuleInformation = 11,
+ SystemFirmwareTableInformation = 76,
+ SystemBootEnvironmentInformation = 90,
+ SystemLogicalProcessorAndGroupInformation = 107,
+ SystemSecureBootInformation = 146,
+};
+
+typedef struct _PROCESSOR_POWER_INFORMATION {
+ ULONG Number;
+ ULONG MaxMhz;
+ ULONG CurrentMhz;
+ ULONG MhzLimit;
+ ULONG MaxIdleState;
+ ULONG CurrentIdleState;
+} PROCESSOR_POWER_INFORMATION, *PPROCESSOR_POWER_INFORMATION;
+
+NTSYSAPI NTSTATUS NTAPI NtPowerInformation(
+ IN POWER_INFORMATION_LEVEL InformationLevel,
+ IN PVOID InputBuffer OPTIONAL,
+ IN ULONG InputBufferLength,
+ OUT PVOID OutputBuffer OPTIONAL,
+ IN ULONG OutputBufferLength);
+
+NTSYSAPI NTSTATUS NTAPI RtlGetVersion(
+ _Inout_ PRTL_OSVERSIONINFOW lpVersionInformation);
+
+NTSYSAPI NTSTATUS NTAPI NtQueryDirectoryFile(
+ IN HANDLE FileHandle,
+ IN HANDLE Event OPTIONAL,
+ IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
+ IN PVOID ApcContext OPTIONAL,
+ OUT PIO_STATUS_BLOCK IoStatusBlock,
+ OUT PVOID FileInformation,
+ IN ULONG Length,
+ IN FILE_INFORMATION_CLASS FileInformationClass,
+ IN BOOLEAN ReturnSingleEntry,
+ IN PUNICODE_STRING FileName OPTIONAL,
+ IN BOOLEAN RestartScan);
+
+// https://ntdoc.m417z.com/process_devicemap_information_ex
+typedef struct _PROCESS_DEVICEMAP_INFORMATION_EX {
+ union {
+ struct
+ {
+ HANDLE DirectoryHandle; // A handle to a directory object that can be set as the new device map for the process. This handle must have DIRECTORY_TRAVERSE access.
+ } Set;
+ struct
+ {
+ ULONG DriveMap; // A bitmask that indicates which drive letters are currently in use in the process's device map.
+ UCHAR DriveType[32]; // A value that indicates the type of each drive (e.g., local disk, network drive, etc.). // DRIVE_* WinBase.h
+ } Query;
+ };
+ ULONG Flags; // PROCESS_LUID_DOSDEVICES_ONLY
+} PROCESS_DEVICEMAP_INFORMATION_EX, *PPROCESS_DEVICEMAP_INFORMATION_EX;
+
+#ifndef NtCurrentProcess
+ #define NtCurrentProcess() ((HANDLE) (LONG_PTR) - 1)
+#endif
+
+typedef struct _CURDIR {
+ UNICODE_STRING DosPath;
+ HANDLE Handle;
+} CURDIR, *PCURDIR;
+
+NTSYSAPI PIMAGE_NT_HEADERS NTAPI RtlImageNtHeader(IN PVOID BaseOfImage);
+
+/**
+ * The SECTION_IMAGE_INFORMATION structure contains detailed information about an image section.
+ */
+typedef struct _SECTION_IMAGE_INFORMATION {
+ PVOID TransferAddress; // The address of the image entry point function.
+ ULONG ZeroBits; // The number of high-order address bits that must be zero in the image base address.
+ SIZE_T MaximumStackSize; // The maximum stack size of threads from the PE file header.
+ SIZE_T CommittedStackSize; // The initial stack size of threads from the PE file header.
+ ULONG SubSystemType; // The image subsystem from the PE file header (e.g., Windows GUI, Windows CUI, POSIX).
+ union {
+ struct
+ {
+ USHORT SubSystemMinorVersion;
+ USHORT SubSystemMajorVersion;
+ };
+ ULONG SubSystemVersion;
+ };
+ union {
+ struct
+ {
+ USHORT MajorOperatingSystemVersion;
+ USHORT MinorOperatingSystemVersion;
+ };
+ ULONG OperatingSystemVersion;
+ };
+ USHORT ImageCharacteristics; // The image characteristics from the PE file header.
+ USHORT DllCharacteristics; // The DLL characteristics flags (e.g., ASLR, NX compatibility).
+ USHORT Machine; // The image architecture (e.g., x86, x64, ARM).
+ BOOLEAN ImageContainsCode; // The image contains native executable code.
+ union {
+ UCHAR ImageFlags;
+ struct
+ {
+ UCHAR ComPlusNativeReady : 1; // The image contains precompiled .NET assembly generated by NGEN (Native Image Generator).
+ UCHAR ComPlusILOnly : 1; // the image contains only Microsoft Intermediate Language (IL) assembly.
+ UCHAR ImageDynamicallyRelocated : 1; // The image was mapped using a random base address rather than the preferred base address.
+ UCHAR ImageMappedFlat : 1; // The image was mapped using a single contiguous region, rather than separate regions for each section.
+ UCHAR BaseBelow4gb : 1; // The image was mapped using a base address below the 4 GB boundary.
+ UCHAR ComPlusPrefer32bit : 1; // The image prefers to run as a 32-bit process, even on a 64-bit system.
+ UCHAR Reserved : 2;
+ };
+ };
+ ULONG LoaderFlags; // Reserved by ntdll.dll for the Windows loader.
+ ULONG ImageFileSize; // The size of the image, in bytes, including all headers.
+ ULONG CheckSum; // The image file checksum, from the PE optional header.
+} SECTION_IMAGE_INFORMATION, *PSECTION_IMAGE_INFORMATION;
+
+typedef struct _SYSTEM_BOOT_ENVIRONMENT_INFORMATION {
+ GUID BootIdentifier;
+ FIRMWARE_TYPE FirmwareType;
+ union {
+ ULONGLONG BootFlags;
+ struct
+ {
+ ULONGLONG DbgMenuOsSelection : 1; // REDSTONE4
+ ULONGLONG DbgHiberBoot : 1;
+ ULONGLONG DbgSoftBoot : 1;
+ ULONGLONG DbgMeasuredLaunch : 1;
+ ULONGLONG DbgMeasuredLaunchCapable : 1; // 19H1
+ ULONGLONG DbgSystemHiveReplace : 1;
+ ULONGLONG DbgMeasuredLaunchSmmProtections : 1;
+ ULONGLONG DbgMeasuredLaunchSmmLevel : 7; // 20H1
+ ULONGLONG DbgBugCheckRecovery : 1; // 24H2
+ ULONGLONG DbgFASR : 1;
+ ULONGLONG DbgUseCachedBcd : 1;
+ };
+ };
+} SYSTEM_BOOT_ENVIRONMENT_INFORMATION;
+
+typedef struct _RTL_PROCESS_MODULE_INFORMATION {
+ PVOID Section;
+ PVOID MappedBase;
+ PVOID ImageBase;
+ ULONG ImageSize;
+ ULONG Flags;
+ USHORT LoadOrderIndex;
+ USHORT InitOrderIndex;
+ USHORT LoadCount;
+ USHORT OffsetToFileName;
+ UCHAR FullPathName[256];
+} RTL_PROCESS_MODULE_INFORMATION, *PRTL_PROCESS_MODULE_INFORMATION;
+
+typedef struct _RTL_PROCESS_MODULES {
+ ULONG NumberOfModules;
+ _Field_size_(NumberOfModules) RTL_PROCESS_MODULE_INFORMATION Modules[1];
+} RTL_PROCESS_MODULES, *PRTL_PROCESS_MODULES;
+
+NTSTATUS NTAPI NtQuerySystemEnvironmentValueEx(
+ _In_ PCUNICODE_STRING VariableName,
+ _In_ const GUID* VendorGuid,
+ _Out_writes_bytes_opt_(*BufferLength) PVOID Buffer,
+ _Inout_ PULONG BufferLength,
+ _Out_opt_ PULONG Attributes // EFI_VARIABLE_*
+);
+
+NTSTATUS NTAPI RtlGUIDFromString(IN PCUNICODE_STRING GuidString, OUT GUID* Guid);
+NTSTATUS NTAPI RtlStringFromGUIDEx(IN GUID* Guid, OUT PCUNICODE_STRING GuidString, _In_ BOOLEAN AllocateGuidString);
+
+typedef struct _SYSTEM_SECUREBOOT_INFORMATION {
+ BOOLEAN SecureBootEnabled;
+ BOOLEAN SecureBootCapable;
+} SYSTEM_SECUREBOOT_INFORMATION, *PSYSTEM_SECUREBOOT_INFORMATION;
+
+NTSTATUS NTAPI NtQuerySystemInformationEx(
+ _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
+ _In_reads_bytes_(InputBufferLength) PVOID InputBuffer,
+ _In_ ULONG InputBufferLength,
+ _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
+ _In_ ULONG SystemInformationLength,
+ _Out_opt_ PULONG ReturnLength);
+
+typedef enum _SYSTEM_FIRMWARE_TABLE_ACTION {
+ SystemFirmwareTableEnumerate,
+ SystemFirmwareTableGet,
+ SystemFirmwareTableMax
+} SYSTEM_FIRMWARE_TABLE_ACTION;
+
+typedef struct _SYSTEM_FIRMWARE_TABLE_INFORMATION {
+ ULONG ProviderSignature; // (same as the GetSystemFirmwareTable function)
+ SYSTEM_FIRMWARE_TABLE_ACTION Action;
+ ULONG TableID;
+ ULONG TableBufferLength;
+ _Field_size_bytes_(TableBufferLength) UCHAR TableBuffer[];
+} SYSTEM_FIRMWARE_TABLE_INFORMATION, *PSYSTEM_FIRMWARE_TABLE_INFORMATION;
+
+NTSYSAPI NTSTATUS NTAPI NtDelayExecution(_In_ BOOLEAN Alertable, _In_ PLARGE_INTEGER DelayInterval);
+
+/**
+ * The KSYSTEM_TIME structure represents interrupt time, system time, and time zone bias.
+ */
+typedef struct _KSYSTEM_TIME {
+ ULONG LowPart;
+ LONG High1Time;
+ LONG High2Time;
+} KSYSTEM_TIME, *PKSYSTEM_TIME;
+
+/**
+ * PROCESSOR_FEATURE_MAX defines the maximum number of processor feature flags
+ * that may be reported by the system.
+ */
+#define PROCESSOR_FEATURE_MAX 64
+
+/**
+ * The ALTERNATIVE_ARCHITECTURE_TYPE enumeration specifies the hardware
+ * architecture variant used by the system.
+ *
+ * \remarks NEC98x86 represents the NEC PC-98 architecture,
+ * supported only on very early Windows releases.
+ */
+typedef enum _ALTERNATIVE_ARCHITECTURE_TYPE {
+ StandardDesign,
+ NEC98x86,
+ EndAlternatives
+} ALTERNATIVE_ARCHITECTURE_TYPE;
+
+/**
+ * The KUSER_SHARED_DATA structure contains information shared with user-mode.
+ *
+ * \sa https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-kuser_shared_data
+ */
+typedef struct _KUSER_SHARED_DATA {
+ //
+ // Current low 32-bit of tick count and tick count multiplier.
+ //
+ // N.B. The tick count is updated each time the clock ticks.
+ //
+
+ ULONG TickCountLowDeprecated;
+ ULONG TickCountMultiplier;
+
+ //
+ // Current 64-bit interrupt time in 100ns units.
+ //
+
+ volatile KSYSTEM_TIME InterruptTime;
+
+ //
+ // Current 64-bit system time in 100ns units.
+ //
+
+ volatile KSYSTEM_TIME SystemTime;
+
+ //
+ // Current 64-bit time zone bias.
+ //
+
+ volatile KSYSTEM_TIME TimeZoneBias;
+
+ //
+ // Support image magic number range for the host system.
+ //
+ // N.B. This is an inclusive range.
+ //
+
+ USHORT ImageNumberLow;
+ USHORT ImageNumberHigh;
+
+ //
+ // Copy of system root in unicode.
+ //
+ // N.B. This field must be accessed via the RtlGetNtSystemRoot API for
+ // an accurate result.
+ //
+
+ WCHAR NtSystemRoot[260];
+
+ //
+ // Maximum stack trace depth if tracing enabled.
+ //
+
+ ULONG MaxStackTraceDepth;
+
+ //
+ // Crypto exponent value.
+ //
+
+ ULONG CryptoExponent;
+
+ //
+ // Time zone ID.
+ //
+
+ ULONG TimeZoneId;
+
+ //
+ // Minimum size of a large page on the system, in bytes.
+ //
+ // N.B. Returned by GetLargePageMinimum() function.
+ //
+
+ ULONG LargePageMinimum;
+
+ //
+ // This value controls the Application Impact Telemetry (AIT) Sampling rate.
+ //
+ // This value determines how frequently the system records AIT events,
+ // which are used by the Application Experience and compatibility
+ // subsystems to evaluate application behavior, performance, and
+ // potential compatibility issues.
+ //
+ // Lower values increase sampling frequency, while higher values reduce it.
+ // The kernel updates this field as part of its internal telemetry and
+ // heuristics logic.
+ //
+
+ ULONG AitSamplingValue;
+
+ //
+ // This value controls Application Compatibility (AppCompat) switchback processing.
+ //
+
+ union {
+ ULONG AppCompatFlag;
+ struct
+ {
+ ULONG SwitchbackEnabled : 1; // Basic switchback processing
+ ULONG ExtendedHeuristics : 1; // Extended switchback heuristics
+ ULONG TelemetryFallback : 1; // Telemetry-driven fallback
+ ULONG Reserved : 29;
+ } AppCompatFlags;
+ };
+
+ //
+ // Current Kernel Root RNG state seed version
+ //
+
+ ULONGLONG RNGSeedVersion;
+
+ //
+ // This value controls assertion failure handling.
+ //
+ // Historically (prior to Windows 10), this value was also used by
+ // Code Integrity (CI), AppLocker, and related security components to
+ // determine the minimum validation requirements for executable images,
+ // drivers, and privileged operations.
+ //
+ // In modern Windows versions, this field is used primarily by the kernel's
+ // diagnostic and validation infrastructure to decide how assertion failures
+ // should be handled (e.g., logging, debugger break-in, or bugcheck).
+
+ ULONG GlobalValidationRunlevel;
+
+ //
+ // Monotonic stamp incremented by the kernel whenever the system's
+ // time zone bias value changes.
+ //
+ // N.B. This field must be accessed via the RtlGetSystemTimeAndBias API for
+ // an accurate result.
+ // This value is read before and after accessing the bias fields to determine
+ // whether the time zone data changed during the read. If the stamp differs,
+ // the caller must re-read the bias values to ensure consistency.
+ //
+
+ volatile LONG TimeZoneBiasStamp;
+
+ //
+ // The shared collective build number undecorated with C or F.
+ // GetVersionEx hides the real number
+ //
+
+ ULONG NtBuildNumber;
+
+ //
+ // Product type.
+ //
+ // N.B. This field must be accessed via the RtlGetNtProductType API for
+ // an accurate result.
+ //
+
+ NT_PRODUCT_TYPE NtProductType;
+ BOOLEAN ProductTypeIsValid;
+ BOOLEAN Reserved0[1];
+
+ //
+ // Native hardware processor architecture of the running system.
+ //
+ // N.B. User-mode components read this field to determine the true system
+ // architecture, especially in WOW64 scenarios where the process architecture
+ // differs from the native one.
+ //
+
+ USHORT NativeProcessorArchitecture;
+
+ //
+ // The NT Version.
+ //
+ // N. B. Note that each process sees a version from its PEB, but if the
+ // process is running with an altered view of the system version,
+ // the following two fields are used to correctly identify the
+ // version
+ //
+
+ ULONG NtMajorVersion;
+ ULONG NtMinorVersion;
+
+ //
+ // Processor features.
+ //
+
+ BOOLEAN ProcessorFeatures[PROCESSOR_FEATURE_MAX];
+
+ //
+ // Reserved fields - do not use.
+ //
+
+ ULONG MaximumUserModeAddressDeprecated; // Deprecated, use SystemBasicInformation instead.
+ ULONG SystemRangeStartDeprecated; // Deprecated, use SystemRangeStartInformation instead.
+
+ //
+ // Time slippage while in debugger.
+ //
+
+ volatile ULONG TimeSlip;
+
+ //
+ // Alternative system architecture, e.g., NEC PC98xx on x86.
+ //
+
+ ALTERNATIVE_ARCHITECTURE_TYPE AlternativeArchitecture;
+
+ //
+ // Boot sequence, incremented for each boot attempt by the OS loader.
+ //
+
+ ULONG BootId;
+
+ //
+ // If the system is an evaluation unit, the following field contains the
+ // date and time that the evaluation unit expires. A value of 0 indicates
+ // that there is no expiration. A non-zero value is the UTC absolute time
+ // that the system expires.
+ //
+
+ LARGE_INTEGER SystemExpirationDate;
+
+ //
+ // Suite support.
+ //
+ // N.B. This field must be accessed via the RtlGetSuiteMask API for
+ // an accurate result.
+ //
+
+ ULONG SuiteMask;
+
+ //
+ // TRUE if a kernel debugger is connected/enabled.
+ //
+
+ BOOLEAN KdDebuggerEnabled;
+
+ //
+ // Mitigation policies.
+ //
+
+ union {
+ UCHAR MitigationPolicies;
+ struct
+ {
+ UCHAR NXSupportPolicy : 2;
+ UCHAR SEHValidationPolicy : 2;
+ UCHAR CurDirDevicesSkippedForDlls : 2;
+ UCHAR Reserved : 2;
+ };
+ };
+
+ //
+ // Measured duration of a single processor yield, in cycles. This is used by
+ // lock packages to determine how many times to spin waiting for a state
+ // change before blocking.
+ //
+
+ USHORT CyclesPerYield;
+
+ //
+ // Current console session Id. Always zero on non-TS systems.
+ //
+ // N.B. This field must be accessed via the RtlGetActiveConsoleId API for an
+ // accurate result.
+ //
+
+ volatile ULONG ActiveConsoleId;
+
+ //
+ // Force-dismounts cause handles to become invalid. Rather than always
+ // probe handles, a serial number of dismounts is maintained that clients
+ // can use to see if they need to probe handles.
+ //
+
+ volatile ULONG DismountCount;
+
+ //
+ // This field indicates the status of the 64-bit COM+ package on the
+ // system. It indicates whether the Intermediate Language (IL) COM+
+ // images need to use the 64-bit COM+ runtime or the 32-bit COM+ runtime.
+ //
+
+ ULONG ComPlusPackage;
+
+ //
+ // Time in tick count for system-wide last user input across all terminal
+ // sessions. For MP performance, it is not updated all the time (e.g. once
+ // a minute per session). It is used for idle detection.
+ //
+
+ ULONG LastSystemRITEventTickCount;
+
+ //
+ // Number of physical pages in the system. This can dynamically change as
+ // physical memory can be added or removed from a running system. This
+ // cell is too small to hold the non-truncated value on very large memory
+ // machines so code that needs the full value should access
+ // FullNumberOfPhysicalPages instead.
+ //
+
+ ULONG NumberOfPhysicalPages;
+
+ //
+ // True if the system was booted in safe boot mode.
+ //
+
+ BOOLEAN SafeBootMode;
+
+ //
+ // Virtualization flags.
+ //
+
+ union {
+ UCHAR VirtualizationFlags;
+
+#if defined(_ARM64_)
+
+ //
+ // N.B. Keep this bitfield in sync with the one in arc.w.
+ //
+
+ struct
+ {
+ UCHAR ArchStartedInEl2 : 1;
+ UCHAR QcSlIsSupported : 1;
+ UCHAR : 6;
+ };
+
+#endif
+ };
+
+ //
+ // Reserved (available for reuse).
+ //
+
+ UCHAR Reserved12[2];
+
+ //
+ // This is a packed bitfield that contains various flags concerning
+ // the system state. They must be manipulated using interlocked
+ // operations.
+ //
+ // N.B. DbgMultiSessionSku must be accessed via the RtlIsMultiSessionSku
+ // API for an accurate result
+ //
+
+ union {
+ ULONG SharedDataFlags;
+ struct
+ {
+ //
+ // The following bit fields are for the debugger only. Do not use.
+ // Use the bit definitions instead.
+ //
+
+ ULONG DbgErrorPortPresent : 1;
+ ULONG DbgElevationEnabled : 1;
+ ULONG DbgVirtEnabled : 1;
+ ULONG DbgInstallerDetectEnabled : 1;
+ ULONG DbgLkgEnabled : 1;
+ ULONG DbgDynProcessorEnabled : 1;
+ ULONG DbgConsoleBrokerEnabled : 1;
+ ULONG DbgSecureBootEnabled : 1;
+ ULONG DbgMultiSessionSku : 1;
+ ULONG DbgMultiUsersInSessionSku : 1;
+ ULONG DbgStateSeparationEnabled : 1;
+ ULONG DbgSplitTokenEnabled : 1;
+ ULONG DbgShadowAdminEnabled : 1;
+ ULONG SpareBits : 19;
+ };
+ };
+
+ // ... more fields follow, but we don't need them
+} KUSER_SHARED_DATA, *PKUSER_SHARED_DATA;
+
+#ifdef __aarch64__
+ #define SharedUserData ({ \
+ __auto_type shared_user_data = (const volatile KUSER_SHARED_DATA*) (uintptr_t) 0x7FFE0000UL; \
+ __asm__("" : "+r"(shared_user_data)); /* https://github.com/lhmouse/mcfgthread/issues/330 */ \
+ shared_user_data; \
+ })
+#else
+ #define SharedUserData ((const volatile KUSER_SHARED_DATA*) (uintptr_t) 0x7FFE0000UL)
+#endif
+
+static inline uint64_t ffKSystemTimeToUInt64(const volatile KSYSTEM_TIME* pTime) {
+#if _WIN64
+
+ // This is safe even if pTime is not 8-byte aligned
+ // See https://learn.microsoft.com/en-us/windows/win32/winprog64/fault-alignments
+ return *(const volatile uint64_t*) pTime;
+
+#else
+
+ uint32_t low, high1, high2;
+
+ do {
+ high1 = (uint32_t) pTime->High1Time;
+ low = (uint32_t) pTime->LowPart;
+ high2 = (uint32_t) pTime->High2Time;
+ } while (high1 != high2);
+
+ return ((uint64_t) high1 << 32) | low;
+#endif
+}
+
+static inline bool ffIsWindows10OrGreater() {
+#if FF_WIN81_COMPAT
+ return SharedUserData->NtMajorVersion >= 10;
+#else
+ return true;
+#endif
+}
+
+static inline bool ffIsWindows11OrGreater() {
+ return SharedUserData->NtMajorVersion > 10 ||
+ (SharedUserData->NtMajorVersion == 10 && SharedUserData->NtBuildNumber >= 22000);
+}
+
+NTSYSAPI NTSTATUS NTAPI NtOpenProcessToken(
+ _In_ HANDLE ProcessHandle,
+ _In_ ACCESS_MASK DesiredAccess,
+ _Out_ PHANDLE TokenHandle);
+NTSYSAPI NTSTATUS NTAPI NtAdjustPrivilegesToken(
+ _In_ HANDLE TokenHandle,
+ _In_ BOOLEAN DisableAllPrivileges,
+ _In_opt_ PTOKEN_PRIVILEGES NewState,
+ _In_ ULONG BufferLength,
+ _Out_writes_bytes_to_opt_(BufferLength, *ReturnLength) PTOKEN_PRIVILEGES PreviousState,
+ _Out_opt_ PULONG ReturnLength);
+NTSYSAPI NTSTATUS NTAPI NtQueryInformationToken(
+ _In_ HANDLE TokenHandle,
+ _In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
+ _Out_writes_bytes_to_opt_(TokenInformationLength, *ReturnLength) PVOID TokenInformation,
+ _In_ ULONG TokenInformationLength,
+ _Out_ PULONG ReturnLength);
+#define NtCurrentProcessToken() ((HANDLE) (LONG_PTR) - 4) // for NtQueryInformationToken only; Windows 8+
+
+NTSYSAPI NTSTATUS NTAPI NtReadFile(
+ _In_ HANDLE FileHandle,
+ _In_opt_ HANDLE Event,
+ _In_opt_ PIO_APC_ROUTINE ApcRoutine,
+ _In_opt_ PVOID ApcContext,
+ _Out_ PIO_STATUS_BLOCK IoStatusBlock,
+ _Out_writes_bytes_(Length) PVOID Buffer,
+ _In_ ULONG Length,
+ _In_opt_ PLARGE_INTEGER ByteOffset,
+ _In_opt_ PULONG Key);
+
+NTSYSAPI NTSTATUS NTAPI NtCreateEvent(
+ _Out_ PHANDLE EventHandle,
+ _In_ ACCESS_MASK DesiredAccess,
+ _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
+ _In_ EVENT_TYPE EventType,
+ _In_ BOOLEAN InitialState);
+
+NTSYSAPI NTSTATUS NTAPI NtQueryAttributesFile(
+ _In_ PCOBJECT_ATTRIBUTES ObjectAttributes,
+ _Out_ PFILE_BASIC_INFORMATION FileInformation);
+
+NTSYSAPI NTSTATUS NTAPI RtlUnicodeToUTF8N(
+ _Out_writes_bytes_to_(UTF8StringMaxByteCount, *UTF8StringActualByteCount) PCHAR UTF8StringDestination,
+ _In_ ULONG UTF8StringMaxByteCount,
+ _Out_opt_ PULONG UTF8StringActualByteCount,
+ _In_reads_bytes_(UnicodeStringByteCount) PCWCH UnicodeStringSource,
+ _In_ ULONG UnicodeStringByteCount);
+
+NTSYSAPI NTSTATUS NTAPI RtlUTF8ToUnicodeN(
+ _Out_writes_bytes_to_(UnicodeStringMaxByteCount, *UnicodeStringActualByteCount) PWSTR UnicodeStringDestination,
+ _In_ ULONG UnicodeStringMaxByteCount,
+ _Out_opt_ PULONG UnicodeStringActualByteCount,
+ _In_reads_bytes_(UTF8StringByteCount) PCCH UTF8StringSource,
+ _In_ ULONG UTF8StringByteCount);
+
+#define RTL_MAX_DRIVE_LETTERS 32
+typedef struct _RTL_DRIVE_LETTER_CURDIR {
+ USHORT Flags;
+ USHORT Length;
+ ULONG TimeStamp;
+ STRING DosPath;
+} RTL_DRIVE_LETTER_CURDIR, *PRTL_DRIVE_LETTER_CURDIR;
+
+typedef struct _RTL_USER_PROCESS_PARAMETERS_FULL {
+ ULONG MaximumLength;
+ ULONG Length;
+
+ ULONG Flags;
+ ULONG DebugFlags;
+
+ HANDLE ConsoleHandle;
+ ULONG ConsoleFlags;
+ HANDLE StandardInput;
+ HANDLE StandardOutput;
+ HANDLE StandardError;
+
+ CURDIR CurrentDirectory;
+ UNICODE_STRING DllPath;
+ UNICODE_STRING ImagePathName;
+ UNICODE_STRING CommandLine;
+ PVOID Environment;
+
+ ULONG StartingX;
+ ULONG StartingY;
+ ULONG CountX;
+ ULONG CountY;
+ ULONG CountCharsX;
+ ULONG CountCharsY;
+ ULONG FillAttribute;
+
+ ULONG WindowFlags;
+ ULONG ShowWindowFlags;
+ UNICODE_STRING WindowTitle;
+ UNICODE_STRING DesktopInfo;
+ UNICODE_STRING ShellInfo;
+ UNICODE_STRING RuntimeData;
+ RTL_DRIVE_LETTER_CURDIR CurrentDirectories[RTL_MAX_DRIVE_LETTERS];
+
+ // Windows Vista
+ ULONG_PTR EnvironmentSize;
+ // Windows 7
+ ULONG_PTR EnvironmentVersion;
+
+ // Windows 8
+ PVOID PackageDependencyData;
+ ULONG ProcessGroupId;
+
+ // ...
+} RTL_USER_PROCESS_PARAMETERS_FULL, *PRTL_USER_PROCESS_PARAMETERS_FULL;
+
+typedef struct KERNEL_CALLBACK_TABLE* PKERNEL_CALLBACK_TABLE;
+typedef struct API_SET_NAMESPACE* PAPI_SET_NAMESPACE;
+typedef struct RTL_BITMAP* PRTL_BITMAP;
+typedef struct SILO_USER_SHARED_DATA* PSILO_USER_SHARED_DATA;
+typedef struct CPTABLEINFO* PCPTABLEINFO;
+typedef struct NLSTABLEINFO* PNLSTABLEINFO;
+typedef struct GDI_HANDLE_ENTRY* PGDI_HANDLE_ENTRY;
+
+typedef struct _PEB_FULL {
+ //
+ // The process was cloned with an inherited address space.
+ //
+ BOOLEAN InheritedAddressSpace;
+
+ //
+ // The process has image file execution options (IFEO).
+ //
+ BOOLEAN ReadImageFileExecOptions;
+
+ //
+ // The process has a debugger attached.
+ //
+ BOOLEAN BeingDebugged;
+
+ union {
+ BOOLEAN BitField;
+ struct
+ {
+ BOOLEAN ImageUsesLargePages : 1; // The process uses large image regions (4 MB).
+ BOOLEAN IsProtectedProcess : 1; // The process is a protected process.
+ BOOLEAN IsImageDynamicallyRelocated : 1; // The process image base address was relocated.
+ BOOLEAN SkipPatchingUser32Forwarders : 1; // The process skipped forwarders for User32.dll functions. 1 for 64-bit, 0 for 32-bit.
+ BOOLEAN IsPackagedProcess : 1; // The process is a packaged store process (APPX/MSIX).
+ BOOLEAN IsAppContainerProcess : 1; // The process has an AppContainer token.
+ BOOLEAN IsProtectedProcessLight : 1; // The process is a protected process (light).
+ BOOLEAN IsLongPathAwareProcess : 1; // The process is long path aware.
+ };
+ };
+
+ //
+ // Handle to a mutex for synchronization.
+ //
+ HANDLE Mutant;
+
+ //
+ // Pointer to the base address of the process image.
+ //
+ PVOID ImageBaseAddress;
+
+ //
+ // Pointer to the process loader data.
+ //
+ PPEB_LDR_DATA Ldr;
+
+ //
+ // Pointer to the process parameters.
+ //
+ PRTL_USER_PROCESS_PARAMETERS_FULL ProcessParameters;
+
+ //
+ // Reserved.
+ //
+ PVOID SubSystemData;
+
+ //
+ // Pointer to the process default heap.
+ //
+ PVOID ProcessHeap;
+
+ //
+ // Pointer to a critical section used to synchronize access to the PEB.
+ //
+ PRTL_CRITICAL_SECTION FastPebLock;
+
+ //
+ // Pointer to a singly linked list used by ATL.
+ //
+ PSLIST_HEADER AtlThunkSListPtr;
+
+ //
+ // Handle to the Image File Execution Options key.
+ //
+ HANDLE IFEOKey;
+
+ //
+ // Cross process flags.
+ //
+ union {
+ ULONG CrossProcessFlags;
+ struct
+ {
+ ULONG ProcessInJob : 1; // The process is part of a job.
+ ULONG ProcessInitializing : 1; // The process is initializing.
+ ULONG ProcessUsingVEH : 1; // The process is using VEH.
+ ULONG ProcessUsingVCH : 1; // The process is using VCH.
+ ULONG ProcessUsingFTH : 1; // The process is using FTH.
+ ULONG ProcessPreviouslyThrottled : 1; // The process was previously throttled.
+ ULONG ProcessCurrentlyThrottled : 1; // The process is currently throttled.
+ ULONG ProcessImagesHotPatched : 1; // The process images are hot patched. // RS5
+ ULONG ReservedBits0 : 24;
+ };
+ };
+
+ //
+ // User32 KERNEL_CALLBACK_TABLE (ntuser.h)
+ //
+ union {
+ PKERNEL_CALLBACK_TABLE KernelCallbackTable;
+ PVOID UserSharedInfoPtr;
+ };
+
+ //
+ // Reserved.
+ //
+ ULONG SystemReserved;
+
+ //
+ // Pointer to the Active Template Library (ATL) singly linked list (32-bit)
+ //
+ ULONG AtlThunkSListPtr32;
+
+ //
+ // Pointer to the API Set Schema.
+ //
+ PAPI_SET_NAMESPACE ApiSetMap;
+
+ //
+ // Counter for TLS expansion.
+ //
+ ULONG TlsExpansionCounter;
+
+ //
+ // Pointer to the TLS bitmap.
+ //
+ PRTL_BITMAP TlsBitmap;
+
+ //
+ // Bits for the TLS bitmap.
+ //
+ ULONG TlsBitmapBits[2];
+
+ //
+ // Reserved for CSRSS.
+ //
+ PVOID ReadOnlySharedMemoryBase;
+
+ //
+ // Pointer to the USER_SHARED_DATA for the current SILO.
+ //
+ PSILO_USER_SHARED_DATA SharedData;
+
+ //
+ // Reserved for CSRSS.
+ //
+ PVOID* ReadOnlyStaticServerData;
+
+ //
+ // Pointer to the ANSI code page data.
+ //
+ PCPTABLEINFO AnsiCodePageData;
+
+ //
+ // Pointer to the OEM code page data.
+ //
+ PCPTABLEINFO OemCodePageData;
+
+ //
+ // Pointer to the Unicode case table data.
+ //
+ PNLSTABLEINFO UnicodeCaseTableData;
+
+ //
+ // The total number of system processors.
+ //
+ ULONG NumberOfProcessors;
+
+ //
+ // Global flags for the system.
+ //
+ union {
+ ULONG NtGlobalFlag;
+ struct
+ {
+ ULONG StopOnException : 1; // FLG_STOP_ON_EXCEPTION
+ ULONG ShowLoaderSnaps : 1; // FLG_SHOW_LDR_SNAPS
+ ULONG DebugInitialCommand : 1; // FLG_DEBUG_INITIAL_COMMAND
+ ULONG StopOnHungGUI : 1; // FLG_STOP_ON_HUNG_GUI
+ ULONG HeapEnableTailCheck : 1; // FLG_HEAP_ENABLE_TAIL_CHECK
+ ULONG HeapEnableFreeCheck : 1; // FLG_HEAP_ENABLE_FREE_CHECK
+ ULONG HeapValidateParameters : 1; // FLG_HEAP_VALIDATE_PARAMETERS
+ ULONG HeapValidateAll : 1; // FLG_HEAP_VALIDATE_ALL
+ ULONG ApplicationVerifier : 1; // FLG_APPLICATION_VERIFIER
+ ULONG MonitorSilentProcessExit : 1; // FLG_MONITOR_SILENT_PROCESS_EXIT
+ ULONG PoolEnableTagging : 1; // FLG_POOL_ENABLE_TAGGING
+ ULONG HeapEnableTagging : 1; // FLG_HEAP_ENABLE_TAGGING
+ ULONG UserStackTraceDb : 1; // FLG_USER_STACK_TRACE_DB
+ ULONG KernelStackTraceDb : 1; // FLG_KERNEL_STACK_TRACE_DB
+ ULONG MaintainObjectTypeList : 1; // FLG_MAINTAIN_OBJECT_TYPELIST
+ ULONG HeapEnableTagByDll : 1; // FLG_HEAP_ENABLE_TAG_BY_DLL
+ ULONG DisableStackExtension : 1; // FLG_DISABLE_STACK_EXTENSION
+ ULONG EnableCsrDebug : 1; // FLG_ENABLE_CSRDEBUG
+ ULONG EnableKDebugSymbolLoad : 1; // FLG_ENABLE_KDEBUG_SYMBOL_LOAD
+ ULONG DisablePageKernelStacks : 1; // FLG_DISABLE_PAGE_KERNEL_STACKS
+ ULONG EnableSystemCritBreaks : 1; // FLG_ENABLE_SYSTEM_CRIT_BREAKS
+ ULONG HeapDisableCoalescing : 1; // FLG_HEAP_DISABLE_COALESCING
+ ULONG EnableCloseExceptions : 1; // FLG_ENABLE_CLOSE_EXCEPTIONS
+ ULONG EnableExceptionLogging : 1; // FLG_ENABLE_EXCEPTION_LOGGING
+ ULONG EnableHandleTypeTagging : 1; // FLG_ENABLE_HANDLE_TYPE_TAGGING
+ ULONG HeapPageAllocs : 1; // FLG_HEAP_PAGE_ALLOCS
+ ULONG DebugInitialCommandEx : 1; // FLG_DEBUG_INITIAL_COMMAND_EX
+ ULONG DisableDbgPrint : 1; // FLG_DISABLE_DBGPRINT
+ ULONG CritSecEventCreation : 1; // FLG_CRITSEC_EVENT_CREATION
+ ULONG LdrTopDown : 1; // FLG_LDR_TOP_DOWN
+ ULONG EnableHandleExceptions : 1; // FLG_ENABLE_HANDLE_EXCEPTIONS
+ ULONG DisableProtDlls : 1; // FLG_DISABLE_PROTDLLS
+ } NtGlobalFlags;
+ };
+
+ //
+ // Timeout for critical sections.
+ //
+ LARGE_INTEGER CriticalSectionTimeout;
+
+ //
+ // Reserved size for heap segments.
+ //
+ SIZE_T HeapSegmentReserve;
+
+ //
+ // Committed size for heap segments.
+ //
+ SIZE_T HeapSegmentCommit;
+
+ //
+ // Threshold for decommitting total free heap.
+ //
+ SIZE_T HeapDeCommitTotalFreeThreshold;
+
+ //
+ // Threshold for decommitting free heap blocks.
+ //
+ SIZE_T HeapDeCommitFreeBlockThreshold;
+
+ //
+ // Number of process heaps.
+ //
+ ULONG NumberOfHeaps;
+
+ //
+ // Maximum number of process heaps.
+ //
+ ULONG MaximumNumberOfHeaps;
+
+ //
+ // Pointer to an array of process heaps. ProcessHeaps is initialized
+ // to point to the first free byte after the PEB and MaximumNumberOfHeaps
+ // is computed from the page size used to hold the PEB, less the fixed
+ // size of this data structure.
+ //
+ PVOID* ProcessHeaps;
+
+ //
+ // Pointer to the system GDI shared handle table.
+ //
+ PGDI_HANDLE_ENTRY GdiSharedHandleTable;
+
+ //
+ // Pointer to the process starter helper.
+ //
+ PVOID ProcessStarterHelper;
+
+ //
+ // The maximum number of GDI function calls during batch operations (GdiSetBatchLimit)
+ //
+ ULONG GdiDCAttributeList;
+
+ //
+ // Pointer to the loader lock critical section.
+ //
+ PRTL_CRITICAL_SECTION LoaderLock;
+
+ //
+ // Major version of the operating system.
+ //
+ ULONG OSMajorVersion;
+
+ //
+ // Minor version of the operating system.
+ //
+ ULONG OSMinorVersion;
+
+ //
+ // Build number of the operating system.
+ //
+ USHORT OSBuildNumber;
+
+ //
+ // CSD version of the operating system.
+ //
+ USHORT OSCSDVersion;
+
+ //
+ // Platform ID of the operating system.
+ //
+ ULONG OSPlatformId;
+
+ //
+ // Subsystem version of the current process image (PE Headers).
+ //
+ ULONG ImageSubsystem;
+
+ //
+ // Major version of the current process image subsystem (PE Headers).
+ //
+ ULONG ImageSubsystemMajorVersion;
+
+ //
+ // Minor version of the current process image subsystem (PE Headers).
+ //
+ ULONG ImageSubsystemMinorVersion;
+
+ // ...
+} PEB_FULL, *PPEB_FULL;
+
+typedef struct _TEB_FULL {
+ //
+ // Thread Information Block (TIB) contains the thread's stack, base and limit addresses, the current stack pointer, and the exception list.
+ //
+ NT_TIB NtTib;
+
+ //
+ // Reserved.
+ //
+ PVOID EnvironmentPointer;
+
+ //
+ // Client ID for this thread.
+ //
+ CLIENT_ID ClientId;
+
+ //
+ // A handle to an active Remote Procedure Call (RPC) if the thread is currently involved in an RPC operation.
+ //
+ PVOID ActiveRpcHandle;
+
+ //
+ // A pointer to the __declspec(thread) local storage array.
+ //
+ PVOID ThreadLocalStoragePointer;
+
+ //
+ // A pointer to the Process Environment Block (PEB), which contains information about the process.
+ //
+ PPEB_FULL ProcessEnvironmentBlock;
+
+ //
+ // The previous Win32 error value for this thread.
+ //
+ ULONG LastErrorValue;
+
+ //
+ // The number of critical sections currently owned by this thread.
+ //
+ ULONG CountOfOwnedCriticalSections;
+
+ //
+ // Reserved.
+ //
+ PVOID CsrClientThread;
+
+ //
+ // Reserved for win32k.sys
+ //
+ PVOID Win32ThreadInfo;
+
+ //
+ // Reserved for user32.dll
+ //
+ ULONG User32Reserved[26];
+
+ //
+ // Reserved for winsrv.dll
+ //
+ ULONG UserReserved[5];
+
+ //
+ // Reserved.
+ //
+ PVOID WOW32Reserved;
+
+ //
+ // The LCID of the current thread. (Kernel32!GetThreadLocale)
+ //
+ LCID CurrentLocale;
+} TEB_FULL, *PTEB_FULL;
+
+static inline PTEB_FULL ffGetTeb() {
+ return (PTEB_FULL) NtCurrentTeb();
+}
+
+static inline PPEB_FULL ffGetPeb() {
+ return ffGetTeb()->ProcessEnvironmentBlock;
+}
+
+NTSYSAPI NTSTATUS NTAPI RtlExpandEnvironmentStrings(
+ _In_opt_ PVOID Environment,
+ _In_reads_(SourceLength) PCWSTR Source,
+ _In_ SIZE_T SourceLength,
+ _Out_writes_(DestinationLength) PWSTR Destination,
+ _In_ SIZE_T DestinationLength,
+ _Out_opt_ PSIZE_T ReturnLength);
+
+NTSYSAPI NTSTATUS NTAPI NtOpenKey(
+ _Out_ PHANDLE KeyHandle,
+ _In_ ACCESS_MASK DesiredAccess,
+ _In_ POBJECT_ATTRIBUTES ObjectAttributes);
+
+typedef enum _KEY_VALUE_INFORMATION_CLASS {
+ KeyValueBasicInformation, // KEY_VALUE_BASIC_INFORMATION
+ KeyValueFullInformation, // KEY_VALUE_FULL_INFORMATION
+ KeyValuePartialInformation, // KEY_VALUE_PARTIAL_INFORMATION
+ KeyValueFullInformationAlign64, // KEY_VALUE_FULL_INFORMATION_ALIGN64
+ KeyValuePartialInformationAlign64, // KEY_VALUE_PARTIAL_INFORMATION_ALIGN64
+ KeyValueLayerInformation, // KEY_VALUE_LAYER_INFORMATION
+ MaxKeyValueInfoClass
+} KEY_VALUE_INFORMATION_CLASS;
+
+NTSYSAPI NTSTATUS NTAPI NtQueryValueKey(
+ _In_ HANDLE KeyHandle,
+ _In_ PCUNICODE_STRING ValueName,
+ _In_ KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
+ _Out_writes_bytes_to_opt_(Length, *ResultLength) PVOID KeyValueInformation,
+ _In_ ULONG Length,
+ _Out_ PULONG ResultLength);
+
+NTSYSAPI NTSTATUS NTAPI RtlOpenCurrentUser(
+ _In_ ACCESS_MASK DesiredAccess,
+ _Out_ PHANDLE CurrentUserKey);
+
+typedef struct _KEY_VALUE_PARTIAL_INFORMATION {
+ ULONG TitleIndex;
+ ULONG Type;
+ ULONG DataLength;
+ _Field_size_bytes_(DataLength) UCHAR Data[];
+} KEY_VALUE_PARTIAL_INFORMATION, *PKEY_VALUE_PARTIAL_INFORMATION;
+
+typedef enum _KEY_INFORMATION_CLASS {
+ KeyBasicInformation, // KEY_BASIC_INFORMATION
+ KeyNodeInformation, // KEY_NODE_INFORMATION
+ KeyFullInformation, // KEY_FULL_INFORMATION
+ KeyNameInformation, // KEY_NAME_INFORMATION
+ KeyCachedInformation, // KEY_CACHED_INFORMATION
+ KeyFlagsInformation, // KEY_FLAGS_INFORMATION
+ KeyVirtualizationInformation, // KEY_VIRTUALIZATION_INFORMATION
+ KeyHandleTagsInformation, // KEY_HANDLE_TAGS_INFORMATION
+ KeyTrustInformation, // KEY_TRUST_INFORMATION
+ KeyLayerInformation, // KEY_LAYER_INFORMATION
+ MaxKeyInfoClass
+} KEY_INFORMATION_CLASS;
+
+NTSYSAPI NTSTATUS NTAPI NtEnumerateKey(
+ _In_ HANDLE KeyHandle,
+ _In_ ULONG Index,
+ _In_ KEY_INFORMATION_CLASS KeyInformationClass,
+ _Out_writes_bytes_to_opt_(Length, *ResultLength) PVOID KeyInformation,
+ _In_ ULONG Length,
+ _Out_ PULONG ResultLength);
+
+typedef struct _KEY_BASIC_INFORMATION {
+ LARGE_INTEGER LastWriteTime; // Number of 100-nanosecond intervals since this key or any of its values changed.
+ ULONG TitleIndex; // Reserved // A legacy field originally intended for use with localization such as an index of a resource table.
+ ULONG NameLength; // The size, in bytes, of the key name string in the Name array.
+ _Field_size_bytes_(NameLength) WCHAR Name[]; // The name of the registry key. This string is not null-terminated.
+} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
+
+typedef struct _KEY_FULL_INFORMATION {
+ LARGE_INTEGER LastWriteTime;
+ ULONG TitleIndex;
+ ULONG ClassOffset;
+ ULONG ClassLength;
+ ULONG SubKeys;
+ ULONG MaxNameLength;
+ ULONG MaxClassLength;
+ ULONG Values;
+ ULONG MaxValueNameLength;
+ ULONG MaxValueDataLength;
+ WCHAR Class[];
+} KEY_FULL_INFORMATION, *PKEY_FULL_INFORMATION;
+
+NTSYSAPI NTSTATUS NTAPI NtQueryKey(
+ _In_ HANDLE KeyHandle,
+ _In_ KEY_INFORMATION_CLASS KeyInformationClass,
+ _Out_writes_bytes_to_opt_(Length, *ResultLength) PVOID KeyInformation,
+ _In_ ULONG Length,
+ _Out_ PULONG ResultLength);
+
+NTSYSAPI NTSTATUS NTAPI NtOpenProcess(
+ _Out_ PHANDLE ProcessHandle,
+ _In_ ACCESS_MASK DesiredAccess,
+ _In_ PCOBJECT_ATTRIBUTES ObjectAttributes,
+ _In_opt_ PCLIENT_ID ClientId);
+
+NTSYSAPI NTSTATUS NTAPI LdrLoadDll(
+ _In_opt_ PCWSTR DllPath,
+ _In_opt_ PULONG DllCharacteristics,
+ _In_ PCUNICODE_STRING DllName,
+ _Out_ PVOID* DllHandle);
+
+NTSYSAPI NTSTATUS NTAPI LdrUnloadDll(
+ _In_ PVOID DllHandle);
+
+NTSYSAPI NTSTATUS NTAPI LdrGetDllHandle(
+ _In_opt_ PCWSTR DllPath,
+ _In_opt_ PULONG DllCharacteristics,
+ _In_ PCUNICODE_STRING DllName,
+ _Out_ PVOID* DllHandle);
+
+NTSYSAPI NTSTATUS NTAPI LdrGetProcedureAddress(
+ _In_ PVOID DllHandle,
+ _In_opt_ PCANSI_STRING ProcedureName,
+ _In_opt_ ULONG ProcedureNumber,
+ _Out_ PVOID* ProcedureAddress);
+
+typedef enum _SECTION_INHERIT {
+ ViewShare = 1,
+ ViewUnmap = 2
+} SECTION_INHERIT;
+
+NTSYSAPI NTSTATUS NTAPI NtCreateSection(
+ _Out_ PHANDLE SectionHandle,
+ _In_ ACCESS_MASK DesiredAccess,
+ _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
+ _In_opt_ PLARGE_INTEGER MaximumSize,
+ _In_ ULONG SectionPageProtection,
+ _In_ ULONG AllocationAttributes,
+ _In_opt_ HANDLE FileHandle);
+
+NTSYSAPI NTSTATUS NTAPI NtMapViewOfSection(
+ _In_ HANDLE SectionHandle,
+ _In_ HANDLE ProcessHandle,
+ _Inout_ _At_(*BaseAddress, _Readable_bytes_(*ViewSize) _Writable_bytes_(*ViewSize) _Post_readable_byte_size_(*ViewSize)) PVOID* BaseAddress,
+ _In_ ULONG_PTR ZeroBits,
+ _In_ SIZE_T CommitSize,
+ _Inout_opt_ PLARGE_INTEGER SectionOffset,
+ _Inout_ PSIZE_T ViewSize,
+ _In_ SECTION_INHERIT InheritDisposition,
+ _In_ ULONG AllocationType,
+ _In_ ULONG PageProtection);
+
+NTSYSAPI NTSTATUS NTAPI NtUnmapViewOfSection(
+ _In_ HANDLE ProcessHandle,
+ _In_opt_ PVOID BaseAddress);
+
+NTSYSAPI LOGICAL NTAPI RtlQueryPerformanceCounter(
+ _Out_ PLARGE_INTEGER PerformanceCounter);
+
+NTSYSAPI LOGICAL NTAPI RtlQueryPerformanceFrequency(
+ _Out_ PLARGE_INTEGER PerformanceFrequency);
+
+NTSYSAPI NTSTATUS NTAPI NtCancelIoFileEx(
+ _In_ HANDLE FileHandle,
+ _In_opt_ PIO_STATUS_BLOCK IoRequestToCancel,
+ _Out_ PIO_STATUS_BLOCK IoStatusBlock);
+
+NTSYSAPI NTSTATUS NTAPI NtTerminateProcess(
+ _In_opt_ HANDLE ProcessHandle,
+ _In_ NTSTATUS ExitStatus);
diff --git a/src/common/windows/perflib_.h b/src/common/windows/perflib_.h
new file mode 100644
index 0000000..f30ed54
--- /dev/null
+++ b/src/common/windows/perflib_.h
@@ -0,0 +1,134 @@
+#pragma once
+
+#include <windows.h>
+#include <perflib.h>
+
+// Missing from <perflib.h> of MinGW-w64 SDK
+
+#define PERF_WILDCARD_COUNTER 0xFFFFFFFF
+#define PERF_WILDCARD_INSTANCE L"*"
+#define PERF_AGGREGATE_INSTANCE L"_Total"
+#define PERF_MAX_INSTANCE_NAME 1024
+
+typedef struct _PERF_INSTANCE_HEADER {
+ ULONG Size; // = sizeof(PERF_INSTANCE_HEADER) + sizeof(InstanceName) + sizeof(Padding)
+ ULONG InstanceId; // Instance ID.
+ // Followed by:
+ // WCHAR InstanceName[]; // Nul-terminated.
+ // WCHAR Padding[]; // Pad to a multiple of 8 bytes
+} PERF_INSTANCE_HEADER, *PPERF_INSTANCE_HEADER;
+
+typedef struct _PERF_COUNTER_IDENTIFIER {
+ GUID CounterSetGuid; // The GUID of the counterset.
+ ULONG Status; // Win32 error code indicating success/failure of the add/delete operation.
+ ULONG Size; // sizeof(PERF_COUNTER_IDENTIFIER) + sizeof(InstanceName) + sizeof(Padding)
+ ULONG CounterId; // CounterId, or PERF_WILDCARD_COUNTER for all counters.
+ ULONG InstanceId; // InstanceId, or 0xFFFFFFFF to not filter on instance ID.
+ ULONG Index; // Set by PerfQueryCounterInfo to the position in which the corresponding counter data is returned.
+ ULONG Reserved; // Reserved.
+ // Followed by:
+ // WCHAR InstanceName[];
+ // WCHAR Padding[];
+} PERF_COUNTER_IDENTIFIER, *PPERF_COUNTER_IDENTIFIER;
+
+typedef struct _PERF_DATA_HEADER {
+ ULONG dwTotalSize; // = sizeof(PERF_DATA_HEADER) + sizeof(PERF_COUNTER_HEADER blocks...)
+ ULONG dwNumCounters; // The number of PERF_COUNTER_HEADER blocks.
+ LONGLONG PerfTimeStamp; // Timestamp from a high-resolution clock.
+ LONGLONG PerfTime100NSec; // The number of 100 nanosecond intervals since January 1, 1601, in Coordinated Universal Time (UTC).
+ LONGLONG PerfFreq; // The frequency of a high-resolution clock.
+ SYSTEMTIME SystemTime; // The time at which data is collected on the provider side.
+ // Followed by:
+ // PERF_COUNTER_HEADER blocks...;
+} PERF_DATA_HEADER, *PPERF_DATA_HEADER;
+
+typedef enum _PerfCounterDataType {
+ PERF_ERROR_RETURN = 0, /* An error occurred when the performance counter value was queried. */
+ PERF_SINGLE_COUNTER = 1, /* Query returned a single counter from a single-instance. */
+ PERF_MULTIPLE_COUNTERS = 2, /* Query returned multiple counters from a single instance. */
+ PERF_MULTIPLE_INSTANCES = 4, /* Query returned a single counter from each of multiple instances. */
+ PERF_COUNTERSET = 6 /* Query returned multiple counters from each of multiple instances. */
+} PerfCounterDataType;
+
+typedef struct _PERF_COUNTER_HEADER {
+ ULONG dwStatus; // Win32 error code indicating success/failure of the query operation.
+ PerfCounterDataType dwType; // Result type - error, single/single, multi/single, single/multi, multi/multi.
+ ULONG dwSize; // = sizeof(PERF_COUNTER_HEADER) + sizeof(Additional data)
+ ULONG Reserved; // Reserved.
+ // Followed by additional data:
+ // If dwType == PERF_ERROR_RETURN: nothing.
+ // If dwType == PERF_SINGLE_COUNTER: PERF_COUNTER_DATA block.
+ // If dwType == PERF_MULTIPLE_COUNTERS: PERF_MULTI_COUNTERS block + PERF_COUNTER_DATA blocks.
+ // If dwType == PERF_MULTIPLE_INSTANCES: PERF_MULTI_INSTANCES block.
+ // If dwType == PERF_COUNTERSET: PERF_MULTI_COUNTERS block + PERF_MULTI_INSTANCES block.
+} PERF_COUNTER_HEADER, *PPERF_COUNTER_HEADER;
+
+typedef struct _PERF_MULTI_INSTANCES {
+ ULONG dwTotalSize; // = sizeof(PERF_MULTI_INSTANCES) + sizeof(instance data blocks...)
+ ULONG dwInstances; // Number of instance data blocks.
+ // Followed by:
+ // Instance data blocks...;
+} PERF_MULTI_INSTANCES, *PPERF_MULTI_INSTANCES;
+
+typedef struct _PERF_MULTI_COUNTERS {
+ ULONG dwSize; // sizeof(PERF_MULTI_COUNTERS) + sizeof(CounterIds)
+ ULONG dwCounters; // Number of counter ids.
+ // Followed by:
+ // DWORD CounterIds[dwCounters];
+} PERF_MULTI_COUNTERS, *PPERF_MULTI_COUNTERS;
+
+typedef struct _PERF_COUNTER_DATA {
+ ULONG dwDataSize; // Size of the counter data, in bytes.
+ ULONG dwSize; // = sizeof(PERF_COUNTER_DATA) + sizeof(Data) + sizeof(Padding)
+ // Followed by:
+ // BYTE Data[dwDataSize];
+ // BYTE Padding[];
+} PERF_COUNTER_DATA, *PPERF_COUNTER_DATA;
+
+_Success_(return == ERROR_SUCCESS)
+ ULONG
+ WINAPI
+ PerfEnumerateCounterSetInstances(
+ _In_opt_z_ LPCWSTR szMachine,
+ _In_ LPCGUID pCounterSetId,
+ _Out_opt_bytecap_post_bytecount_(cbInstances, *pcbInstancesActual) PPERF_INSTANCE_HEADER pInstances,
+ DWORD cbInstances,
+ _Out_ LPDWORD pcbInstancesActual);
+
+_Success_(return == ERROR_SUCCESS)
+ ULONG
+ WINAPI
+ PerfOpenQueryHandle(
+ _In_opt_z_ LPCWSTR szMachine,
+ _Out_ HANDLE* phQuery);
+
+_Success_(return == ERROR_SUCCESS)
+ ULONG
+ WINAPI
+ PerfCloseQueryHandle(
+ _In_ HANDLE hQuery);
+
+_Success_(return == ERROR_SUCCESS)
+ ULONG
+ WINAPI
+ PerfAddCounters(
+ _In_ HANDLE hQuery,
+ _Inout_bytecount_(cbCounters) PPERF_COUNTER_IDENTIFIER pCounters,
+ DWORD cbCounters);
+
+_Success_(return == ERROR_SUCCESS)
+ ULONG
+ WINAPI
+ PerfDeleteCounters(
+ _In_ HANDLE hQuery,
+ _Inout_bytecount_(cbCounters) PPERF_COUNTER_IDENTIFIER pCounters,
+ DWORD cbCounters);
+
+_Success_(return == ERROR_SUCCESS)
+ ULONG
+ WINAPI
+ PerfQueryCounterData(
+ _In_ HANDLE hQuery,
+ _Out_opt_bytecap_post_bytecount_(cbCounterBlock, *pcbCounterBlockActual) PPERF_DATA_HEADER pCounterBlock,
+ DWORD cbCounterBlock,
+ _Out_ LPDWORD pcbCounterBlockActual);
diff --git a/src/common/windows/registry.c b/src/common/windows/registry.c
new file mode 100644
index 0000000..3618760
--- /dev/null
+++ b/src/common/windows/registry.c
@@ -0,0 +1,388 @@
+#include "registry.h"
+#include "unicode.h"
+#include "common/mallocHelper.h"
+#include "common/debug.h"
+#include "common/windows/nt.h"
+
+#include <stdalign.h>
+#include <ntstatus.h>
+
+static HANDLE hRootKeys[8 /*(uintptr_t) HKEY_CURRENT_USER_LOCAL_SETTINGS - (uintptr_t) HKEY_CLASSES_ROOT + 1*/];
+
+static const char* hKey2Str(HANDLE hRootKey) {
+#define HKEY_CASE(compareKey) \
+ if (hRootKey == hRootKeys[(uintptr_t) compareKey - (uintptr_t) HKEY_CLASSES_ROOT]) return #compareKey;
+ HKEY_CASE(HKEY_CLASSES_ROOT)
+ HKEY_CASE(HKEY_CURRENT_USER)
+ HKEY_CASE(HKEY_LOCAL_MACHINE)
+ HKEY_CASE(HKEY_USERS)
+ HKEY_CASE(HKEY_PERFORMANCE_DATA)
+ HKEY_CASE(HKEY_CURRENT_CONFIG)
+ HKEY_CASE(HKEY_DYN_DATA)
+ HKEY_CASE(HKEY_CURRENT_USER_LOCAL_SETTINGS)
+#undef HKEY_CASE
+
+ return "UNKNOWN";
+}
+
+HANDLE ffRegGetRootKeyHandle(HKEY hKey) {
+ assert(hKey);
+ assert((uintptr_t) hKey >= (uintptr_t) HKEY_CLASSES_ROOT && (uintptr_t) hKey <= (uintptr_t) HKEY_CURRENT_USER_LOCAL_SETTINGS);
+
+ FF_DEBUG("Getting root key handle for HKEY %08llx", (uint64_t) (uintptr_t) hKey);
+
+ HANDLE result = hRootKeys[(uintptr_t) hKey - (uintptr_t) HKEY_CLASSES_ROOT];
+ if (result) {
+ FF_DEBUG("Found cached root key handle for %s -> %p", hKey2Str(result), result);
+ return result;
+ }
+
+ switch ((uintptr_t) hKey) {
+ case (uintptr_t) HKEY_CURRENT_USER: {
+ NTSTATUS status = RtlOpenCurrentUser(KEY_READ, &result);
+ if (!NT_SUCCESS(status)) {
+ FF_DEBUG("RtlOpenCurrentUser() failed: %s", ffDebugNtStatus(status));
+ return NULL;
+ }
+ break;
+ }
+
+ case (uintptr_t) HKEY_LOCAL_MACHINE: {
+ UNICODE_STRING path = RTL_CONSTANT_STRING(L"\\Registry\\Machine");
+ NTSTATUS status = NtOpenKey(&result, KEY_READ, &(OBJECT_ATTRIBUTES) {
+ .Length = sizeof(OBJECT_ATTRIBUTES),
+ .RootDirectory = NULL,
+ .ObjectName = &path,
+ .Attributes = OBJ_CASE_INSENSITIVE,
+ });
+ if (!NT_SUCCESS(status)) {
+ FF_DEBUG("NtOpenKey(%ls) failed: %s (0x%08lx)", path.Buffer, ffDebugNtStatus(status), status);
+ return NULL;
+ }
+ break;
+ }
+ default:
+ // Unsupported
+ FF_DEBUG("Unsupported root key: %p", hKey);
+ assert(false);
+ return NULL;
+ }
+ hRootKeys[(uintptr_t) hKey - (uintptr_t) HKEY_CLASSES_ROOT] = result;
+ FF_DEBUG("Opened root key %s -> %p", hKey2Str(result), result);
+ return result;
+}
+
+bool ffRegOpenSubkeyForRead(HANDLE hKey, const wchar_t* subKeyW, HANDLE* result, FFstrbuf* error) {
+ assert(hKey);
+ assert(subKeyW);
+ assert(result);
+
+ FF_DEBUG("Opening subkey %s\\%ls for read", hKey2Str(hKey), subKeyW);
+
+ USHORT subKeyLen = (USHORT) (wcslen(subKeyW) * sizeof(wchar_t));
+ if (!NT_SUCCESS(NtOpenKey(result, KEY_READ, &(OBJECT_ATTRIBUTES) {
+ .Length = sizeof(OBJECT_ATTRIBUTES),
+ .RootDirectory = hKey,
+ .ObjectName = &(UNICODE_STRING) {
+ .Length = subKeyLen,
+ .MaximumLength = subKeyLen + (USHORT) sizeof(wchar_t),
+ .Buffer = (wchar_t*) subKeyW,
+ },
+ }))) {
+ FF_DEBUG("NtOpenKey(%s\\<subkey>) failed", hKey2Str(hKey));
+ if (error) {
+ FF_STRBUF_AUTO_DESTROY subKeyA = ffStrbufCreateWS(subKeyW);
+ ffStrbufAppendF(error, "NtOpenKey(%s\\%s) failed", hKey2Str(hKey), subKeyA.chars);
+ }
+ return false;
+ }
+ FF_DEBUG("Opened subkey under %s -> %p", hKey2Str(hKey), *result);
+ return true;
+}
+
+static bool processRegValue(const FFRegValueArg* arg, const ULONG regType, const void* regData, ULONG regDataLen, FFstrbuf* error) {
+ switch (arg->type) {
+ case FF_ARG_TYPE_STRBUF: {
+ if (regType != REG_SZ && regType != REG_EXPAND_SZ) {
+ goto type_mismatch;
+ }
+
+ FFstrbuf* strbuf = (FFstrbuf*) arg->value;
+ uint32_t strLen = regDataLen / sizeof(wchar_t);
+ if (strLen == 0) {
+ ffStrbufClear(strbuf);
+ } else {
+ const wchar_t* ws = (const wchar_t*) regData;
+ if (ws[strLen - 1] == L'\0') {
+ --strLen;
+ }
+ ffStrbufSetNWS(strbuf, strLen, ws);
+ }
+ break;
+ }
+
+ case FF_ARG_TYPE_UINT:
+ case FF_ARG_TYPE_UINT64:
+ case FF_ARG_TYPE_UINT16:
+ case FF_ARG_TYPE_UINT8:
+ case FF_ARG_TYPE_BOOL: {
+ uint64_t value = 0;
+
+ if (regType == REG_DWORD) {
+ if (regDataLen < sizeof(uint32_t)) {
+ goto type_mismatch;
+ }
+ value = *(uint32_t*) regData;
+ } else if (regType == REG_QWORD) {
+ if (regDataLen < sizeof(uint64_t)) {
+ goto type_mismatch;
+ }
+ value = *(uint64_t*) regData;
+ } else {
+ goto type_mismatch;
+ }
+
+ if (arg->type == FF_ARG_TYPE_UINT) {
+ *(uint32_t*) arg->value = (uint32_t) value;
+ } else if (arg->type == FF_ARG_TYPE_UINT64) {
+ *(uint64_t*) arg->value = (uint64_t) value;
+ } else if (arg->type == FF_ARG_TYPE_UINT16) {
+ *(uint16_t*) arg->value = (uint16_t) value;
+ } else if (arg->type == FF_ARG_TYPE_UINT8) {
+ *(uint8_t*) arg->value = (uint8_t) value;
+ } else if (arg->type == FF_ARG_TYPE_BOOL) {
+ *(bool*) arg->value = value != 0;
+ }
+ break;
+ }
+
+ case FF_ARG_TYPE_FLOAT: {
+ if (regDataLen < sizeof(float)) {
+ goto type_mismatch;
+ }
+ *(float*) arg->value = *(float*) regData;
+ break;
+ }
+
+ case FF_ARG_TYPE_DOUBLE: {
+ if (regDataLen < sizeof(double)) {
+ goto type_mismatch;
+ }
+ *(double*) arg->value = *(double*) regData;
+ break;
+ }
+
+ case FF_ARG_TYPE_LIST: {
+ if (regType != REG_MULTI_SZ) {
+ goto type_mismatch;
+ }
+
+ FFlist* list = (FFlist*) arg->value;
+ ffListClear(list);
+
+ for (
+ const wchar_t* ptr = (const wchar_t*) regData;
+ (const uint8_t*) ptr < (const uint8_t*) regData + regDataLen && *ptr;
+ ptr++) {
+ uint32_t strLen = (uint32_t) wcsnlen(ptr, regDataLen / sizeof(wchar_t) - (size_t) (ptr - (const wchar_t*) regData));
+ ffStrbufInitNWS(FF_LIST_ADD(FFstrbuf, *list), strLen, ptr);
+ ptr += strLen;
+ }
+ break;
+ }
+
+ case FF_ARG_TYPE_BUFFER: {
+ FFArgBuffer* buffer = (FFArgBuffer*) arg->value;
+ if (buffer->length == 0) {
+ buffer->data = malloc(regDataLen);
+ } else if (buffer->length < regDataLen) {
+ if (error) {
+ FF_STRBUF_AUTO_DESTROY nameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
+ ffStrbufAppendF(error, "ffRegReadValues(%s) buffer too small (%u): expected %u", nameA.chars, (unsigned) buffer->length, (unsigned) regDataLen);
+ }
+ return false;
+ }
+ buffer->length = regDataLen;
+ memcpy(buffer->data, regData, regDataLen);
+ break;
+ }
+
+ case FF_ARG_TYPE_INT: // Use UINT instead
+ case FF_ARG_TYPE_STRING:
+ case FF_ARG_TYPE_NULL:
+ default:
+ if (error) {
+ FF_STRBUF_AUTO_DESTROY nameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
+ ffStrbufAppendF(error, "processRegValue(%s) unsupported FFArgType %u", nameA.chars, (unsigned) arg->type);
+ }
+ return false;
+ }
+
+ return true;
+
+type_mismatch:
+ FF_DEBUG("ffRegReadValues(%ls) type mismatch: regType=%u, argType=%u, dataLen=%u",
+ arg->name ?: L"(default)",
+ (unsigned) regType,
+ (unsigned) arg->type,
+ (unsigned) regDataLen);
+ if (error) {
+ FF_STRBUF_AUTO_DESTROY nameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
+ ffStrbufAppendF(error, "ffRegReadValues(%s) type mismatch: regType=%u, argType=%u, dataLen=%u", nameA.chars, (unsigned) regType, (unsigned) arg->type, (unsigned) regDataLen);
+ }
+ return false;
+}
+
+bool ffRegReadValue(HANDLE hKey, const FFRegValueArg* arg, FFstrbuf* error) {
+ UNICODE_STRING* valueNameU = &(UNICODE_STRING) {
+ .Length = arg->name ? (USHORT) (wcslen(arg->name) * sizeof(wchar_t)) : 0 /*(default)*/,
+ .MaximumLength = 0,
+ .Buffer = (wchar_t*) arg->name,
+ };
+
+ alignas(KEY_VALUE_PARTIAL_INFORMATION) uint8_t staticBuffer[128 + sizeof(KEY_VALUE_PARTIAL_INFORMATION)];
+ FF_AUTO_FREE uint8_t* dynamicBuffer = NULL;
+
+ KEY_VALUE_PARTIAL_INFORMATION* buffer = (KEY_VALUE_PARTIAL_INFORMATION*) &staticBuffer;
+ DWORD bufSize = sizeof(staticBuffer);
+ if (NT_SUCCESS(NtQueryValueKey(hKey, valueNameU, KeyValuePartialInformation, buffer, bufSize, &bufSize))) {
+ goto process_value;
+ }
+
+ if (bufSize == 0) {
+ FF_DEBUG("NtQueryValueKey(%p, %ls) failed (bufSize=0)", hKey, arg->name ?: L"(default)");
+ if (error) {
+ FF_STRBUF_AUTO_DESTROY valueNameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
+ ffStrbufAppendF(error, "NtQueryValueKey(%p, %s) failed", hKey, valueNameA.chars);
+ }
+ return false;
+ }
+
+ dynamicBuffer = (uint8_t*) malloc(bufSize);
+ buffer = (KEY_VALUE_PARTIAL_INFORMATION*) dynamicBuffer;
+
+ if (!NT_SUCCESS(NtQueryValueKey(hKey, valueNameU, KeyValuePartialInformation, buffer, bufSize, &bufSize))) {
+ FF_DEBUG("NtQueryValueKey(%p, %ls, buffer=%u) failed", hKey, arg->name ?: L"(default)", (unsigned) bufSize);
+ if (error) {
+ FF_STRBUF_AUTO_DESTROY valueNameA = arg->name ? ffStrbufCreateWS(arg->name) : ffStrbufCreateStatic("(default)");
+ ffStrbufAppendF(error, "NtQueryValueKey(%p, %s, buffer) failed", hKey, valueNameA.chars);
+ }
+ return false;
+ }
+
+process_value:
+ FF_DEBUG("Read value from %p (%ls), type=%u, len=%u", hKey, arg->name ?: L"(default)", (unsigned) buffer->Type, (unsigned) buffer->DataLength);
+ return processRegValue(arg, buffer->Type, buffer->Data, buffer->DataLength, error);
+}
+
+bool ffRegReadValues(HANDLE hKey, uint32_t argc, const FFRegValueArg argv[], FFstrbuf* error) {
+ if (__builtin_expect(argc == 0, false)) {
+ return true;
+ }
+
+ assert(argv);
+
+ FF_AUTO_FREE UNICODE_STRING* names = (UNICODE_STRING*) calloc(argc, sizeof(*names));
+ FF_AUTO_FREE KEY_VALUE_ENTRY* entries = (KEY_VALUE_ENTRY*) calloc(argc, sizeof(*entries));
+
+ for (uint32_t i = 0; i < argc; ++i) {
+ if (__builtin_expect(!argv[i].value, false)) {
+ FF_DEBUG("ffRegReadValues(argv[%u].value) is NULL", (unsigned) i);
+ if (error) {
+ ffStrbufAppendF(error, "ffRegReadValues(argv[%u].pVar) is NULL", (unsigned) i);
+ }
+ return false;
+ }
+
+ names[i] = (UNICODE_STRING) {
+ .Length = argv[i].name ? (USHORT) (wcslen(argv[i].name) * sizeof(wchar_t)) : 0 /*(default)*/,
+ .MaximumLength = 0,
+ .Buffer = (wchar_t*) argv[i].name,
+ };
+ entries[i].ValueName = &names[i];
+ }
+
+ ULONG bufferSize = argc * 128;
+ if (bufferSize < 512) {
+ bufferSize = 512;
+ }
+
+ FF_AUTO_FREE uint8_t* buffer = NULL;
+
+ while (true) {
+ buffer = (uint8_t*) realloc(buffer, bufferSize);
+
+ ULONG writtenSize = bufferSize;
+ ULONG requiredSize = 0;
+ NTSTATUS status = NtQueryMultipleValueKey(hKey, entries, argc, buffer, &writtenSize, &requiredSize);
+
+ if (!NT_SUCCESS(status)) {
+ // Buffer too small: docs guarantee requiredSize is returned when provided.
+ if (requiredSize > bufferSize) {
+ FF_DEBUG("NtQueryMultipleValueKey(%p) resize buffer: %u -> %u", hKey, (unsigned) bufferSize, (unsigned) requiredSize);
+ bufferSize = requiredSize;
+ continue;
+ }
+
+ FF_DEBUG("NtQueryMultipleValueKey(%p, argc=%u) failed, status=0x%08X", hKey, (unsigned) argc, (unsigned) status);
+ if (error) {
+ ffStrbufAppendF(error, "NtQueryMultipleValueKey(%p, argc=%u) failed, status=0x%08X", hKey, (unsigned) argc, (unsigned) status);
+ }
+ return false;
+ }
+
+ break;
+ }
+
+ for (uint32_t i = 0; i < argc; ++i) {
+ const FFRegValueArg* arg = &argv[i];
+ const KEY_VALUE_ENTRY* entry = &entries[i];
+
+ FF_DEBUG("Read value[%u] from %p: type=%u, len=%u", (unsigned) i, hKey, (unsigned) entry->Type, (unsigned) entry->DataLength);
+ if (!processRegValue(arg, entry->Type, buffer + entry->DataOffset, entry->DataLength, error)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool ffRegGetSubKey(HANDLE hKey, uint32_t index, FFstrbuf* result, FFstrbuf* error) {
+ assert(hKey);
+ assert(result);
+
+ alignas(KEY_BASIC_INFORMATION) uint8_t buffer[sizeof(KEY_BASIC_INFORMATION) + MAX_PATH * sizeof(wchar_t)];
+ ULONG bufSize = (ULONG) sizeof(buffer);
+ KEY_BASIC_INFORMATION* keyInfo = (KEY_BASIC_INFORMATION*) buffer;
+
+ if (!NT_SUCCESS(NtEnumerateKey(hKey, index, KeyBasicInformation, keyInfo, bufSize, &bufSize))) {
+ FF_DEBUG("NtEnumerateKey(hKey=%p, index=%u) failed", hKey, (unsigned) index);
+ if (error) {
+ ffStrbufAppendF(error, "NtEnumerateKey(hKey=%p, %u, keyInfo) failed", hKey, (unsigned) index);
+ }
+ return false;
+ }
+
+ ffStrbufSetNWS(result, keyInfo->NameLength / sizeof(wchar_t), keyInfo->Name);
+ return true;
+}
+
+bool ffRegGetNSubKeys(HANDLE hKey, uint32_t* result, FFstrbuf* error) {
+ assert(hKey);
+ assert(result);
+
+ alignas(KEY_FULL_INFORMATION) uint8_t buffer[sizeof(KEY_FULL_INFORMATION) + MAX_PATH * sizeof(wchar_t)];
+ ULONG bufSize = sizeof(buffer);
+ KEY_FULL_INFORMATION* keyInfo = (KEY_FULL_INFORMATION*) buffer;
+
+ if (!NT_SUCCESS(NtQueryKey(hKey, KeyFullInformation, keyInfo, bufSize, &bufSize))) {
+ FF_DEBUG("NtQueryKey(hKey=%p, KeyFullInformation) failed", hKey);
+ if (error) {
+ ffStrbufAppendF(error, "NtQueryKey(hKey=%p, KeyFullInformation, keyInfo) failed", hKey);
+ }
+ return false;
+ }
+
+ *result = (uint32_t) keyInfo->SubKeys;
+ return true;
+}
diff --git a/src/common/windows/registry.h b/src/common/windows/registry.h
new file mode 100644
index 0000000..9745372
--- /dev/null
+++ b/src/common/windows/registry.h
@@ -0,0 +1,66 @@
+#pragma once
+
+#include "fastfetch.h"
+#include "common/argType.h"
+#include "common/io.h"
+
+#ifndef HKEY_CURRENT_USER
+ #define HKEY_CLASSES_ROOT ((HKEY) (ULONG_PTR) ((LONG) 0x80000000))
+ #define HKEY_CURRENT_USER ((HKEY) (ULONG_PTR) ((LONG) 0x80000001))
+ #define HKEY_LOCAL_MACHINE ((HKEY) (ULONG_PTR) ((LONG) 0x80000002))
+ #define HKEY_USERS ((HKEY) (ULONG_PTR) ((LONG) 0x80000003))
+ #define HKEY_PERFORMANCE_DATA ((HKEY) (ULONG_PTR) ((LONG) 0x80000004))
+ #define HKEY_CURRENT_CONFIG ((HKEY) (ULONG_PTR) ((LONG) 0x80000005))
+ #define HKEY_DYN_DATA ((HKEY) (ULONG_PTR) ((LONG) 0x80000006))
+ #define HKEY_CURRENT_USER_LOCAL_SETTINGS ((HKEY) (ULONG_PTR) ((LONG) 0x80000007))
+#endif
+
+typedef struct FFRegValueArg {
+ FFArgType type;
+ const void* value;
+ const wchar_t* name;
+} FFRegValueArg;
+
+HANDLE ffRegGetRootKeyHandle(HKEY hKey);
+bool ffRegOpenSubkeyForRead(HANDLE hKey, const wchar_t* subKeyW, HANDLE* result, FFstrbuf* error);
+bool ffRegReadValue(HANDLE hKey, const FFRegValueArg* arg, FFstrbuf* error);
+bool ffRegReadValues(HANDLE hKey, uint32_t argc, const FFRegValueArg argv[], FFstrbuf* error);
+bool ffRegGetSubKey(HANDLE hKey, uint32_t index, FFstrbuf* result, FFstrbuf* error);
+bool ffRegGetNSubKeys(HANDLE hKey, uint32_t* result, FFstrbuf* error);
+
+static inline bool ffRegOpenKeyForRead(HKEY hRootKey, const wchar_t* subKeyW, HANDLE* result, FFstrbuf* error) {
+ return ffRegOpenSubkeyForRead(ffRegGetRootKeyHandle(hRootKey), subKeyW, result, error);
+}
+
+static inline bool ffRegReadStrbuf(HANDLE hKey, const wchar_t* valueNameW, FFstrbuf* result, FFstrbuf* error) {
+ return ffRegReadValue(hKey, &(FFRegValueArg) {
+ .type = FF_ARG_TYPE_STRBUF,
+ .value = result,
+ .name = valueNameW,
+ },
+ error);
+}
+static inline bool ffRegReadUint(HANDLE hKey, const wchar_t* valueNameW, uint32_t* result, FFstrbuf* error) {
+ return ffRegReadValue(hKey, &(FFRegValueArg) {
+ .type = FF_ARG_TYPE_UINT,
+ .value = result,
+ .name = valueNameW,
+ },
+ error);
+}
+static inline bool ffRegReadUint64(HANDLE hKey, const wchar_t* valueNameW, uint64_t* result, FFstrbuf* error) {
+ return ffRegReadValue(hKey, &(FFRegValueArg) {
+ .type = FF_ARG_TYPE_UINT64,
+ .value = result,
+ .name = valueNameW,
+ },
+ error);
+}
+static inline bool ffRegReadData(HANDLE hKey, const wchar_t* valueNameW, FFArgBuffer* buffer, FFstrbuf* error) {
+ return ffRegReadValue(hKey, &(FFRegValueArg) {
+ .type = FF_ARG_TYPE_BUFFER,
+ .value = buffer,
+ .name = valueNameW,
+ },
+ error);
+}
diff --git a/src/common/windows/unicode.c b/src/common/windows/unicode.c
new file mode 100644
index 0000000..06008ba
--- /dev/null
+++ b/src/common/windows/unicode.c
@@ -0,0 +1,44 @@
+#include "unicode.h"
+
+#include "common/windows/nt.h"
+
+void ffStrbufSetNWS(FFstrbuf* result, uint32_t length, const wchar_t* source) {
+ if (!length) {
+ ffStrbufClear(result);
+ return;
+ }
+
+ ULONG size_needed = 0;
+ NTSTATUS status = RtlUnicodeToUTF8N(NULL, 0, &size_needed, source, length * sizeof(wchar_t));
+
+ if (size_needed == 0) {
+ ffStrbufSetF(result, "RtlUnicodeToUTF8N failed: %X", (unsigned) status);
+ return;
+ }
+
+ ffStrbufEnsureFixedLengthFree(result, size_needed);
+ RtlUnicodeToUTF8N(result->chars, size_needed, &size_needed, source, length * sizeof(wchar_t));
+
+ result->length = size_needed;
+ result->chars[size_needed] = '\0';
+}
+
+void ffStrbufAppendNWS(FFstrbuf* result, uint32_t length, const wchar_t* source) {
+ if (!length) {
+ return;
+ }
+
+ ULONG size_needed = 0;
+ NTSTATUS status = RtlUnicodeToUTF8N(NULL, 0, &size_needed, source, length * sizeof(wchar_t));
+
+ if (size_needed == 0) {
+ ffStrbufAppendF(result, "RtlUnicodeToUTF8N failed: %X", (unsigned) status);
+ return;
+ }
+
+ ffStrbufEnsureFree(result, size_needed);
+ RtlUnicodeToUTF8N(result->chars + result->length, size_needed, &size_needed, source, length * sizeof(wchar_t));
+
+ result->length += size_needed;
+ result->chars[result->length] = '\0';
+}
diff --git a/src/common/windows/unicode.h b/src/common/windows/unicode.h
new file mode 100644
index 0000000..f58b425
--- /dev/null
+++ b/src/common/windows/unicode.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "common/FFstrbuf.h"
+#include <wchar.h>
+
+void ffStrbufSetNWS(FFstrbuf* result, uint32_t length, const wchar_t* source);
+void ffStrbufAppendNWS(FFstrbuf* result, uint32_t length, const wchar_t* source);
+
+static inline void ffStrbufSetWS(FFstrbuf* result, const wchar_t* source) {
+ if (!source) {
+ return ffStrbufClear(result);
+ }
+ return ffStrbufSetNWS(result, (uint32_t) wcslen(source), source);
+}
+
+static inline void ffStrbufAppendWS(FFstrbuf* result, const wchar_t* source) {
+ if (!source) {
+ return;
+ }
+ return ffStrbufAppendNWS(result, (uint32_t) wcslen(source), source);
+}
+
+static inline void ffStrbufInitNWS(FFstrbuf* result, uint32_t length, const wchar_t* source) {
+ ffStrbufInit(result);
+ return ffStrbufSetNWS(result, length, source);
+}
+
+static inline void ffStrbufInitWS(FFstrbuf* result, const wchar_t* source) {
+ if (!source) {
+ return ffStrbufInit(result);
+ }
+ return ffStrbufInitNWS(result, (uint32_t) wcslen(source), source);
+}
+
+static inline FFstrbuf ffStrbufCreateNWS(uint32_t length, const wchar_t* source) {
+ FFstrbuf result;
+ ffStrbufInitNWS(&result, length, source);
+ return result;
+}
+
+static inline FFstrbuf ffStrbufCreateWS(const wchar_t* source) {
+ if (!source) {
+ return ffStrbufCreate();
+ }
+ return ffStrbufCreateNWS((uint32_t) wcslen(source), source);
+}
diff --git a/src/common/windows/unicode.hpp b/src/common/windows/unicode.hpp
new file mode 100644
index 0000000..f9c355a
--- /dev/null
+++ b/src/common/windows/unicode.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#ifdef __cplusplus
+
+extern "C" {
+ #include "unicode.h"
+}
+
+ #include <string_view>
+
+static inline void ffStrbufInitWSV(FFstrbuf* result, const std::wstring_view source) {
+ return ffStrbufInitNWS(result, (uint32_t) source.size(), source.data());
+}
+
+static inline FFstrbuf ffStrbufCreateWSV(const std::wstring_view source) {
+ return ffStrbufCreateNWS((uint32_t) source.size(), source.data());
+}
+
+static inline void ffStrbufSetWSV(FFstrbuf* result, const std::wstring_view source) {
+ return ffStrbufSetNWS(result, (uint32_t) source.size(), source.data());
+}
+
+#else
+
+ #error Must be included in C++ source file
+
+#endif
diff --git a/src/common/windows/util.hpp b/src/common/windows/util.hpp
new file mode 100644
index 0000000..939f6bb
--- /dev/null
+++ b/src/common/windows/util.hpp
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <utility>
+#include <type_traits>
+
+template <typename Fn>
+struct on_scope_exit {
+ static_assert(std::is_nothrow_move_constructible<Fn>::value,
+ "Fn must be nothrow move constructible");
+
+ explicit on_scope_exit(Fn&& fn) noexcept
+ : _fn(std::move(fn)) {};
+ on_scope_exit(const on_scope_exit&) = delete;
+ on_scope_exit& operator=(const on_scope_exit&) = delete;
+ ~on_scope_exit() noexcept {
+ this->_fn();
+ }
+
+ private:
+ Fn _fn;
+};
diff --git a/src/common/windows/variant.cpp b/src/common/windows/variant.cpp
new file mode 100644
index 0000000..d5610d3
--- /dev/null
+++ b/src/common/windows/variant.cpp
@@ -0,0 +1,20 @@
+#include "variant.hpp"
+
+#include <oleauto.h>
+
+FFWmiVariant::FFWmiVariant(std::initializer_list<PCWSTR> strings) : FFWmiVariant() {
+ SAFEARRAYBOUND bound = {
+ .cElements = (ULONG) strings.size(),
+ .lLbound = 0,
+ };
+ SAFEARRAY* psa = SafeArrayCreate(VT_BSTR, 1, &bound);
+
+ LONG i = 0;
+ for (PCWSTR str : strings) {
+ SafeArrayPutElement(psa, &i, bstr_t(str));
+ ++i;
+ }
+
+ this->vt = VT_ARRAY | VT_BSTR;
+ this->parray = psa;
+}
diff --git a/src/common/windows/variant.hpp b/src/common/windows/variant.hpp
new file mode 100644
index 0000000..58e46b7
--- /dev/null
+++ b/src/common/windows/variant.hpp
@@ -0,0 +1,164 @@
+#include <oaidl.h>
+#include <propidl.h>
+#include <type_traits>
+#include <utility>
+#include <string_view>
+#include <cassert>
+#include <cstdint>
+
+template <typename TVariant>
+struct FFBaseVariant : TVariant {
+ bool hasValue() {
+ return this->vt != VT_EMPTY;
+ }
+
+ explicit operator bool() {
+ return this->hasValue();
+ }
+
+ template <typename T>
+ T get() {
+ // boolean
+ if constexpr (std::is_same_v<T, bool>) {
+ assert(this->vt == VT_BOOL);
+ return this->boolVal != VARIANT_FALSE;
+ }
+
+ // signed
+ else if constexpr (std::is_same_v<T, int8_t>) {
+ assert(this->vt == VT_I1);
+ return this->cVal;
+ } else if constexpr (std::is_same_v<T, int16_t>) {
+ assert(this->vt == VT_I2);
+ return this->iVal;
+ } else if constexpr (std::is_same_v<T, int32_t>) {
+ assert(this->vt == VT_I4 || this->vt == VT_INT);
+ return this->intVal;
+ } else if constexpr (std::is_same_v<T, int64_t>) {
+ assert(this->vt == VT_I8);
+ return this->llVal;
+ }
+
+ // unsigned
+ else if constexpr (std::is_same_v<T, uint8_t>) {
+ assert(this->vt == VT_UI1);
+ return this->bVal;
+ } else if constexpr (std::is_same_v<T, uint16_t>) {
+ assert(this->vt == VT_UI2);
+ return this->uiVal;
+ } else if constexpr (std::is_same_v<T, uint32_t>) {
+ assert(this->vt == VT_UI4 || this->vt == VT_UINT);
+ return this->uintVal;
+ } else if constexpr (std::is_same_v<T, uint64_t>) {
+ assert(this->vt == VT_UI8);
+ return this->ullVal;
+ }
+
+ // decimal
+ else if constexpr (std::is_same_v<T, float>) {
+ assert(this->vt == VT_R4);
+ return this->fltVal;
+ } else if constexpr (std::is_same_v<T, double>) {
+ assert(this->vt == VT_R8);
+ return this->dblVal;
+ }
+
+ // string
+ else if constexpr (std::is_same_v<T, std::string_view>) {
+ assert(this->vt == VT_LPSTR);
+ return this->pcVal;
+ } else if constexpr (std::is_same_v<T, std::wstring_view>) {
+ assert(this->vt == VT_BSTR || this->vt == VT_LPWSTR);
+ if (this->vt == VT_LPWSTR) {
+ return this->bstrVal;
+ } else {
+ return { this->bstrVal, SysStringLen(this->bstrVal) };
+ }
+ }
+
+ // array signed
+ else if constexpr (std::is_same_v<T, std::pair<const int8_t*, uint32_t>>) {
+ assert(this->vt & VT_ARRAY);
+ assert((this->vt & ~VT_ARRAY) == VT_I1);
+ return std::make_pair((int8_t*) this->parray->pvData, this->parray->cDims);
+ } else if constexpr (std::is_same_v<T, std::pair<const int16_t*, uint32_t>>) {
+ assert(this->vt & VT_ARRAY);
+ assert((this->vt & ~VT_ARRAY) == VT_I2);
+ return std::make_pair((int16_t*) this->parray->pvData, this->parray->cDims);
+ } else if constexpr (std::is_same_v<T, std::pair<const int32_t*, uint32_t>>) {
+ assert(this->vt & VT_ARRAY);
+ assert((this->vt & ~VT_ARRAY) == VT_I4);
+ return std::make_pair((int32_t*) this->parray->pvData, this->parray->cDims);
+ } else if constexpr (std::is_same_v<T, std::pair<const int64_t*, uint32_t>>) {
+ assert(this->vt & VT_ARRAY);
+ assert((this->vt & ~VT_ARRAY) == VT_I8);
+ return std::make_pair((int64_t*) this->parray->pvData, this->parray->cDims);
+ }
+
+ // array unsigned
+ else if constexpr (std::is_same_v<T, std::pair<const uint8_t*, uint32_t>>) {
+ assert(this->vt & VT_ARRAY);
+ assert((this->vt & ~VT_ARRAY) == VT_UI1);
+ return std::make_pair((uint8_t*) this->parray->pvData, this->parray->cDims);
+ } else if constexpr (std::is_same_v<T, std::pair<const uint16_t*, uint32_t>>) {
+ assert(this->vt & VT_ARRAY);
+ assert((this->vt & ~VT_ARRAY) == VT_UI2);
+ return std::make_pair((uint16_t*) this->parray->pvData, this->parray->cDims);
+ } else if constexpr (std::is_same_v<T, std::pair<const uint32_t*, uint32_t>>) {
+ assert(this->vt & VT_ARRAY);
+ assert((this->vt & ~VT_ARRAY) == VT_UI4);
+ return std::make_pair((uint32_t*) this->parray->pvData, this->parray->cDims);
+ } else if constexpr (std::is_same_v<T, std::pair<const uint64_t*, uint32_t>>) {
+ assert(this->vt & VT_ARRAY);
+ assert((this->vt & ~VT_ARRAY) == VT_UI8);
+ return std::make_pair((uint64_t*) this->parray->pvData, this->parray->cDims);
+ } else {
+ assert(false && "unsupported type");
+ __builtin_unreachable();
+ }
+ }
+};
+
+struct FFWmiVariant : FFBaseVariant<VARIANT> {
+ FFWmiVariant(const FFWmiVariant&) = delete;
+ FFWmiVariant(FFWmiVariant&&); // don't define it to enforce NRVO optimization
+ explicit FFWmiVariant() {
+ VariantInit(this);
+ }
+ explicit FFWmiVariant(std::initializer_list<PCWSTR> strings);
+ ~FFWmiVariant() {
+ VariantClear(this);
+ }
+};
+static_assert(sizeof(FFWmiVariant) == sizeof(VARIANT), "");
+
+struct FFPropVariant : FFBaseVariant<PROPVARIANT> {
+ FFPropVariant(const FFPropVariant&) = delete;
+ FFPropVariant(FFPropVariant&&); // don't define it to enforce NRVO optimization
+ explicit FFPropVariant() {
+ PropVariantInit(this);
+ }
+ ~FFPropVariant() {
+ PropVariantClear(this);
+ }
+};
+static_assert(sizeof(FFPropVariant) == sizeof(PROPVARIANT), "");
+
+namespace {
+// Provide our bstr_t to avoid libstdc++ dependency
+struct bstr_t {
+ explicit bstr_t(const wchar_t* str) noexcept : _bstr(SysAllocString(str)) {}
+ ~bstr_t(void) noexcept {
+ SysFreeString(_bstr);
+ }
+ explicit operator const wchar_t*(void) const noexcept {
+ return _bstr;
+ }
+ operator BSTR(void) const noexcept {
+ return _bstr;
+ }
+
+ private:
+ BSTR _bstr;
+};
+} // namespace
diff --git a/src/common/windows/version.c b/src/common/windows/version.c
new file mode 100644
index 0000000..4d61c78
--- /dev/null
+++ b/src/common/windows/version.c
@@ -0,0 +1,77 @@
+#include "common/debug.h"
+#include "common/mallocHelper.h"
+#include "common/windows/version.h"
+#include "common/windows/unicode.h"
+
+#include <windows.h>
+
+bool ffGetFileVersion(const wchar_t* filePath, const wchar_t* stringName, FFstrbuf* version) {
+ FF_DEBUG("ffGetFileVersion: enter filePath=%ls stringName=%ls", filePath, stringName);
+
+ DWORD handle;
+ DWORD size = GetFileVersionInfoSizeW(filePath, &handle);
+ if (size == 0) {
+ FF_DEBUG("GetFileVersionInfoSizeW failed: %s", ffDebugWin32Error(GetLastError()));
+ return false;
+ }
+
+ FF_DEBUG("GetFileVersionInfoSizeW ok: size=%lu handle=%lu",
+ (unsigned long) size,
+ (unsigned long) handle);
+
+ FF_AUTO_FREE void* versionData = malloc(size);
+ if (!versionData) {
+ FF_DEBUG("malloc failed: size=%lu", (unsigned long) size);
+ return false;
+ }
+
+ if (!GetFileVersionInfoW(filePath, handle, size, versionData)) {
+ FF_DEBUG("GetFileVersionInfoW failed: %s", ffDebugWin32Error(GetLastError()));
+ return false;
+ }
+
+ FF_DEBUG("GetFileVersionInfoW ok");
+
+ if (!stringName) {
+ VS_FIXEDFILEINFO* verInfo;
+ UINT len;
+ if (VerQueryValueW(versionData, L"\\", (void**) &verInfo, &len) &&
+ len &&
+ verInfo->dwSignature == 0xFEEF04BD) {
+ ffStrbufSetF(version, "%u.%u.%u.%u", (unsigned) ((verInfo->dwProductVersionMS >> 16) & 0xffff), (unsigned) ((verInfo->dwProductVersionMS >> 0) & 0xffff), (unsigned) ((verInfo->dwProductVersionLS >> 16) & 0xffff), (unsigned) ((verInfo->dwProductVersionLS >> 0) & 0xffff));
+ FF_DEBUG("fixed version resolved: %s", version->chars);
+ return true;
+ }
+
+ FF_DEBUG("fixed version query failed or invalid signature");
+ return false;
+ }
+
+ struct {
+ WORD language;
+ WORD codePage;
+ }* translations;
+ UINT translationsLen;
+
+ if (VerQueryValueW(versionData, L"\\VarFileInfo\\Translation", (void**) &translations, &translationsLen) &&
+ translationsLen >= sizeof(*translations)) {
+ wchar_t subBlock[128];
+ snwprintf(subBlock, ARRAY_SIZE(subBlock), L"\\StringFileInfo\\%04x%04x\\%ls", translations[0].language, translations[0].codePage, stringName);
+ FF_DEBUG("query version string with translation: %ls", subBlock);
+
+ wchar_t* value;
+ UINT valueLen; // Number of characters, including null terminator
+ if (VerQueryValueW(versionData, subBlock, (void**) &value, &valueLen) && valueLen > 0) {
+ ffStrbufSetNWS(version, valueLen - 1, value);
+ FF_DEBUG("version string resolved (translation fallback): %s", version->chars);
+ return true;
+ }
+
+ FF_DEBUG("translation fallback query failed");
+ } else {
+ FF_DEBUG("no translation table found in version resource");
+ }
+
+ FF_DEBUG("ffGetFileVersion failed");
+ return false;
+}
diff --git a/src/common/windows/version.h b/src/common/windows/version.h
new file mode 100644
index 0000000..457c053
--- /dev/null
+++ b/src/common/windows/version.h
@@ -0,0 +1,14 @@
+#include "fastfetch.h"
+
+/**
+ * @brief Retrieves a specific version string for a Windows file.
+ *
+ * This function gets a version string from a Windows file's version information.
+ *
+ * @param filePath The path to the file for which version information is requested.
+ * @param stringName The name of the specific version string to retrieve (e.g., "FileVersion", "ProductVersion").
+ * @param version Pointer to an FFstrbuf where the version string will be stored.
+ *
+ * @return true if the version string was successfully retrieved, false otherwise.
+ */
+bool ffGetFileVersion(const wchar_t* filePath, const wchar_t* stringName, FFstrbuf* version);
diff --git a/src/common/windows/version.rc b/src/common/windows/version.rc
new file mode 100644
index 0000000..f01ca7f
--- /dev/null
+++ b/src/common/windows/version.rc
@@ -0,0 +1,37 @@
+#ifdef RC_INVOKED
+
+#include <winuser.h>
+#include <winver.h>
+#include <ntdef.h>
+#include "fastfetch_config.h"
+
+#define FF_TO_STR1(str) #str
+#define FF_TO_STR(str) FF_TO_STR1(str)
+
+CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "manifest.xml"
+id ICON "logo.ico"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION FASTFETCH_PROJECT_VERSION_MAJOR,FASTFETCH_PROJECT_VERSION_MINOR,FASTFETCH_PROJECT_VERSION_PATCH,FASTFETCH_PROJECT_VERSION_TWEAK_NUM
+ PRODUCTVERSION FASTFETCH_PROJECT_VERSION_MAJOR,FASTFETCH_PROJECT_VERSION_MINOR,FASTFETCH_PROJECT_VERSION_PATCH,FASTFETCH_PROJECT_VERSION_TWEAK_NUM
+ FILEOS VOS_NT
+ FILETYPE VFT_APP
+{
+ BLOCK "StringFileInfo" {
+ BLOCK "040904b0" {
+ VALUE "Comments", FASTFETCH_PROJECT_DESCRIPTION
+ VALUE "FileDescription", FF_TO_STR(FASTFETCH_TARGET_BINARY_NAME)
+ VALUE "FileVersion", FASTFETCH_PROJECT_VERSION FASTFETCH_PROJECT_VERSION_TWEAK
+ VALUE "InternalName", FF_TO_STR(FASTFETCH_TARGET_BINARY_NAME)
+ VALUE "LegalCopyright", FASTFETCH_PROJECT_LICENSE
+ VALUE "OriginalFilename", FF_TO_STR(FASTFETCH_TARGET_BINARY_NAME) ".exe"
+ VALUE "ProductName", FASTFETCH_PROJECT_NAME " - " FASTFETCH_PROJECT_DESCRIPTION
+ VALUE "ProductVersion", FASTFETCH_PROJECT_VERSION FASTFETCH_PROJECT_VERSION_TWEAK
+ VALUE "CompanyName", FASTFETCH_PROJECT_HOMEPAGE_URL
+ }
+ }
+ BLOCK "VarFileInfo" {
+ VALUE "Translation", 0x0409, 1200
+ }
+}
+#endif
diff --git a/src/common/windows/wmi.h b/src/common/windows/wmi.h
new file mode 100644
index 0000000..aa50c00
--- /dev/null
+++ b/src/common/windows/wmi.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include <windef.h>
+#include <wmistr.h>
+#include <assert.h>
+
+/**
+ * The WmiOpenBlock function opens the WMI data block object for the specified WMI class.
+ *
+ * \param Guid Specifies the GUID for WMI class.
+ * \param DesiredAccess Specifies the desired access rights to the data block object.
+ * \param DataBlockHandle Pointer to a memory location where the routine returns a handle to the data block object.
+ * \return ULONG Successful or errant status.
+ */
+NTSYSAPI ULONG NTAPI
+WmiOpenBlock(
+ _In_ LPCGUID Guid,
+ _In_ ACCESS_MASK DesiredAccess,
+ _Out_ PHANDLE DataBlockHandle);
+
+/**
+ * The WmiQueryAllDataW function returns all WMI data blocks that implement a given WMI class (Unicode).
+ *
+ * \param DataBlockHandle Handle to a WMI data block object.
+ * \param BufferLength Pointer to a memory location that specifies the size of the buffer.
+ * \param Buffer Pointer to the buffer where the routine returns the WMI data.
+ * \return ULONG Successful or errant status.
+ */
+NTSYSAPI ULONG NTAPI
+WmiQueryAllDataW(
+ _In_ HANDLE DataBlockHandle,
+ _Inout_ PULONG BufferLength,
+ _Out_writes_bytes_opt_(*BufferLength) PVOID Buffer);
+
+/**
+ * The WmiCloseBlock function closes a WMI data block object.
+ *
+ * \param DataBlockHandle Handle to the data block object to be closed.
+ * \return ULONG Successful or errant status.
+ */
+NTSYSAPI ULONG NTAPI
+WmiCloseBlock(
+ _In_ HANDLE DataBlockHandle);
+
+static inline void ffCloseWmiBlock(HANDLE* hBlock) {
+ assert(hBlock);
+ if (*hBlock) {
+ WmiCloseBlock(*hBlock);
+ }
+}
+
+#define FF_AUTO_CLOSE_WMI_BLOCK __attribute__((cleanup(ffCloseWmiBlock)))
+
+// MOF: https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/km/wmicore.mof