diff options
Diffstat (limited to 'src/common')
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 Binary files differnew file mode 100644 index 0000000..cdd89ad --- /dev/null +++ b/src/common/windows/logo.ico 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 |