diff options
| author | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
|---|---|---|
| committer | sumuel <samuel@yakubos.org> | 2026-08-17 20:44:55 +0000 |
| commit | 76424950e373d3b04ac3dd13019151bfba3e8423 (patch) | |
| tree | 4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/common/impl | |
Add the files
Diffstat (limited to 'src/common/impl')
58 files changed, 12770 insertions, 0 deletions
diff --git a/src/common/impl/FFPlatform.c b/src/common/impl/FFPlatform.c new file mode 100644 index 0000000..ff4c45c --- /dev/null +++ b/src/common/impl/FFPlatform.c @@ -0,0 +1,94 @@ +#include "FFPlatform_private.h" +#include "common/strutil.h" +#include "common/io.h" +#include "detection/version/version.h" + +void ffPlatformInit(FFPlatform* platform) { + ffStrbufInit(&platform->homeDir); + ffStrbufInit(&platform->cacheDir); + ffListInit(&platform->configDirs); + ffListInit(&platform->dataDirs); + ffStrbufInit(&platform->exePath); + ffStrbufInit(&platform->cwd); + + ffStrbufInit(&platform->userName); + ffStrbufInit(&platform->fullUserName); + ffStrbufInit(&platform->hostName); + ffStrbufInit(&platform->userShell); + +#ifdef _WIN32 + ffStrbufInit(&platform->sid); +#endif + + FFPlatformSysinfo* info = &platform->sysinfo; + + ffStrbufInit(&info->name); + ffStrbufInit(&info->release); + ffStrbufInit(&info->version); + ffStrbufInit(&info->architecture); + info->pageSize = 0; + + ffPlatformInitImpl(platform); + + if (info->name.length == 0) { + ffStrbufSetStatic(&info->name, ffVersionResult.sysName); + } + + if (info->architecture.length == 0) { + ffStrbufSetStatic(&info->architecture, ffVersionResult.architecture); + } +} + +void ffPlatformDestroy(FFPlatform* platform) { + ffStrbufDestroy(&platform->homeDir); + ffStrbufDestroy(&platform->cacheDir); + + FF_LIST_FOR_EACH (FFstrbuf, dir, platform->configDirs) { + ffStrbufDestroy(dir); + } + ffListDestroy(&platform->configDirs); + + FF_LIST_FOR_EACH (FFstrbuf, dir, platform->dataDirs) { + ffStrbufDestroy(dir); + } + ffListDestroy(&platform->dataDirs); + ffStrbufDestroy(&platform->exePath); + ffStrbufDestroy(&platform->cwd); + + ffStrbufDestroy(&platform->userName); + ffStrbufDestroy(&platform->hostName); + ffStrbufDestroy(&platform->userShell); + ffStrbufDestroy(&platform->fullUserName); + +#ifdef _WIN32 + ffStrbufDestroy(&platform->sid); +#endif + + FFPlatformSysinfo* info = &platform->sysinfo; + ffStrbufDestroy(&info->architecture); + ffStrbufDestroy(&info->name); + ffStrbufDestroy(&info->release); + ffStrbufDestroy(&info->version); +} + +void ffPlatformPathAddAbsolute(FFlist* dirs, const char* path) { + if (!ffPathExists(path, FF_PATHTYPE_DIRECTORY)) { + return; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateS(path); + ffStrbufEnsureEndsWithC(&buffer, '/'); + if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) { + ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer); + } +} + +void ffPlatformPathAddHome(FFlist* dirs, const FFPlatform* platform, const char* suffix) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateA(64); + ffStrbufAppend(&buffer, &platform->homeDir); + ffStrbufAppendS(&buffer, suffix); + ffStrbufEnsureEndsWithC(&buffer, '/'); + if (ffPathExists(buffer.chars, FF_PATHTYPE_DIRECTORY) && !FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) { + ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer); + } +} diff --git a/src/common/impl/FFPlatform_private.h b/src/common/impl/FFPlatform_private.h new file mode 100644 index 0000000..e9dfd71 --- /dev/null +++ b/src/common/impl/FFPlatform_private.h @@ -0,0 +1,8 @@ +#pragma once + +#include "common/FFPlatform.h" + +void ffPlatformInitImpl(FFPlatform* platform); + +void ffPlatformPathAddAbsolute(FFlist* dirs, const char* path); +void ffPlatformPathAddHome(FFlist* dirs, const FFPlatform* platform, const char* suffix); diff --git a/src/common/impl/FFPlatform_unix.c b/src/common/impl/FFPlatform_unix.c new file mode 100644 index 0000000..de69f6b --- /dev/null +++ b/src/common/impl/FFPlatform_unix.c @@ -0,0 +1,321 @@ +#include "FFPlatform_private.h" +#include "common/FFstrbuf.h" +#include "common/arrutil.h" +#include "common/strutil.h" +#include "common/io.h" +#include "fastfetch_config.h" + +#include <unistd.h> +#include <pwd.h> +#include <limits.h> +#include <sys/utsname.h> +#include <paths.h> + +#ifdef __APPLE__ + #include <mach-o/dyld.h> + #include <sys/sysctl.h> +#elif defined(__FreeBSD__) || defined(__NetBSD__) + #include <sys/sysctl.h> +#elif defined(__OpenBSD__) + #include <sys/sysctl.h> + #include <sys/stat.h> + #include <kvm.h> + #include "common/path.h" +#elif defined(__HAIKU__) + #include <image.h> + #include <OS.h> +#endif + +static void getExePath(FFPlatform* platform) { + char exePath[PATH_MAX]; +#if defined(__linux__) || defined(__GNU__) + ssize_t exePathLen = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (exePathLen >= 0) { + exePath[exePathLen] = '\0'; + } +#elif defined(__APPLE__) + uint32_t exePathLen = sizeof(exePath); + if (_NSGetExecutablePath(exePath, &exePathLen) == 0) { + exePathLen = (uint32_t) strlen(exePath); + } else { + exePathLen = 0; + } +#elif defined(__FreeBSD__) || defined(__NetBSD__) + size_t exePathLen = sizeof(exePath); + if (sysctl( + (int[]) { CTL_KERN, + #ifdef __FreeBSD__ + KERN_PROC, + KERN_PROC_PATHNAME, + (pid_t) platform->pid + #else + KERN_PROC_ARGS, + (pid_t) platform->pid, + KERN_PROC_PATHNAME + #endif + }, + 4, + exePath, + &exePathLen, + NULL, + 0) < 0) + exePathLen = 0; + else { + exePathLen--; // remove terminating NUL + } +#elif defined(__OpenBSD__) + // OpenBSD doesn't have a reliable way to get the executable path. + // Current implementation uses argv[0], which can be easily spoofed. + // See #2195 + size_t exePathLen = 0; + kvm_t* kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, NULL); + if (kd) { + int kpCount; + struct kinfo_proc* kp = kvm_getprocs(kd, KERN_PROC_PID, (pid_t) platform->pid, sizeof(*kp), &kpCount); + if (kp && kpCount == 1) { + char** argv = kvm_getargv(kd, kp, 0); + if (argv && argv[0]) { + char* arg0 = argv[0]; + if (arg0[0]) { + if (strchr(arg0, '/') != NULL) // likely a path (absolute or relative) + { + exePathLen = strlen(arg0); + if (exePathLen < ARRAY_SIZE(exePath)) { + memcpy(exePath, arg0, exePathLen); + exePath[exePathLen] = '\0'; + } else { + exePathLen = 0; + } + } else { + FF_STRBUF_AUTO_DESTROY tmpPath = ffStrbufCreate(); + if (ffFindExecutableInPath(arg0, &tmpPath) == NULL && tmpPath.length < ARRAY_SIZE(exePath)) { + memcpy(exePath, tmpPath.chars, tmpPath.length + 1); + exePathLen = tmpPath.length; + } + } + + if (exePathLen > 0) { + struct stat st; + if (stat(exePath, &st) == 0 && S_ISREG(st.st_mode)) { + int cntp; + struct kinfo_file* kf = kvm_getfiles(kd, KERN_FILE_BYPID, (pid_t) platform->pid, sizeof(*kf), &cntp); + if (kf) { + int i; + for (i = 0; i < cntp; i++) { + if (kf[i].fd_fd == KERN_FILE_TEXT) { + // KERN_FILE_TEXT is the executable file, not a shared library, and should be unique in the list. + if (st.st_dev != (dev_t) kf[i].va_fsid || st.st_ino != (ino_t) kf[i].va_fileid) { + i = -1; + } + break; + } + } + if (i < 0) { + exePathLen = 0; + } + } else { + // If we can't get the list of open files, we can't verify that the file is actually the executable + // Assume it is + } + } else { + exePathLen = 0; + } + } + } + } + } + kvm_close(kd); + } +#elif defined(__sun) + ssize_t exePathLen = readlink("/proc/self/path/a.out", exePath, sizeof(exePath) - 1); + if (exePathLen >= 0) { + exePath[exePathLen] = '\0'; + } +#elif defined(__HAIKU__) + size_t exePathLen = 0; + image_info info; + int32 cookie = 0; + + while (get_next_image_info(B_CURRENT_TEAM, &cookie, &info) == B_OK) { + if (info.type == B_APP_IMAGE) { + exePathLen = strlcpy(exePath, info.name, sizeof(exePath)); + break; + } + } +#endif + if (exePathLen > 0) { + ffStrbufEnsureFree(&platform->exePath, PATH_MAX); + if (realpath(exePath, platform->exePath.chars)) { + ffStrbufRecalculateLength(&platform->exePath); + } else { + ffStrbufSetNS(&platform->exePath, (uint32_t) exePathLen, exePath); + } + } +} + +static void platformPathAddEnv(FFlist* dirs, const char* env) { + const char* envValue = getenv(env); + if (!ffStrSet(envValue)) { + return; + } + + FF_STRBUF_AUTO_DESTROY value = ffStrbufCreateA(64); + ffStrbufAppendS(&value, envValue); + + uint32_t startIndex = 0; + while (startIndex < value.length) { + uint32_t colonIndex = ffStrbufNextIndexC(&value, startIndex, ':'); + value.chars[colonIndex] = '\0'; + + if (!ffStrSet(value.chars + startIndex)) { + startIndex = colonIndex + 1; + continue; + } + + ffPlatformPathAddAbsolute(dirs, value.chars + startIndex); + + startIndex = colonIndex + 1; + } +} + +static void getHomeDir(FFPlatform* platform, const struct passwd* pwd) { + const char* home = pwd ? pwd->pw_dir : getenv("HOME"); + ffStrbufAppendS(&platform->homeDir, home); + ffStrbufEnsureEndsWithC(&platform->homeDir, '/'); +} + +static void getCacheDir(FFPlatform* platform) { + const char* cache = getenv("XDG_CACHE_HOME"); + if (ffStrSet(cache)) { + ffStrbufAppendS(&platform->cacheDir, cache); + ffStrbufEnsureEndsWithC(&platform->cacheDir, '/'); + } else { + ffStrbufAppend(&platform->cacheDir, &platform->homeDir); + ffStrbufAppendS(&platform->cacheDir, ".cache/"); + } +} + +static void getConfigDirs(FFPlatform* platform) { + // Always make sure `${XDG_CONFIG_HOME:-$HOME/.config}` is the first entry + platformPathAddEnv(&platform->configDirs, "XDG_CONFIG_HOME"); + ffPlatformPathAddHome(&platform->configDirs, platform, ".config/"); + +#if defined(__APPLE__) + ffPlatformPathAddHome(&platform->configDirs, platform, "Library/Preferences/"); + ffPlatformPathAddHome(&platform->configDirs, platform, "Library/Application Support/"); +#endif +#if defined(__HAIKU__) + ffPlatformPathAddHome(&platform->configDirs, platform, "config/settings/"); +#endif + + ffPlatformPathAddHome(&platform->configDirs, platform, ""); + platformPathAddEnv(&platform->configDirs, "XDG_CONFIG_DIRS"); + +#if !defined(__APPLE__) + ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_ETC "/xdg/"); +#endif + + ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_ETC "/"); + ffPlatformPathAddAbsolute(&platform->configDirs, FASTFETCH_TARGET_DIR_INSTALL_SYSCONF "/"); +} + +static void getDataDirs(FFPlatform* platform) { + platformPathAddEnv(&platform->dataDirs, "XDG_DATA_HOME"); + ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/"); + + // Add ${currentExePath}/../share + if (platform->exePath.length > 0) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreateCopy(&platform->exePath); + ffStrbufSubstrBeforeLastC(&path, '/'); + ffStrbufSubstrBeforeLastC(&path, '/'); + ffStrbufAppendS(&path, "/share"); + ffPlatformPathAddAbsolute(&platform->dataDirs, path.chars); + } + +#ifdef __APPLE__ + ffPlatformPathAddHome(&platform->dataDirs, platform, "Library/Application Support/"); +#endif + + ffPlatformPathAddHome(&platform->dataDirs, platform, ""); + platformPathAddEnv(&platform->dataDirs, "XDG_DATA_DIRS"); +#ifdef _PATH_LOCALBASE + ffPlatformPathAddAbsolute(&platform->dataDirs, _PATH_LOCALBASE "/share/"); +#endif + ffPlatformPathAddAbsolute(&platform->dataDirs, FASTFETCH_TARGET_DIR_USR "/local/share/"); + ffPlatformPathAddAbsolute(&platform->dataDirs, FASTFETCH_TARGET_DIR_USR "/share/"); +} + +static void getUserName(FFPlatform* platform, const struct passwd* pwd) { + if (pwd) { + ffStrbufSetS(&platform->userName, pwd->pw_name); + ffStrbufSetS(&platform->fullUserName, pwd->pw_gecos); + ffStrbufTrimSpace(&platform->fullUserName); + } else { + ffStrbufSetS(&platform->userName, getenv("USER")); + } +} + +static void getHostName(FFPlatform* platform, const struct utsname* uts) { + ffStrbufAppendS(&platform->hostName, uts->nodename); +} + +static void getUserShell(FFPlatform* platform, const struct passwd* pwd) { + const char* shell = getenv("SHELL"); + if (!ffStrSet(shell) && pwd) { + shell = pwd->pw_shell; + } + + ffStrbufAppendS(&platform->userShell, shell); +} + +static void getSysinfo(FFPlatformSysinfo* info, const struct utsname* uts) { + ffStrbufAppendS(&info->name, uts->sysname); + ffStrbufAppendS(&info->release, uts->release); + ffStrbufAppendS(&info->version, uts->version); +#ifdef __HAIKU__ + /* historical reason */ + if (ffStrEquals(uts->machine, "BePC")) { + ffStrbufSetStatic(&info->architecture, "i386"); + } else +#endif + ffStrbufAppendS(&info->architecture, uts->machine); + +#if defined(__FreeBSD__) || defined(__APPLE__) || defined(__OpenBSD__) || defined(__NetBSD__) + size_t length = sizeof(info->pageSize); + sysctl((int[]) { CTL_HW, HW_PAGESIZE }, 2, &info->pageSize, &length, NULL, 0); +#else + info->pageSize = (uint32_t) sysconf(_SC_PAGESIZE); +#endif +} + +static void getCwd(FFPlatform* platform) { + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) != NULL) { + ffStrbufSetS(&platform->cwd, cwd); + ffStrbufEnsureEndsWithC(&platform->cwd, '/'); + } +} + +void ffPlatformInitImpl(FFPlatform* platform) { + platform->pid = (uint32_t) getpid(); + platform->uid = getuid(); + struct passwd* pwd = getpwuid(platform->uid); + + struct utsname uts; + if (uname(&uts) < 0) { + memset(&uts, 0, sizeof(uts)); + } + + getExePath(platform); + getCwd(platform); + getHomeDir(platform, pwd); + getCacheDir(platform); + getConfigDirs(platform); + getDataDirs(platform); + + getUserName(platform, pwd); + getHostName(platform, &uts); + getUserShell(platform, pwd); + + getSysinfo(&platform->sysinfo, &uts); +} diff --git a/src/common/impl/FFPlatform_windows.c b/src/common/impl/FFPlatform_windows.c new file mode 100644 index 0000000..083d20a --- /dev/null +++ b/src/common/impl/FFPlatform_windows.c @@ -0,0 +1,313 @@ +#include "FFPlatform_private.h" +#include "common/io.h" +#include "common/library.h" +#include "common/strutil.h" +#include "common/windows/unicode.h" +#include "common/windows/registry.h" +#include "common/windows/nt.h" + +#include <stdalign.h> +#include <windows.h> +#include <shlobj.h> +#include <sddl.h> + +#define SECURITY_WIN32 1 // For secext.h +#include <secext.h> + +static void getExePath(FFPlatform* platform) { + wchar_t exePathW[MAX_PATH]; + + FF_AUTO_CLOSE_FD HANDLE hPath = CreateFileW( + ffGetPeb()->ProcessParameters->ImagePathName.Buffer, + GENERIC_READ, + FILE_SHARE_READ, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + if (hPath != INVALID_HANDLE_VALUE) { + DWORD len = GetFinalPathNameByHandleW(hPath, exePathW, MAX_PATH, FILE_NAME_NORMALIZED); + if (len > 0 && len < MAX_PATH) { + ffStrbufSetNWS(&platform->exePath, len, exePathW); + if (ffStrbufStartsWithS(&platform->exePath, "\\\\?\\")) { + ffStrbufSubstrAfter(&platform->exePath, 3); + } + } + } + + if (platform->exePath.length == 0) { + PCUNICODE_STRING imagePathName = &ffGetPeb()->ProcessParameters->ImagePathName; + ffStrbufSetNWS(&platform->exePath, imagePathName->Length / sizeof(wchar_t), imagePathName->Buffer); + } + + ffStrbufReplaceAllC(&platform->exePath, '\\', '/'); +} + +static void getHomeDir(FFPlatform* platform) { + PWSTR pPath = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_Profile, KF_FLAG_DEFAULT, NULL, &pPath))) { + ffStrbufSetWS(&platform->homeDir, pPath); + ffStrbufReplaceAllC(&platform->homeDir, '\\', '/'); + ffStrbufEnsureEndsWithC(&platform->homeDir, '/'); + } else { + ffStrbufSetS(&platform->homeDir, getenv("USERPROFILE")); + ffStrbufReplaceAllC(&platform->homeDir, '\\', '/'); + ffStrbufEnsureEndsWithC(&platform->homeDir, '/'); + } + CoTaskMemFree(pPath); +} + +static void getCacheDir(FFPlatform* platform) { + PWSTR pPath = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_LocalAppData, KF_FLAG_DEFAULT, NULL, &pPath))) { + ffStrbufSetWS(&platform->cacheDir, pPath); + ffStrbufReplaceAllC(&platform->cacheDir, '\\', '/'); + ffStrbufEnsureEndsWithC(&platform->cacheDir, '/'); + } else { + ffStrbufAppend(&platform->cacheDir, &platform->homeDir); + ffStrbufAppendS(&platform->cacheDir, "AppData/Local/"); + } + CoTaskMemFree(pPath); +} + +static void platformPathAddKnownFolder(FFlist* dirs, REFKNOWNFOLDERID folderId) { + PWSTR pPath = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(folderId, KF_FLAG_DEFAULT, NULL, &pPath))) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateWS(pPath); + CoTaskMemFree(pPath); + ffStrbufReplaceAllC(&buffer, '\\', '/'); + ffStrbufEnsureEndsWithC(&buffer, '/'); + if (!FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) { + ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer); + } + } +} + +static void platformPathAddEnvSuffix(FFlist* dirs, const char* env, const char* suffix) { + const char* value = getenv(env); + if (!ffStrSet(value)) { + return; + } + + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreateA(64); + ffStrbufAppendS(&buffer, value); + ffStrbufReplaceAllC(&buffer, '\\', '/'); + ffStrbufEnsureEndsWithC(&buffer, '/'); + if (suffix) { + ffStrbufAppendS(&buffer, suffix); + ffStrbufEnsureEndsWithC(&buffer, '/'); + } + + if (ffPathExists(buffer.chars, FF_PATHTYPE_DIRECTORY) && !FF_LIST_CONTAINS(*dirs, &buffer, ffStrbufEqual)) { + ffStrbufInitMove(FF_LIST_ADD(FFstrbuf, *dirs), &buffer); + } +} + +static void getConfigDirs(FFPlatform* platform) { + if (getenv("MSYSTEM")) { + // We are in MSYS2 / Git Bash + platformPathAddEnvSuffix(&platform->configDirs, "HOME", ".config/"); + platformPathAddEnvSuffix(&platform->configDirs, "HOME", NULL); + platformPathAddEnvSuffix(&platform->configDirs, "MINGW_PREFIX", "etc"); + } + + ffPlatformPathAddHome(&platform->configDirs, platform, ".config/"); + platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_ProgramData); + platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_RoamingAppData); + platformPathAddKnownFolder(&platform->configDirs, &FOLDERID_LocalAppData); + ffPlatformPathAddHome(&platform->configDirs, platform, ""); +} + +static void getDataDirs(FFPlatform* platform) { + if (getenv("MSYSTEM") && getenv("HOME")) { + // We are in MSYS2 / Git Bash + platformPathAddEnvSuffix(&platform->dataDirs, "HOME", ".local/share/"); + platformPathAddEnvSuffix(&platform->dataDirs, "HOME", NULL); + platformPathAddEnvSuffix(&platform->dataDirs, "MINGW_PREFIX", "share"); + } + ffPlatformPathAddHome(&platform->dataDirs, platform, ".local/share/"); + platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_ProgramData); + platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_RoamingAppData); + platformPathAddKnownFolder(&platform->dataDirs, &FOLDERID_LocalAppData); + ffPlatformPathAddHome(&platform->dataDirs, platform, ""); +} + +static void getUserName(FFPlatform* platform) { + wchar_t buffer[256]; + DWORD size = ARRAY_SIZE(buffer); + if (GetUserNameExW(NameDisplay, buffer, &size)) { + ffStrbufSetWS(&platform->fullUserName, buffer); + } + + NTSYSAPI NTSTATUS NTAPI LsaGetUserName( + _Outptr_ PLSA_UNICODE_STRING * UserName, + _Outptr_opt_ PLSA_UNICODE_STRING * DomainName); + PLSA_UNICODE_STRING userName = NULL; + if (NT_SUCCESS(LsaGetUserName(&userName, NULL))) { + ffStrbufSetNWS(&platform->userName, userName->Length / sizeof(wchar_t), userName->Buffer); + RtlFreeUnicodeString(userName); // Required. userName.Buffer is allocated separately + LsaFreeMemory(userName); + } else { + ffStrbufSetS(&platform->userName, getenv("USERNAME")); + } + + alignas(TOKEN_USER) char buf[SECURITY_MAX_SID_SIZE + sizeof(TOKEN_USER)]; + if (NT_SUCCESS(NtQueryInformationToken(NtCurrentProcessToken(), TokenUser, buf, sizeof(buf), &size))) { + TOKEN_USER* tokenUser = (TOKEN_USER*) buf; + UNICODE_STRING sidString = { .Buffer = buffer, .Length = 0, .MaximumLength = sizeof(buffer) }; + if (NT_SUCCESS(RtlConvertSidToUnicodeString(&sidString, tokenUser->User.Sid, FALSE))) { + ffStrbufSetNWS(&platform->sid, sidString.Length / sizeof(wchar_t), sidString.Buffer); + } + } +} + +static void getHostName(FFPlatform* platform) { + wchar_t buffer[256]; + DWORD len = ARRAY_SIZE(buffer); + if (GetComputerNameExW(ComputerNameDnsHostname, buffer, &len) && len > 0) { + ffStrbufSetNWS(&platform->hostName, len, buffer); + } else { + len = ARRAY_SIZE(buffer); + if (GetComputerNameExW(ComputerNameNetBIOS, buffer, &len) && len > 0) { + ffStrbufSetNWS(&platform->hostName, len, buffer); + } + } +} + +static void getUserShell(FFPlatform* platform) { + // Works in MSYS2 + const char* userShell = getenv("SHELL"); + if (userShell) { + ffStrbufAppendS(&platform->userShell, userShell); + ffStrbufReplaceAllC(&platform->userShell, '\\', '/'); + } +} + +static const char* detectWine(void) { + const char* __cdecl wine_get_version(void); + void* hntdll = ffLibraryGetModule(L"ntdll.dll"); + if (!hntdll) { + return NULL; + } + FF_LIBRARY_LOAD_SYMBOL_LAZY(hntdll, wine_get_version); + if (!ffwine_get_version) { + return NULL; + } + return ffwine_get_version(); +} + +static void getSystemReleaseAndVersion(FFPlatformSysinfo* info) { + FF_AUTO_CLOSE_FD HANDLE hKey = NULL; + if (!ffRegOpenKeyForRead(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", &hKey, NULL)) { + return; + } + + uint32_t ubr = 0; + ffRegReadValues(hKey, 2, (FFRegValueArg[]) { + FF_ARG(ubr, L"UBR"), + FF_ARG(info->version, L"BuildLabEx"), + }, + NULL); + + PPEB_FULL peb = ffGetPeb(); + + ffStrbufSetF(&info->release, + "%u.%u.%u.%u", + (unsigned) peb->OSMajorVersion, + (unsigned) peb->OSMinorVersion, + (unsigned) peb->OSBuildNumber, + (unsigned) ubr); + + const char* wineVersion = detectWine(); + if (wineVersion) { + ffStrbufSetF(&info->name, "Wine_%s", wineVersion); + } else { + ffStrbufSetStatic(&info->name, "WIN32_NT"); + } +} + +static void getSystemPageSize(FFPlatformSysinfo* info) { + SYSTEM_BASIC_INFORMATION sbi; + if (NT_SUCCESS(NtQuerySystemInformation(SystemBasicInformation, &sbi, sizeof(sbi), NULL))) { + info->pageSize = sbi.PhysicalPageSize; + } else { + info->pageSize = 4096; + } +} + +static void getSystemArchitecture(FFPlatformSysinfo* info) { + SYSTEM_PROCESSOR_INFORMATION spi; + if (NT_SUCCESS(NtQuerySystemInformation(SystemProcessorInformation, &spi, sizeof(spi), NULL))) { + switch (spi.ProcessorArchitecture) { + case PROCESSOR_ARCHITECTURE_AMD64: + ffStrbufSetStatic(&info->architecture, "x86_64"); + break; + case PROCESSOR_ARCHITECTURE_IA64: + ffStrbufSetStatic(&info->architecture, "ia64"); + break; + case PROCESSOR_ARCHITECTURE_INTEL: + switch (spi.ProcessorLevel) { + case 4: + ffStrbufSetStatic(&info->architecture, "i486"); + break; + case 5: + ffStrbufSetStatic(&info->architecture, "i586"); + break; + case 6: + ffStrbufSetStatic(&info->architecture, "i686"); + break; + default: + ffStrbufSetStatic(&info->architecture, "i386"); + break; + } + break; + case PROCESSOR_ARCHITECTURE_ARM64: + ffStrbufSetStatic(&info->architecture, "aarch64"); + break; + case PROCESSOR_ARCHITECTURE_ARM: + ffStrbufSetStatic(&info->architecture, "arm"); + break; + case PROCESSOR_ARCHITECTURE_PPC: + ffStrbufSetStatic(&info->architecture, "ppc"); + break; + case PROCESSOR_ARCHITECTURE_MIPS: + ffStrbufSetStatic(&info->architecture, "mips"); + break; + case PROCESSOR_ARCHITECTURE_ALPHA: + ffStrbufSetStatic(&info->architecture, "alpha"); + break; + case PROCESSOR_ARCHITECTURE_ALPHA64: + ffStrbufSetStatic(&info->architecture, "alpha64"); + break; + case PROCESSOR_ARCHITECTURE_UNKNOWN: + default: + ffStrbufSetStatic(&info->architecture, "unknown"); + break; + } + } +} + +static void getCwd(FFPlatform* platform) { + PCURDIR cwd = &ffGetPeb()->ProcessParameters->CurrentDirectory; + ffStrbufSetNWS(&platform->cwd, cwd->DosPath.Length / sizeof(WCHAR), cwd->DosPath.Buffer); + ffStrbufReplaceAllC(&platform->cwd, '\\', '/'); + ffStrbufEnsureEndsWithC(&platform->cwd, '/'); +} + +void ffPlatformInitImpl(FFPlatform* platform) { + platform->pid = (uint32_t) (uintptr_t) ffGetTeb()->ClientId.UniqueProcess; + getExePath(platform); + getCwd(platform); + getHomeDir(platform); + getCacheDir(platform); + getConfigDirs(platform); + getDataDirs(platform); + + getUserName(platform); + getHostName(platform); + getUserShell(platform); + + getSystemReleaseAndVersion(&platform->sysinfo); + getSystemArchitecture(&platform->sysinfo); + getSystemPageSize(&platform->sysinfo); +} diff --git a/src/common/impl/FFlist.c b/src/common/impl/FFlist.c new file mode 100644 index 0000000..eddd0fa --- /dev/null +++ b/src/common/impl/FFlist.c @@ -0,0 +1,34 @@ +#include "common/FFlist.h" + +#include <stdlib.h> +#include <string.h> + +void* ffListAdd(FFlist* list, uint32_t elementSize) { + if (list->length == list->capacity) { + ffListReserve(list, elementSize, list->capacity == 0 ? FF_LIST_DEFAULT_ALLOC : list->capacity * 2); + } + + ++list->length; + return ffListGet(list, elementSize, list->length - 1); +} + +bool ffListShift(FFlist* list, uint32_t elementSize, void* __restrict result) { + if (list->length == 0) { + return false; + } + + memcpy(result, list->data, elementSize); + memmove(list->data, list->data + elementSize, (size_t) elementSize * (list->length - 1)); + --list->length; + return true; +} + +bool ffListPop(FFlist* list, uint32_t elementSize, void* __restrict result) { + if (list->length == 0) { + return false; + } + + memcpy(result, ffListGet(list, elementSize, list->length - 1), elementSize); + --list->length; + return true; +} diff --git a/src/common/impl/FFstrbuf.c b/src/common/impl/FFstrbuf.c new file mode 100644 index 0000000..1f6477b --- /dev/null +++ b/src/common/impl/FFstrbuf.c @@ -0,0 +1,903 @@ +#include "common/FFstrbuf.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" + +#include <ctype.h> +#include <inttypes.h> +#include <math.h> + +char* CHAR_NULL_PTR = ""; + +void ffStrbufInitA(FFstrbuf* strbuf, uint32_t allocate) { + strbuf->allocated = allocate; + + if (strbuf->allocated > 0) { + strbuf->chars = (char*) malloc(sizeof(char) * strbuf->allocated); + } + + // This will set the length to zero and the null byte. + ffStrbufClear(strbuf); +} + +void ffStrbufInitVF(FFstrbuf* strbuf, const char* format, va_list arguments) { + assert(format != NULL); + + char* buffer = NULL; + int len = vasprintf(&buffer, format, arguments); + assert(len >= 0); + + ffStrbufInitMoveNS(strbuf, (uint32_t) len, buffer); +} + +// Takes ownership of `heapStr`. The caller must not free `heapStr` after calling this +// function; the memory will be managed and freed via the associated FFstrbuf. +void ffStrbufInitMoveNS(FFstrbuf* strbuf, uint32_t length, char* heapStr) { + assert(heapStr != NULL); + + strbuf->length = length; + size_t allocSize = ffMallocUsableSize(heapStr); + if (allocSize == 0) { + allocSize = length + 1; + } else if (allocSize > UINT32_MAX) { + allocSize = UINT32_MAX; + } + strbuf->allocated = (uint32_t) allocSize; + strbuf->chars = heapStr; +} + +void ffStrbufEnsureFree(FFstrbuf* strbuf, uint32_t free) { + if (ffStrbufGetFree(strbuf) >= free && !(strbuf->allocated == 0 && strbuf->length > 0)) { + return; + } + + uint32_t allocate = strbuf->allocated; + if (allocate < FASTFETCH_STRBUF_DEFAULT_ALLOC) { + allocate = FASTFETCH_STRBUF_DEFAULT_ALLOC; + } + + while ((strbuf->length + free + 1) > allocate) { // + 1 for the null byte + allocate *= 2; + } + + if (strbuf->allocated == 0) { + char* newbuf = malloc(sizeof(*strbuf->chars) * allocate); + if (strbuf->length == 0) { + *newbuf = '\0'; + } else { + memcpy(newbuf, strbuf->chars, strbuf->length + 1); + } + strbuf->chars = newbuf; + } else { + strbuf->chars = realloc(strbuf->chars, sizeof(*strbuf->chars) * allocate); + } + + strbuf->allocated = allocate; +} + +// Ensure that at least `free` bytes are available in the buffer besides the current length +// for an empty buffer, free + 1 length memory will be allocated(+1 for the NUL) +void ffStrbufEnsureFixedLengthFree(FFstrbuf* strbuf, uint32_t free) { + uint32_t oldFree = ffStrbufGetFree(strbuf); + if (oldFree >= free && !(strbuf->allocated == 0 && strbuf->length > 0)) { + return; + } + + uint32_t newCap = strbuf->allocated + (free - oldFree); + + if (strbuf->allocated == 0) { + newCap += strbuf->length + 1; + char* newbuf = malloc(sizeof(*strbuf->chars) * newCap); + if (strbuf->length == 0) { + *newbuf = '\0'; + } else { + memcpy(newbuf, strbuf->chars, strbuf->length + 1); + } + strbuf->chars = newbuf; + } else { + strbuf->chars = realloc(strbuf->chars, sizeof(*strbuf->chars) * newCap); + } + + strbuf->allocated = newCap; +} + +void ffStrbufClear(FFstrbuf* strbuf) { + assert(strbuf != NULL); + + if (strbuf->allocated == 0) { + strbuf->chars = CHAR_NULL_PTR; + } else { + strbuf->chars[0] = '\0'; + } + + strbuf->length = 0; +} + +void ffStrbufAppendC(FFstrbuf* strbuf, char c) { + ffStrbufEnsureFree(strbuf, 1); + strbuf->chars[strbuf->length++] = c; + strbuf->chars[strbuf->length] = '\0'; +} + +void ffStrbufAppendNC(FFstrbuf* strbuf, uint32_t num, char c) { + if (num == 0) { + return; + } + + ffStrbufEnsureFree(strbuf, num); + memset(&strbuf->chars[strbuf->length], c, num); + strbuf->length += num; + strbuf->chars[strbuf->length] = '\0'; +} + +void ffStrbufAppendNS(FFstrbuf* strbuf, uint32_t length, const char* value) { + if (value == NULL || length == 0) { + return; + } + + ffStrbufEnsureFree(strbuf, length); + memcpy(&strbuf->chars[strbuf->length], value, length); + strbuf->length += length; + strbuf->chars[strbuf->length] = '\0'; +} + +void ffStrbufAppendTransformS(FFstrbuf* strbuf, const char* value, int (*transformFunc)(int)) { + if (value == NULL) { + return; + } + + // Ensure capacity > 0 or the modification below will fail + uint32_t length = (uint32_t) strlen(value); + if (length == 0) { + return; + } + + ffStrbufEnsureFree(strbuf, length); + for (uint32_t i = 0; value[i] != '\0'; i++) { + strbuf->chars[strbuf->length++] = (char) transformFunc(value[i]); + } + strbuf->chars[strbuf->length] = '\0'; +} + +void ffStrbufAppendVF(FFstrbuf* strbuf, const char* format, va_list arguments) { + assert(format != NULL); + + va_list copy; + va_copy(copy, arguments); + + uint32_t free = ffStrbufGetFree(strbuf); + int written = vsnprintf(strbuf->chars + strbuf->length, strbuf->allocated > 0 ? free + 1 : 0, format, arguments); + + if (written > 0 && (uint32_t) written > free) { + ffStrbufEnsureFree(strbuf, (uint32_t) written); + written = vsnprintf(strbuf->chars + strbuf->length, (uint32_t) written + 1, format, copy); + } + + va_end(copy); + + if (written > 0) { + strbuf->length += (uint32_t) written; + } +} + +const char* ffStrbufAppendSUntilC(FFstrbuf* strbuf, const char* value, char until) { + if (value == NULL) { + return NULL; + } + + const char* end = strchr(value, until); + if (end == NULL) { + ffStrbufAppendS(strbuf, value); + } else { + ffStrbufAppendNS(strbuf, (uint32_t) (end - value), value); + } + return end; +} + +void ffStrbufSetF(FFstrbuf* strbuf, const char* format, ...) { + assert(format != NULL); + + va_list arguments; + va_start(arguments, format); + + if (strbuf->allocated == 0) { + ffStrbufInitVF(strbuf, format, arguments); + va_end(arguments); + return; + } + + ffStrbufClear(strbuf); + ffStrbufAppendVF(strbuf, format, arguments); + va_end(arguments); +} + +void ffStrbufAppendF(FFstrbuf* strbuf, const char* format, ...) { + assert(format != NULL); + + va_list arguments; + va_start(arguments, format); + ffStrbufAppendVF(strbuf, format, arguments); + va_end(arguments); +} + +void ffStrbufPrependNS(FFstrbuf* strbuf, uint32_t length, const char* value) { + if (value == NULL || length == 0) { + return; + } + + ffStrbufEnsureFree(strbuf, length); + memmove(strbuf->chars + length, strbuf->chars, strbuf->length + 1); // + 1 for the null byte + memcpy(strbuf->chars, value, length); + strbuf->length += length; +} + +void ffStrbufPrependC(FFstrbuf* strbuf, char c) { + ffStrbufEnsureFree(strbuf, 1); + memmove(strbuf->chars + 1, strbuf->chars, strbuf->length + 1); // + 1 for the null byte + strbuf->chars[0] = c; + strbuf->length += 1; +} + +void ffStrbufSetNS(FFstrbuf* strbuf, uint32_t length, const char* value) { + assert(strbuf != NULL); + + if (length == 0) { + ffStrbufClear(strbuf); + return; + } + + assert(value != NULL); + + if (strbuf->allocated <= length) { + char* newBuf = malloc(sizeof(char) * (length + 1)); + memcpy(newBuf, value, length); + if (strbuf->allocated > 0) { + free(strbuf->chars); + } + strbuf->chars = newBuf; + strbuf->allocated = length + 1; + } else { + memmove(strbuf->chars, value, length); + } + + strbuf->length = length; + strbuf->chars[length] = '\0'; +} + +void ffStrbufSet(FFstrbuf* strbuf, const FFstrbuf* value) { + assert(value && value != strbuf); + + if (value->length == 0) { + ffStrbufClear(strbuf); + return; + } + + if (value->allocated == 0) // static string + { + if (strbuf->allocated != 0) { + free(strbuf->chars); + strbuf->allocated = 0; + } + strbuf->chars = value->chars; + strbuf->length = value->length; + return; + } + ffStrbufSetNS(strbuf, value->length, value->chars); +} + +void ffStrbufTrimLeft(FFstrbuf* strbuf, char c) { + if (strbuf->length == 0) { + return; + } + + uint32_t index = 0; + while (index < strbuf->length && strbuf->chars[index] == c) { + ++index; + } + + if (index == 0) { + return; + } + + if (strbuf->allocated == 0) { + // static string + strbuf->length -= index; + strbuf->chars += index; + return; + } + + memmove(strbuf->chars, strbuf->chars + index, strbuf->length - index); + strbuf->length -= index; + strbuf->chars[strbuf->length] = '\0'; +} + +void ffStrbufTrimRight(FFstrbuf* strbuf, char c) { + if (strbuf->length == 0) { + return; + } + + if (!ffStrbufEndsWithC(strbuf, c)) { + return; + } + + do { + --strbuf->length; + } while (ffStrbufEndsWithC(strbuf, c)); + + if (strbuf->allocated == 0) { + // static string + ffStrbufInitNS(strbuf, strbuf->length, strbuf->chars); + return; + } + + strbuf->chars[strbuf->length] = '\0'; +} + +void ffStrbufTrimLeftSpace(FFstrbuf* strbuf) { + if (strbuf->length == 0) { + return; + } + + uint32_t index = 0; + while (index < strbuf->length && isspace(strbuf->chars[index])) { + ++index; + } + + if (index == 0) { + return; + } + + if (strbuf->allocated == 0) { + // static string + strbuf->length -= index; + strbuf->chars += index; + return; + } + + memmove(strbuf->chars, strbuf->chars + index, strbuf->length - index); + strbuf->length -= index; + strbuf->chars[strbuf->length] = '\0'; +} + +void ffStrbufTrimRightSpace(FFstrbuf* strbuf) { + if (strbuf->length == 0) { + return; + } + + if (!ffStrbufEndsWithFn(strbuf, isspace)) { + return; + } + + do { + --strbuf->length; + } while (ffStrbufEndsWithFn(strbuf, isspace)); + + if (strbuf->allocated == 0) { + // static string + ffStrbufInitNS(strbuf, strbuf->length, strbuf->chars); + return; + } + + strbuf->chars[strbuf->length] = '\0'; +} + +bool ffStrbufRemoveSubstr(FFstrbuf* strbuf, uint32_t startIndex, uint32_t endIndex) { + if (startIndex > strbuf->length || startIndex >= endIndex) { + return false; + } + + if (endIndex > strbuf->length) { + ffStrbufSubstrBefore(strbuf, startIndex); + return true; + } + + ffStrbufEnsureFree(strbuf, 0); + memmove(strbuf->chars + startIndex, strbuf->chars + endIndex, strbuf->length - endIndex); + strbuf->length -= (endIndex - startIndex); + strbuf->chars[strbuf->length] = '\0'; + return true; +} + +void ffStrbufRemoveS(FFstrbuf* strbuf, const char* str) { + uint32_t stringLength = (uint32_t) strlen(str); + + for (uint32_t i = ffStrbufNextIndexS(strbuf, 0, str); i < strbuf->length; i = ffStrbufNextIndexS(strbuf, i, str)) { + ffStrbufRemoveSubstr(strbuf, i, i + stringLength); + } +} + +void ffStrbufRemoveStrings(FFstrbuf* strbuf, uint32_t numStrings, const char* strings[]) { + for (uint32_t i = 0; i < numStrings; i++) { + ffStrbufRemoveS(strbuf, strings[i]); + } +} + +void ffStrbufReplaceAllC(FFstrbuf* strbuf, char find, char replace) { + if (strbuf->length == 0) { + return; + } + + ffStrbufEnsureFree(strbuf, 0); + for ( + char* current_pos = memchr(strbuf->chars, find, strbuf->length); + current_pos; + current_pos = memchr( + current_pos + 1, + find, + strbuf->length - (uint32_t) (current_pos + 1 - strbuf->chars))) { + *current_pos = replace; + } +} + +bool ffStrbufSubstrBefore(FFstrbuf* strbuf, uint32_t index) { + if (strbuf->length <= index) { + return false; + } + + if (strbuf->allocated == 0) { + // static string + if (index < strbuf->length) { + ffStrbufInitNS(strbuf, index, strbuf->chars); + } + return true; + } + + strbuf->length = index; + strbuf->chars[strbuf->length] = '\0'; + return true; +} + +bool ffStrbufSubstrAfter(FFstrbuf* strbuf, uint32_t index) { + if (index >= strbuf->length) { + ffStrbufClear(strbuf); + return true; + } + + if (strbuf->allocated == 0) { + // static string + strbuf->length -= index + 1; + strbuf->chars += index + 1; + return true; + } + + memmove(strbuf->chars, strbuf->chars + index + 1, strbuf->length - index - 1); + strbuf->length -= (index + 1); + strbuf->chars[strbuf->length] = '\0'; + return true; +} + +bool ffStrbufSubstrAfterFirstC(FFstrbuf* strbuf, char c) { + uint32_t index = ffStrbufFirstIndexC(strbuf, c); + if (index >= strbuf->length) { + return false; + } + ffStrbufSubstrAfter(strbuf, index); + return true; +} + +bool ffStrbufSubstrAfterFirstS(FFstrbuf* strbuf, const char* str) { + if (*str == '\0') { + return false; + } + + uint32_t index = ffStrbufFirstIndexS(strbuf, str) + (uint32_t) strlen(str) - 1; // -1, because firstIndexS is already pointing to str[0], we want to add only the remaining length + if (index >= strbuf->length) { + return false; + } + + ffStrbufSubstrAfter(strbuf, index); + return true; +} + +bool ffStrbufSubstrAfterLastC(FFstrbuf* strbuf, char c) { + uint32_t index = ffStrbufLastIndexC(strbuf, c); + if (index >= strbuf->length) { + return false; + } + + ffStrbufSubstrAfter(strbuf, index); + return true; +} + +bool ffStrbufSubstr(FFstrbuf* strbuf, uint32_t start, uint32_t end) { + if (__builtin_expect(start >= end, false)) { + ffStrbufClear(strbuf); + return false; + } + + if (__builtin_expect(start == 0, false)) { + return ffStrbufSubstrBefore(strbuf, end); + } + if (__builtin_expect(end >= strbuf->length, false)) { + return ffStrbufSubstrAfter(strbuf, start - 1); + } + + uint32_t len = end - start; + ffStrbufEnsureFixedLengthFree(strbuf, len); // In case of static string + memmove(strbuf->chars, strbuf->chars + start, len); + + strbuf->length = len; + strbuf->chars[len] = '\0'; + return true; +} + +uint32_t ffStrbufCountC(const FFstrbuf* strbuf, char c) { + uint32_t result = 0; + for (uint32_t i = 0; i < strbuf->length; i++) { + if (strbuf->chars[i] == c) { + result++; + } + } + + return result; +} + +bool ffStrbufRemoveIgnCaseEndS(FFstrbuf* strbuf, const char* end) { + uint32_t endLength = (uint32_t) strlen(end); + if (ffStrbufEndsWithIgnCaseNS(strbuf, endLength, end)) { + ffStrbufSubstrBefore(strbuf, strbuf->length - endLength); + return true; + } + + return false; +} + +bool ffStrbufEnsureEndsWithC(FFstrbuf* strbuf, char c) { + if (ffStrbufEndsWithC(strbuf, c)) { + return false; + } + + ffStrbufAppendC(strbuf, c); + return true; +} + +void ffStrbufWriteTo(const FFstrbuf* strbuf, FILE* file) { + fwrite(strbuf->chars, sizeof(*strbuf->chars), strbuf->length, file); +} + +void ffStrbufPutTo(const FFstrbuf* strbuf, FILE* file) { + ffStrbufWriteTo(strbuf, file); + fputc('\n', file); +} + +double ffStrbufToDouble(const FFstrbuf* strbuf, double defaultValue) { + char* str_end; + double result = strtod(strbuf->chars, &str_end); + return str_end == strbuf->chars ? defaultValue : result; +} + +uint64_t ffStrbufToUInt(const FFstrbuf* strbuf, uint64_t defaultValue) { + char* str_end; + unsigned long long result = strtoull(strbuf->chars, &str_end, 10); + return str_end == strbuf->chars ? defaultValue : (uint64_t) result; +} + +int64_t ffStrbufToSInt(const FFstrbuf* strbuf, int64_t defaultValue) { + char* str_end; + long long result = strtoll(strbuf->chars, &str_end, 10); + return str_end == strbuf->chars ? defaultValue : (int64_t) result; +} + +void ffStrbufAppendSInt(FFstrbuf* strbuf, int64_t value) { + ffStrbufEnsureFree(strbuf, 21); // Required by yyjson_write_number + char* start = strbuf->chars + strbuf->length; + + yyjson_val val = {}; + unsafe_yyjson_set_sint(&val, value); + char* end = yyjson_write_number(&val, start); + + assert(end != NULL); + + strbuf->length += (uint32_t) (end - start); +} + +void ffStrbufAppendUInt(FFstrbuf* strbuf, uint64_t value) { + ffStrbufEnsureFree(strbuf, 21); // Required by yyjson_write_number + char* start = strbuf->chars + strbuf->length; + + yyjson_val val = {}; + unsafe_yyjson_set_uint(&val, value); + char* end = yyjson_write_number(&val, start); + + assert(end != NULL); + + strbuf->length += (uint32_t) (end - start); +} + +void ffStrbufAppendDouble(FFstrbuf* strbuf, double value, int8_t precision, bool trailingZeros) { + assert(precision <= 15); // yyjson_write_number supports up to 15 digits after the decimal point + + ffStrbufEnsureFree(strbuf, 40); // Required by yyjson_write_number + char* start = strbuf->chars + strbuf->length; + + if (precision == 0) { + value = round(value); + } + yyjson_val val = {}; + unsafe_yyjson_set_double(&val, value); + if (precision > 0) { + unsafe_yyjson_set_fp_to_fixed(&val, precision); + } + + // Write at most <precision> digits after the decimal point; doesn't append trailing zeros + char* end = yyjson_write_number(&val, start); + + assert(end > start); + + strbuf->length += (uint32_t) (end - start); + + if (__builtin_expect(value > 1e21 || value < -1e21, false)) { + // If the value is too large, yyjson_write_number will write it in scientific notation + return; + } + + if (trailingZeros) { + if (precision > 1) { + for (char* p = end - 1; *p != '.' && p > start; --p) { + --precision; + } + if (precision > 0) { + ffStrbufAppendNC(strbuf, (uint32_t) precision, '0'); + } + } else if (precision == 0 || (precision < 0 && end[-1] == '0')) { + goto removeDecimalPoint; + } + } else { + if (end[-1] == '0') { + removeDecimalPoint: + // yyjson always appends ".0" to make it a float point number. We need to remove it + strbuf->length -= 2; + strbuf->chars[strbuf->length] = '\0'; + } + } +} + +void ffStrbufUpperCase(FFstrbuf* strbuf) { + for (uint32_t i = 0; i < strbuf->length; ++i) { + strbuf->chars[i] = (char) toupper(strbuf->chars[i]); + } +} + +void ffStrbufLowerCase(FFstrbuf* strbuf) { + for (uint32_t i = 0; i < strbuf->length; ++i) { + strbuf->chars[i] = (char) tolower(strbuf->chars[i]); + } +} + +void ffStrbufInsertNC(FFstrbuf* strbuf, uint32_t index, uint32_t num, char c) { + if (num == 0) { + return; + } + if (index >= strbuf->length) { + index = strbuf->length; + } + + ffStrbufEnsureFree(strbuf, num); + memmove(strbuf->chars + index + num, strbuf->chars + index, strbuf->length - index + 1); + memset(&strbuf->chars[index], c, num); + strbuf->length += num; +} + +bool ffStrbufGetdelim(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) { + assert(lineptr && n && buffer); + assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0)); + assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length)); + + const char* pBufferEnd = buffer->chars + buffer->length; + if (!*lineptr) { + *lineptr = buffer->chars; + } else { + *lineptr += *n; + if (*lineptr >= pBufferEnd) { // non-empty last line + return false; + } + **lineptr = delimiter; + ++*lineptr; + } + if (*lineptr >= pBufferEnd) { // empty last line + return false; + } + + size_t remaining = (size_t) (pBufferEnd - *lineptr); + char* ending = memchr(*lineptr, delimiter, remaining); + if (ending) { + *n = (size_t) (ending - *lineptr); + *ending = '\0'; + } else { + *n = remaining; + } + return true; +} + +void ffStrbufGetdelimRestore(char** lineptr, size_t* n, char delimiter, FFstrbuf* buffer) { + assert(buffer && lineptr && n); + assert(buffer->allocated > 0 || (buffer->allocated == 0 && buffer->length == 0)); + assert(!*lineptr || (*lineptr >= buffer->chars && *lineptr <= buffer->chars + buffer->length)); + + if (!*lineptr) { + return; + } + + *lineptr += *n; + if (*lineptr < buffer->chars + buffer->length) { + **lineptr = delimiter; + } +} + +bool ffStrbufRemoveDupWhitespaces(FFstrbuf* strbuf) { + if (strbuf->allocated == 0) { + return false; // Doesn't work with static strings + } + + bool changed = false; + for (uint32_t i = 0; i < strbuf->length; i++) { + if (strbuf->chars[i] != ' ') { + continue; + } + + i++; + uint32_t j = i; + for (; j < strbuf->length && strbuf->chars[j] == ' '; j++); + + if (j == i) { + continue; + } + memmove(&strbuf->chars[i], &strbuf->chars[j], strbuf->length - j + 1); + strbuf->length -= j - i; + changed = true; + } + + return changed; +} + +/// @brief Check if a separated string (comp) contains a substring (strbuf). +/// @param strbuf The substring to check. +/// @param compLength The length of the separated string to check. +/// @param comp The separated string to check. +/// @param separator The separator character. +bool ffStrbufMatchSeparatedNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) { + if (strbuf->length == 0) { + return true; + } + + if (compLength == 0) { + return false; + } + + for (const char* p = comp; p < comp + compLength;) { + const char* colon = memchr(p, separator, (size_t) (comp + compLength - p)); + if (colon == NULL) { + uint32_t remainingLen = (uint32_t) (comp + compLength - p); + return strbuf->length == remainingLen && memcmp(strbuf->chars, p, remainingLen) == 0; + } + + uint32_t substrLength = (uint32_t) (colon - p); + if (strbuf->length == substrLength && memcmp(strbuf->chars, p, substrLength) == 0) { + return true; + } + + p = colon + 1; + } + + return false; +} + +/// @brief Case insensitive version of ffStrbufMatchSeparatedNS. +bool ffStrbufMatchSeparatedIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) { + if (strbuf->length == 0) { + return true; + } + + if (compLength == 0) { + return false; + } + + for (const char* p = comp; p < comp + compLength;) { + const char* colon = memchr(p, separator, (size_t) (comp + compLength - p)); + if (colon == NULL) { + uint32_t remainingLen = (uint32_t) (comp + compLength - p); + return strbuf->length == remainingLen && strncasecmp(strbuf->chars, p, remainingLen) == 0; + } + + uint32_t substrLength = (uint32_t) (colon - p); + if (strbuf->length == substrLength && strncasecmp(strbuf->chars, p, substrLength) == 0) { + return true; + } + + p = colon + 1; + } + + return false; +} + +int ffStrbufAppendUtf32CodePoint(FFstrbuf* strbuf, uint32_t codepoint) { + if (codepoint <= 0x7F) { + ffStrbufAppendC(strbuf, (char) codepoint); + return 1; + } else if (codepoint <= 0x7FF) { + ffStrbufAppendNS(strbuf, 2, (char[]) { (char) (0xC0 | (codepoint >> 6)), (char) (0x80 | (codepoint & 0x3F)) }); + return 2; + } else if (codepoint <= 0xFFFF) { + ffStrbufAppendNS(strbuf, 3, (char[]) { (char) (0xE0 | (codepoint >> 12)), (char) (0x80 | ((codepoint >> 6) & 0x3F)), (char) (0x80 | (codepoint & 0x3F)) }); + return 3; + } else if (codepoint <= 0x10FFFF) { + ffStrbufAppendNS(strbuf, 4, (char[]) { (char) (0xF0 | (codepoint >> 18)), (char) (0x80 | ((codepoint >> 12) & 0x3F)), (char) (0x80 | ((codepoint >> 6) & 0x3F)), (char) (0x80 | (codepoint & 0x3F)) }); + return 4; + } + + ffStrbufAppendS(strbuf, "�"); // U+FFFD REPLACEMENT CHARACTER + return 1; +} + +/// @brief Check if a separated string (strbuf) contains a substring (comp). +/// @param strbuf The separated to check. +/// @param compLength The length of the separated string to check. +/// @param comp The substring to check. +/// @param separator The separator character. +bool ffStrbufSeparatedContainNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) { + uint32_t startIndex = 0; + while (startIndex < strbuf->length) { + uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator); + + uint32_t folderLength = colonIndex - startIndex; + if (folderLength == compLength && memcmp(strbuf->chars + startIndex, comp, compLength) == 0) { + return true; + } + + startIndex = colonIndex + 1; + } + + return false; +} + +bool ffStrbufSeparatedContainIgnCaseNS(const FFstrbuf* strbuf, uint32_t compLength, const char* comp, char separator) { + uint32_t startIndex = 0; + while (startIndex < strbuf->length) { + uint32_t colonIndex = ffStrbufNextIndexC(strbuf, startIndex, separator); + + uint32_t folderLength = colonIndex - startIndex; + if (folderLength == compLength && strncasecmp(strbuf->chars + startIndex, comp, compLength) == 0) { + return true; + } + + startIndex = colonIndex + 1; + } + + return false; +} + +bool ffStrbufDecodeHexEscapeSequences(FFstrbuf* strbuf) { + assert(strbuf); + + if (strbuf->length < 4) { + return false; + } + + // Static string must be converted first. + assert(strbuf->allocated > 0); + + bool changed = false; + uint32_t read = 0; + uint32_t write = 0; + + while (read < strbuf->length) { + if ( + read + 3 < strbuf->length && + strbuf->chars[read] == '\\' && + strbuf->chars[read + 1] == 'x') { + int8_t hi = ffHexCharToInt(strbuf->chars[read + 2]); + int8_t lo = ffHexCharToInt(strbuf->chars[read + 3]); + if (hi >= 0 && lo >= 0) { + strbuf->chars[write++] = (char) ((hi << 4) | lo); + read += 4; + changed = true; + continue; + } + } + + strbuf->chars[write++] = strbuf->chars[read++]; + } + + strbuf->length = write; + strbuf->chars[write] = '\0'; + return changed; +} diff --git a/src/common/impl/base64.c b/src/common/impl/base64.c new file mode 100644 index 0000000..98a415f --- /dev/null +++ b/src/common/impl/base64.c @@ -0,0 +1,110 @@ +#include "common/base64.h" + +// https://github.com/kostya/benchmarks/blob/master/base64/test-nolib.c#L145 +void ffBase64EncodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output) { + static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + char* out = output; + const char* ends = str + (size - size % 3); + while (str != ends) { + uint32_t n = __builtin_bswap32(*(uint32_t*) str); + *out++ = chars[(n >> 26) & 63]; + *out++ = chars[(n >> 20) & 63]; + *out++ = chars[(n >> 14) & 63]; + *out++ = chars[(n >> 8) & 63]; + str += 3; + } + + if (size % 3 == 1) { + uint64_t n = (uint64_t) *str << 16; + *out++ = chars[(n >> 18) & 63]; + *out++ = chars[(n >> 12) & 63]; + *out++ = '='; + *out++ = '='; + } else if (size % 3 == 2) { + uint64_t n = (uint64_t) *str++ << 16; + n |= (uint64_t) *str << 8; + *out++ = chars[(n >> 18) & 63]; + *out++ = chars[(n >> 12) & 63]; + *out++ = chars[(n >> 6) & 63]; + *out++ = '='; + } + *out = '\0'; + *out_size = (uint32_t) (out - output); +} + +static uint8_t decode_table[256]; + +static void init_decode_table() { + uint8_t ch = 0; + do { + int32_t code = -1; + if (ch >= 'A' && ch <= 'Z') { + code = ch - 0x41; + } + if (ch >= 'a' && ch <= 'z') { + code = ch - 0x47; + } + if (ch >= '0' && ch <= '9') { + code = ch + 0x04; + } + if (ch == '+' || ch == '-') { + code = 0x3E; + } + if (ch == '/' || ch == '_') { + code = 0x3F; + } + decode_table[ch] = (uint8_t) code; + } while (ch++ < 0xFF); +} + +#define next_char(x) uint8_t x = decode_table[(uint8_t) *str++]; + +bool ffBase64DecodeRaw(uint32_t size, const char* str, uint32_t* out_size, char* output) { + if (*(uint64_t*) decode_table == 0) { + init_decode_table(); + } + + char* out = output; + while (size > 0 && (str[size - 1] == '\n' || str[size - 1] == '\r' || str[size - 1] == '=')) { + size--; + } + + const char* ends = str + size - 4; + while (true) { + if (str > ends) { + break; + } + while (*str == '\n' || *str == '\r') { + str++; + } + + if (str > ends) { + break; + } + next_char(a); + next_char(b); + next_char(c); + next_char(d); + + *out++ = (char) (a << 2 | b >> 4); + *out++ = (char) (b << 4 | c >> 2); + *out++ = (char) (c << 6 | d >> 0); + } + + uint8_t mod = (uint8_t) (ends - str + 4) % 4; + if (mod == 2) { + next_char(a); + next_char(b); + *out++ = (char) (a << 2 | b >> 4); + } else if (mod == 3) { + next_char(a); + next_char(b); + next_char(c); + *out++ = (char) (a << 2 | b >> 4); + *out++ = (char) (b << 4 | c >> 2); + } + + *out = '\0'; + *out_size = (uint32_t) (out - output); + return true; +} diff --git a/src/common/impl/binary_apple.c b/src/common/impl/binary_apple.c new file mode 100644 index 0000000..8478169 --- /dev/null +++ b/src/common/impl/binary_apple.c @@ -0,0 +1,303 @@ +#include "common/binary.h" +#include "common/io.h" +#include "common/strutil.h" + +#include <string.h> +#include <fcntl.h> +#include <sys/mman.h> +#include <mach-o/loader.h> +#include <mach-o/swap.h> +#include <mach-o/fat.h> + +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" // swap_fat_arch + +// Ref: https://github.com/AlexDenisov/segment_dumper/blob/master/main.c + +typedef struct { + const uint8_t* data; + size_t length; +} FFMemoryMapping; + +static inline void wrapMunmap(FFMemoryMapping* mapping) { + assert(mapping); + if (mapping->data == NULL || mapping->data == MAP_FAILED) { + return; + } + munmap((void*) mapping->data, mapping->length); +} + +/** + * Helper function to access data from a memory-mapped file at a specific offset + */ +static inline const void* readData(const FFMemoryMapping* mapping, size_t size, off_t offset) { + if (offset < 0) { + return NULL; + } + + size_t start = (size_t) offset; + if (start > mapping->length || size > mapping->length - start) { + return NULL; + } + + return mapping->data + start; +} + +/** + * Handles a Mach-O section by extracting strings from the __cstring section + * + * @param mapping Memory mapping of the Mach-O object file + * @param name Section name to check + * @param offset Offset of the section in the file + * @param size Size of the section + * @param cb Callback function to process strings + * @param userdata User data for the callback + * @param minLength Minimum string length to extract + * + * @return true to continue processing, false to stop + */ +static bool handleMachSection(const FFMemoryMapping* mapping, const char* name, off_t offset, size_t size, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) { + if (!ffStrEquals(name, "__cstring")) { + return true; + } + + const char* data = readData(mapping, size, offset); + if (!data) { + return true; + } + + for (size_t off = 0; off < size; ++off) { + const char* p = data + off; + if (*p == '\0') { + continue; + } + uint32_t len = (uint32_t) strnlen(p, size - off); + if (len < minLength) { + off += len; // Skip short strings + continue; + } + if (*p >= ' ' && *p <= '~') { // Ignore control characters + if (!cb(p, len, userdata)) { + return false; + } + } + off += len; + } + return true; +} + +/** + * Processes a Mach-O header (32-bit or 64-bit) + * + * This function parses the load commands in a Mach-O header, looking for + * LC_SEGMENT or LC_SEGMENT_64 commands that contain the __TEXT segment. + * It then processes the sections within that segment to extract strings. + * + * @param mapping Memory mapping of the Mach-O object file + * @param offset Offset of the Mach header in the file + * @param is_64 Whether this is a 64-bit Mach-O header + * @param cb Callback function to process strings + * @param userdata User data for the callback + * @param minLength Minimum string length to extract + * + * @return NULL on success, error message on failure + */ +static const char* dumpMachHeader(const FFMemoryMapping* mapping, off_t offset, bool is_64, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) { + uint32_t ncmds; + off_t loadCommandsOffset = offset; + + if (is_64) { + const struct mach_header_64* header = readData(mapping, sizeof(struct mach_header_64), offset); + if (!header) { + return "read mach header failed"; + } + + ncmds = header->ncmds; + loadCommandsOffset += sizeof(*header); + } else { + const struct mach_header* header = readData(mapping, sizeof(struct mach_header), offset); + if (!header) { + return "read mach header failed"; + } + + ncmds = header->ncmds; + loadCommandsOffset += sizeof(*header); + } + + off_t commandOffset = loadCommandsOffset; + const struct load_command* cmd = NULL; + for (uint32_t i = 0U; i < ncmds; i++, commandOffset += cmd->cmdsize) { + cmd = readData(mapping, sizeof(*cmd), commandOffset); + if (!cmd) { + break; + } + + if (cmd->cmdsize < sizeof(*cmd)) { + break; + } + + if (cmd->cmd == LC_SEGMENT_64) { + const struct segment_command_64* segment = readData(mapping, sizeof(struct segment_command_64), commandOffset); + if (!segment) { + continue; + } + + if (!ffStrEquals(segment->segname, "__TEXT")) { + continue; + } + + for (uint32_t j = 0U; j < segment->nsects; j++) { + off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section_64)); + const struct section_64* section = readData(mapping, sizeof(struct section_64), sectionOffset); + if (!section) { + continue; + } + + if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) { + return NULL; + } + } + } else if (cmd->cmd == LC_SEGMENT) { + const struct segment_command* segment = readData(mapping, sizeof(struct segment_command), commandOffset); + if (!segment) { + continue; + } + + if (!ffStrEquals(segment->segname, "__TEXT")) { + continue; + } + + for (uint32_t j = 0; j < segment->nsects; j++) { + off_t sectionOffset = commandOffset + (off_t) sizeof(*segment) + (off_t) (j * sizeof(struct section)); + const struct section* section = readData(mapping, sizeof(struct section), sectionOffset); + if (!section) { + continue; + } + + if (!handleMachSection(mapping, section->sectname, (off_t) section->offset, (size_t) section->size, cb, userdata, minLength)) { + return NULL; + } + } + } + } + + return NULL; +} + +/** + * Processes a Fat binary header (Universal binary) + * + * This function handles the fat header of a universal binary, which can contain + * multiple Mach-O binaries for different architectures. It extracts and processes + * each embedded Mach-O file. + * + * @param mapping Memory mapping of the universal binary + * @param cb Callback function to process strings + * @param userdata User data for the callback + * @param minLength Minimum string length to extract + * + * @return NULL on success, error message on failure + */ +static const char* dumpFatHeader(const FFMemoryMapping* mapping, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) { + const struct fat_header* headerRaw = readData(mapping, sizeof(struct fat_header), 0); + if (!headerRaw) { + return "read fat header failed"; + } + + struct fat_header header = *headerRaw; + + bool needSwap = header.magic == FAT_CIGAM || header.magic == FAT_CIGAM_64; + + if (needSwap) { + swap_fat_header(&header, NX_UnknownByteOrder); + } + + for (uint32_t i = 0U; i < header.nfat_arch; i++) { + off_t machHeaderOffset = 0; + if (header.magic == FAT_MAGIC) { + off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch)); + const struct fat_arch* archRaw = readData(mapping, sizeof(struct fat_arch), archOffset); + if (!archRaw) { + continue; + } + + struct fat_arch arch = *archRaw; + + if (needSwap) { + swap_fat_arch(&arch, 1, NX_UnknownByteOrder); + } + machHeaderOffset = (off_t) arch.offset; + } else { + off_t archOffset = (off_t) sizeof(struct fat_header) + (off_t) (i * sizeof(struct fat_arch_64)); + const struct fat_arch_64* archRaw = readData(mapping, sizeof(struct fat_arch_64), archOffset); + if (!archRaw) { + continue; + } + + struct fat_arch_64 arch = *archRaw; + + if (needSwap) { + swap_fat_arch_64(&arch, 1, NX_UnknownByteOrder); + } + + machHeaderOffset = (off_t) arch.offset; + } + + const uint32_t* magic = readData(mapping, sizeof(uint32_t), machHeaderOffset); + if (!magic) { + continue; + } + + if (*magic == MH_MAGIC_64 || *magic == MH_MAGIC) { + return dumpMachHeader(mapping, machHeaderOffset, *magic == MH_MAGIC_64, cb, userdata, minLength); + } + } + return "Unsupported fat header"; +} + +/** + * Extracts string literals from a Mach-O (Apple) binary file + * + * This function supports both single-architecture Mach-O files and + * universal binaries (fat binaries) containing multiple architectures. + * It locates the __cstring section in the __TEXT segment which contains + * the string literals used in the program. + */ +const char* ffBinaryExtractStrings(const char* machoFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) { + FF_AUTO_CLOSE_FD int fd = open(machoFile, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "File could not be opened"; + } + + struct stat st; + if (fstat(fd, &st) != 0 || st.st_size <= 0) { + return "Failed to stat file"; + } + + FF_A_CLEANUP(wrapMunmap) FFMemoryMapping mapping = { + .data = mmap(NULL, (size_t) st.st_size, PROT_READ, MAP_PRIVATE, fd, 0), + .length = (size_t) st.st_size, + }; + if (mapping.data == MAP_FAILED) { + return "mmap failed"; + } + + // Read the magic number to determine the type of binary + const uint32_t* magic = readData(&mapping, sizeof(uint32_t), 0); + if (!magic) { + return "read magic number failed"; + } + + // Check for supported formats + // MH_CIGAM and MH_CIGAM_64 seem to be no longer used, as `swap_mach_header` is marked as deprecated. + // However FAT_CIGAM and FAT_CIGAM_64 are still used (/usr/bin/vim). + if (*magic != MH_MAGIC && *magic != MH_MAGIC_64 && *magic != FAT_CIGAM && *magic != FAT_CIGAM_64 && *magic != FAT_MAGIC && *magic != FAT_MAGIC_64) { + return "Unsupported format or big endian mach-o file"; + } + + // Process either a fat binary or a regular Mach-O binary + if (*magic == FAT_MAGIC || *magic == FAT_MAGIC_64 || *magic == FAT_CIGAM || *magic == FAT_CIGAM_64) { + return dumpFatHeader(&mapping, cb, userdata, minLength); + } else { + return dumpMachHeader(&mapping, 0, *magic == MH_MAGIC_64, cb, userdata, minLength); + } +} diff --git a/src/common/impl/binary_linux.c b/src/common/impl/binary_linux.c new file mode 100644 index 0000000..9a24217 --- /dev/null +++ b/src/common/impl/binary_linux.c @@ -0,0 +1,147 @@ +#include "common/binary.h" + +#if defined(FF_HAVE_ELF) || defined(__sun) || (defined(__FreeBSD__) && !defined(__DragonFly__)) || defined(__OpenBSD__) || defined(__NetBSD__) + + #include "common/io.h" + #include "common/library.h" + #include "common/strutil.h" + + #include <libelf.h> // #1254 + #include <fcntl.h> + +/** + * Structure to hold dynamically loaded libelf function pointers + */ +struct FFElfData { + FF_LIBRARY_SYMBOL(elf_version) + FF_LIBRARY_SYMBOL(elf_begin) + FF_LIBRARY_SYMBOL(elf_getshdrstrndx) + FF_LIBRARY_SYMBOL(elf_nextscn) + FF_LIBRARY_SYMBOL(elf64_getshdr) + FF_LIBRARY_SYMBOL(elf32_getshdr) + FF_LIBRARY_SYMBOL(elf_getdata) + FF_LIBRARY_SYMBOL(elf_strptr) + FF_LIBRARY_SYMBOL(elf_end) + + bool inited; +} elfData; + +/** + * Extracts string literals from an ELF (Linux/Unix) binary file + * + * This function loads the libelf library dynamically, opens the ELF file, + * locates the .rodata section (which contains string literals), and + * scans it for valid strings. Each string found is passed to the + * callback function for processing. + * + * The function supports both 32-bit and 64-bit ELF formats. + */ +const char* ffBinaryExtractStrings(const char* elfFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) { + // Initialize libelf if not already done + if (!elfData.inited) { + elfData.inited = true; + FF_LIBRARY_LOAD_MESSAGE(libelf, "libelf" FF_LIBRARY_EXTENSION, 1); + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_version) + if (elfData.ffelf_version(EV_CURRENT) == EV_NONE) { + return "elf_version() failed"; + } + + // Load all required libelf functions + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_begin) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_getshdrstrndx) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_nextscn) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf64_getshdr) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf32_getshdr) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_getdata) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_strptr) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libelf, elfData, elf_end) + + libelf = NULL; + } + + if (elfData.ffelf_end == NULL) { + return "load libelf failed"; + } + + // Open the ELF file + FF_AUTO_CLOSE_FD int fd = open(elfFile, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + return "open() failed"; + } + + Elf* elf = elfData.ffelf_begin(fd, ELF_C_READ, NULL); + if (elf == NULL) { + return "elf_begin() failed"; + } + + // Get the section header string table index + size_t shstrndx = 0; + if (elfData.ffelf_getshdrstrndx(elf, &shstrndx) < 0) { + elfData.ffelf_end(elf); + return "elf_getshdrstrndx() failed"; + } + + // Iterate through all sections, looking for .rodata which contains string literals + Elf_Scn* scn = NULL; + while ((scn = elfData.ffelf_nextscn(elf, scn)) != NULL) { + // Try 64-bit section header first, then 32-bit if that fails + Elf64_Shdr* shdr64 = elfData.ffelf64_getshdr(scn); + Elf32_Shdr* shdr32 = NULL; + if (shdr64 == NULL) { + shdr32 = elfData.ffelf32_getshdr(scn); + if (shdr32 == NULL) { + continue; + } + } + + // Get the section name and check if it's .rodata + const char* name = elfData.ffelf_strptr(elf, shstrndx, shdr64 ? shdr64->sh_name : shdr32->sh_name); + if (name == NULL || !ffStrEquals(name, ".rodata")) { + continue; + } + + // Get the section data + Elf_Data* data = elfData.ffelf_getdata(scn, NULL); + if (data == NULL) { + continue; + } + + // Scan the section for string literals + for (size_t off = 0; off < data->d_size; ++off) { + const char* p = (const char*) data->d_buf + off; + if (*p == '\0') { + continue; + } + uint32_t len = (uint32_t) strnlen(p, data->d_size - off); + if (len < minLength) { + off += len; + continue; + } + // Only process printable ASCII characters + if (*p >= ' ' && *p <= '~') // Ignore control characters + { + if (!cb(p, len, userdata)) { + break; + } + } + off += len; + } + + break; + } + + elfData.ffelf_end(elf); + return NULL; +} + +#else + +/** + * Fallback implementation when libelf is not available + */ +const char* ffBinaryExtractStrings(const char* file, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) { + FF_UNUSED(file, cb, userdata, minLength); + return "Fastfetch was built without libelf support"; +} + +#endif diff --git a/src/common/impl/binary_windows.c b/src/common/impl/binary_windows.c new file mode 100644 index 0000000..918cad5 --- /dev/null +++ b/src/common/impl/binary_windows.c @@ -0,0 +1,71 @@ +#include "common/binary.h" +#include "common/io.h" +#include "common/strutil.h" +#include "common/windows/nt.h" + +#include <windows.h> +#include <stdlib.h> +#include <string.h> + +/** + * Extracts string literals from a PE (Windows) executable + * + * This function maps the PE file into memory, locates the .rdata section + * (which typically contains string literals), and scans it for valid strings. + * Each string found is passed to the callback function for processing. + */ +const char* ffBinaryExtractStrings(const char* peFile, bool (*cb)(const char* str, uint32_t len, void* userdata), void* userdata, uint32_t minLength) { + FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileA(peFile, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) { + return "CreateFileA() failed"; + } + + FF_AUTO_CLOSE_FD HANDLE hSection = NULL; + if (!NT_SUCCESS(NtCreateSection(&hSection, SECTION_MAP_READ, NULL, NULL, PAGE_READONLY, SEC_COMMIT, hFile))) { + return "NtCreateSection() failed"; + } + + PVOID base = NULL; + SIZE_T viewSize = 0; + if (!NT_SUCCESS(NtMapViewOfSection(hSection, NtCurrentProcess(), &base, 0, 0, NULL, &viewSize, ViewUnmap, 0, PAGE_READONLY))) { + return "NtMapViewOfSection() failed"; + } + + PIMAGE_NT_HEADERS ntHeaders = RtlImageNtHeader(base); + if (!ntHeaders) { + NtUnmapViewOfSection(NtCurrentProcess(), base); + return "RtlImageNtHeader() failed"; + } + + PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); + for (WORD i = 0; i < ntHeaders->FileHeader.NumberOfSections; ++i, ++section) { + // Look for initialized data sections with the name ".rdata" which typically contains string literals + if ((section->Characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) && ffStrEquals((const char*) section->Name, ".rdata")) { + uint8_t* data = (uint8_t*) base + section->PointerToRawData; + + // Scan the section for string literals + for (size_t off = 0; off < section->SizeOfRawData; ++off) { + const char* p = (const char*) data + off; + if (*p == '\0') { + continue; + } + uint32_t len = (uint32_t) strnlen(p, section->SizeOfRawData - off); + if (len < minLength) { + off += len; + continue; + } + // Only process printable ASCII characters + if (*p >= ' ' && *p <= '~') // Ignore control characters + { + if (!cb(p, len, userdata)) { + break; + } + } + off += len; + } + } + } + + NtUnmapViewOfSection(NtCurrentProcess(), base); + return NULL; +} diff --git a/src/common/impl/commandoption.c b/src/common/impl/commandoption.c new file mode 100644 index 0000000..2f548df --- /dev/null +++ b/src/common/impl/commandoption.c @@ -0,0 +1,261 @@ +#include "common/commandoption.h" +#include "common/color.h" +#include "common/printing.h" +#include "common/time.h" +#include "common/jsonconfig.h" +#include "common/strutil.h" +#include "fastfetch_datatext.h" +#include "modules/modules.h" + +#include <ctype.h> +#include <inttypes.h> + +bool ffParseModuleOptions(const char* key, const char* value) { + if (!ffStrStartsWith(key, "--") || !ffCharIsEnglishAlphabet(key[2])) { + return false; + } + if (value && !*value) { + value = NULL; + } + for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(key[2]) - 'A']; *modules; ++modules) { + FFModuleBaseInfo* baseInfo = *modules; + const char* subKey = ffOptionTestPrefix(key, baseInfo->name); + if (subKey != NULL) { + if (subKey[0] == '\0' || subKey[0] == '-') // Key is exactly the module name or has a leading '-' + { + fprintf(stderr, "Error: unknown module key %s\n", key); + exit(477); + } + + FF_STRBUF_AUTO_DESTROY moduleName = ffStrbufCreateS(baseInfo->name); + ffStrbufLowerCase(&moduleName); + + FF_STRBUF_AUTO_DESTROY jsonKey = ffStrbufCreate(); + bool flag = false; + for (const char* p = subKey; *p; ++p) { + if (*p == '-') { + if (flag) { + fprintf(stderr, "Error: invalid double `-` in module key %s\n", key); + exit(477); + } + flag = true; + } else { + if (!isalpha((unsigned char) *p) && !isdigit((unsigned char) *p)) { + fprintf(stderr, "Error: invalid character `%c` in module key %s\n", *p, key); + exit(477); + } + + if (flag) { + flag = false; + ffStrbufAppendC(&jsonKey, (char) toupper((unsigned char) *p)); + } else { + ffStrbufAppendC(&jsonKey, *p); + } + } + } + fprintf(stderr, "Error: Unsupported module option: %s\n", key); + fputs(" Support of module options has been removed. Please add the flag to the JSON config instead.\n", stderr); + fprintf(stderr, " Example (demonstration only): `{ \"modules\": [ { \"type\": \"%s\", \"%s\": %s%s%s } ] }`\n", moduleName.chars, jsonKey.chars, value ? "\"" : "", value ?: "true", value ? "\"" : ""); + fputs(" See <https://github.com/fastfetch-cli/fastfetch/wiki/Configuration> for more information.\n", stderr); + exit(477); + } + } + return false; +} + +void ffPrepareCommandOption(FFdata* data) { + char* moduleType = NULL; + size_t moduleLen = 0; + while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) { +#define FF_IF_MODULE_MATCH(moduleNameConstant) if (moduleLen == strlen(moduleNameConstant) && ffStrEqualsIgnCase(moduleType, moduleNameConstant) && !ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleNameConstant, ':')) + + switch (moduleType[0]) { + #if !FF_MODULE_DISABLE_CPUUSAGE + case 'C': + case 'c': + FF_IF_MODULE_MATCH(FF_CPUUSAGE_MODULE_NAME) + ffPrepareCPUUsage(); + break; + #endif + + #if !FF_MODULE_DISABLE_DISKIO + case 'D': + case 'd': + FF_IF_MODULE_MATCH(FF_DISKIO_MODULE_NAME) { + FF_A_CLEANUP(ffDestroyDiskIOOptions) FFDiskIOOptions options; + ffInitDiskIOOptions(&options); + ffPrepareDiskIO(&options); + } + break; + #endif + + #if !FF_MODULE_DISABLE_NETIO + case 'N': + case 'n': + FF_IF_MODULE_MATCH(FF_NETIO_MODULE_NAME) { + FF_A_CLEANUP(ffDestroyNetIOOptions) FFNetIOOptions options; + ffInitNetIOOptions(&options); + ffPrepareNetIO(&options); + } + break; + #endif + + #if !FF_MODULE_DISABLE_PUBLICIP + case 'P': + case 'p': + FF_IF_MODULE_MATCH(FF_PUBLICIP_MODULE_NAME) { + FF_A_CLEANUP(ffDestroyPublicIpOptions) FFPublicIPOptions options; + ffInitPublicIpOptions(&options); + ffPreparePublicIp(&options); + } + break; + #endif + + #if !FF_MODULE_DISABLE_WEATHER + case 'W': + case 'w': + FF_IF_MODULE_MATCH(FF_WEATHER_MODULE_NAME) { + FF_A_CLEANUP(ffDestroyWeatherOptions) FFWeatherOptions options; + ffInitWeatherOptions(&options); + ffPrepareWeather(&options); + } + break; + #endif + } + +#undef FF_IF_MODULE_MATCH + } +} + +static void genJsonConfig(FFdata* data, FFModuleBaseInfo* baseInfo, void* options) { + yyjson_mut_doc* doc = data->resultDoc; + + yyjson_mut_val* modules = yyjson_mut_obj_get(doc->root, "modules"); + if (!modules) { + modules = yyjson_mut_obj_add_arr(doc, doc->root, "modules"); + } + + FF_STRBUF_AUTO_DESTROY type = ffStrbufCreateS(baseInfo->name); + ffStrbufLowerCase(&type); + + if (data->docType == FF_RESULT_DOC_TYPE_CONFIG_FULL) { + yyjson_mut_val* module = yyjson_mut_obj(doc); + yyjson_mut_obj_add_strbuf(doc, module, "type", &type); + + if (baseInfo->generateJsonConfig) { + baseInfo->generateJsonConfig(options, doc, module); + } + + if (yyjson_mut_obj_size(module) > 1) { + yyjson_mut_arr_add_val(modules, module); + } else { + yyjson_mut_arr_add_strbuf(doc, modules, &type); + } + } else { + yyjson_mut_arr_add_strbuf(doc, modules, &type); + } +} + +static void genJsonResult(FFdata* data, FFModuleBaseInfo* baseInfo, void* options) { + yyjson_mut_doc* doc = data->resultDoc; + yyjson_mut_val* module = yyjson_mut_arr_add_obj(doc, doc->root); + yyjson_mut_obj_add_str(doc, module, "type", baseInfo->name); + if (baseInfo->generateJsonResult) { + baseInfo->generateJsonResult(options, doc, module); + } else { + yyjson_mut_obj_add_str(doc, module, "error", "Unsupported for JSON format"); + } +} + +static bool parseStructureCommand( + FFdata* data, + const char* line, + void (*fn)(FFdata*, FFModuleBaseInfo* baseInfo, void* options)) { + if (ffCharIsEnglishAlphabet(line[0])) { + for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(line[0]) - 'A']; *modules; ++modules) { + FFModuleBaseInfo* baseInfo = *modules; + if (ffStrEqualsIgnCase(line, baseInfo->name)) { + uint8_t optionBuf[FF_OPTION_MAX_SIZE]; + baseInfo->initOptions(optionBuf); + if (data->resultDoc != NULL) { + fn(data, baseInfo, optionBuf); + } else { + baseInfo->printModule(optionBuf); + } + baseInfo->destroyOptions(optionBuf); + return true; + } + } + } + + if (data->resultDoc) { + yyjson_mut_doc* doc = data->resultDoc; + yyjson_mut_val* module = yyjson_mut_arr_add_obj(doc, doc->root); + yyjson_mut_obj_add_str(doc, module, "type", line); + yyjson_mut_obj_add_str(doc, module, "error", "Unknown module type"); + } else { + ffPrintError(line, 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "<no implementation provided>"); + } + return false; +} + +void ffPrintCommandOption(FFdata* data) { + // Parse the structure and call the modules + int32_t thres = instance.config.display.stat; + + char* moduleType = NULL; + size_t moduleLen = 0; + while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) { + if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleType, ':')) { + continue; + } + + double ms = 0; + if (thres >= 0) { + ms = ffTimeGetTick(); + } + + parseStructureCommand(data, moduleType, genJsonResult); + + if (thres >= 0) { + ms = ffTimeGetTick() - ms; + + if (data->resultDoc) { + yyjson_mut_val* moduleJson = yyjson_mut_arr_get_last(data->resultDoc->root); + yyjson_mut_obj_add_real(data->resultDoc, moduleJson, "stat", ms); + } else { + char str[64]; + int len = snprintf(str, sizeof str, "%.3fms", ms); + if (thres > 0) { + snprintf(str, sizeof str, "\e[%sm%.3fms\e[m", (ms <= thres ? FF_COLOR_FG_GREEN : ms <= 2 * thres ? FF_COLOR_FG_YELLOW + : FF_COLOR_FG_RED), + ms); + } + printf("\e7\e[1A\e[9999999C\e[%dD%s\e8", len - 1, str); // Save; Up 1; Right 9999999; Left <len - 1>; Print <str>; Load + } + } + +#if defined(_WIN32) + if (!data->resultDoc && !instance.config.display.noBuffer) { + fflush(stdout); + } +#endif + } +} + +void ffMigrateCommandOptionToJsonc(FFdata* data) { + // If we don't have a custom structure, use the default one + if (data->structure.length == 0) { + ffStrbufAppendS(&data->structure, FASTFETCH_DATATEXT_STRUCTURE); // Cannot use `ffStrbufSetStatic` here because we will modify the string + } + + char* moduleType = NULL; + size_t moduleLen = 0; + while (ffStrbufGetdelim(&moduleType, &moduleLen, ':', &data->structure)) { + if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, moduleType, ':')) { + continue; + } + + parseStructureCommand(data, moduleType, genJsonConfig); + } +} diff --git a/src/common/impl/dbus.c b/src/common/impl/dbus.c new file mode 100644 index 0000000..473af2d --- /dev/null +++ b/src/common/impl/dbus.c @@ -0,0 +1,334 @@ +#include "common/dbus.h" + +#ifdef FF_HAVE_DBUS + + #include "common/thread.h" + #include "common/strutil.h" + +static bool loadLibSymbols(FFDBusLibrary* lib) { + FF_LIBRARY_LOAD(dbus, false, "libdbus-1" FF_LIBRARY_EXTENSION, 4); + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_bus_get, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_new_method_call, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_append_args, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_init, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_get_arg_type, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_get_basic, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_recurse, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_has_next, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_iter_next, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_message_unref, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_connection_send_with_reply_and_block, false) + FF_LIBRARY_LOAD_SYMBOL_PTR(dbus, lib, dbus_connection_unref, false) + dbus = NULL; // don't auto dlclose + return true; +} + +static const FFDBusLibrary* loadLib(void) { + static FFDBusLibrary lib; + static bool loaded = false; + static bool loadSuccess = false; + + if (!loaded) { + loaded = true; + loadSuccess = loadLibSymbols(&lib); + } + + return loadSuccess ? &lib : NULL; +} + +const char* ffDBusLoadData(DBusBusType busType, FFDBusData* data) { + data->lib = loadLib(); + if (data->lib == NULL) { + return "Failed to load DBus library"; + } + + data->connection = data->lib->ffdbus_bus_get(busType, NULL); + if (data->connection == NULL) { + return "Failed to connect to DBus"; + } + + return NULL; +} + +void ffDBusDestroyData(FFDBusData* data) { + if (data->connection != NULL) { + data->lib->ffdbus_connection_unref(data->connection); + data->connection = NULL; + } +} + +bool ffDBusGetString(FFDBusData* dbus, DBusMessageIter* iter, FFstrbuf* result) { + int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter); + + if (argType == DBUS_TYPE_STRING || argType == DBUS_TYPE_OBJECT_PATH) { + const char* value = NULL; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + + if (!ffStrSet(value)) { + return false; + } + + ffStrbufAppendS(result, value); + return true; + } + + if (argType == DBUS_TYPE_BYTE) { + uint8_t value; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + ffStrbufAppendC(result, (char) value); + return true; + } + + if (argType != DBUS_TYPE_VARIANT && argType != DBUS_TYPE_ARRAY) { + return false; + } + + DBusMessageIter subIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &subIter); + + if (argType == DBUS_TYPE_VARIANT) { + return ffDBusGetString(dbus, &subIter, result); + } + + // At this point we have an array + + int subArgType = dbus->lib->ffdbus_message_iter_get_arg_type(&subIter); + if (subArgType == DBUS_TYPE_INVALID) { + return false; + } + + if (subArgType == DBUS_TYPE_BYTE) { + while (true) { + uint8_t value; + dbus->lib->ffdbus_message_iter_get_basic(&subIter, &value); + ffStrbufAppendC(result, (char) value); + + if (!dbus->lib->ffdbus_message_iter_next(&subIter)) { + break; + } + } + + return true; + } + + bool foundAValue = false; + + while (true) { + if (ffDBusGetString(dbus, &subIter, result)) { + foundAValue = true; + ffStrbufAppendS(result, ", "); + } + + if (!dbus->lib->ffdbus_message_iter_next(&subIter)) { + break; + } else { + continue; + } + } + + if (foundAValue) { + ffStrbufSubstrBefore(result, result->length - 2); + } + + return foundAValue; +} + +bool ffDBusGetBool(FFDBusData* dbus, DBusMessageIter* iter, bool* result) { + int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter); + + if (argType == DBUS_TYPE_BOOLEAN) { + dbus_bool_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = value != 0; + return true; + } + + if (argType != DBUS_TYPE_VARIANT) { + return false; + } + + DBusMessageIter subIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &subIter); + return ffDBusGetBool(dbus, &subIter, result); +} + +bool ffDBusGetUint(FFDBusData* dbus, DBusMessageIter* iter, uint64_t* result) { + int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter); + + if (argType == DBUS_TYPE_BYTE) { + uint8_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = value; + return true; + } + + if (argType == DBUS_TYPE_UINT16) { + uint16_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = value; + return true; + } + + if (argType == DBUS_TYPE_UINT32) { + uint32_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = value; + return true; + } + + if (argType == DBUS_TYPE_UINT64) { + dbus->lib->ffdbus_message_iter_get_basic(iter, result); + return true; + } + + if (argType != DBUS_TYPE_VARIANT) { + return false; + } + + DBusMessageIter subIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &subIter); + return ffDBusGetUint(dbus, &subIter, result); +} + +bool ffDBusGetInt(FFDBusData* dbus, DBusMessageIter* iter, int64_t* result) { + int argType = dbus->lib->ffdbus_message_iter_get_arg_type(iter); + + if (argType == DBUS_TYPE_INT16) { + int16_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = value; + return true; + } + + if (argType == DBUS_TYPE_INT32) { + int32_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = value; + return true; + } + + if (argType == DBUS_TYPE_INT64) { + dbus->lib->ffdbus_message_iter_get_basic(iter, result); + return true; + } + + if (argType == DBUS_TYPE_BYTE) { + uint8_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = value; + return true; + } + + if (argType == DBUS_TYPE_UINT16) { + uint16_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = (int32_t) value; + return true; + } + + if (argType == DBUS_TYPE_UINT32) { + uint32_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + *result = (int32_t) value; + return true; + } + + if (argType == DBUS_TYPE_UINT64) { + uint64_t value = 0; + dbus->lib->ffdbus_message_iter_get_basic(iter, &value); + if (value > INT64_MAX) { + return false; + } + *result = (int64_t) value; + return true; + } + + if (argType != DBUS_TYPE_VARIANT) { + return false; + } + + DBusMessageIter subIter; + dbus->lib->ffdbus_message_iter_recurse(iter, &subIter); + return ffDBusGetInt(dbus, &subIter, result); +} + +DBusMessage* ffDBusGetMethodReply(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* method, const char* arg1, const char* arg2) { + DBusMessage* message = dbus->lib->ffdbus_message_new_method_call(busName, objectPath, interface, method); + if (message == NULL) { + return NULL; + } + + if (arg1) { + if (arg2) { + dbus->lib->ffdbus_message_append_args(message, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_STRING, &arg2, DBUS_TYPE_INVALID); + } else { + dbus->lib->ffdbus_message_append_args(message, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_INVALID); + } + } + + DBusMessage* reply = dbus->lib->ffdbus_connection_send_with_reply_and_block(dbus->connection, message, instance.config.general.processingTimeout, NULL); + + dbus->lib->ffdbus_message_unref(message); + + return reply; +} + +DBusMessage* ffDBusGetProperty(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property) { + DBusMessage* message = dbus->lib->ffdbus_message_new_method_call(busName, objectPath, "org.freedesktop.DBus.Properties", "Get"); + if (message == NULL) { + return NULL; + } + + dbus->lib->ffdbus_message_append_args(message, + DBUS_TYPE_STRING, + &interface, + DBUS_TYPE_STRING, + &property, + DBUS_TYPE_INVALID); + + DBusMessage* reply = dbus->lib->ffdbus_connection_send_with_reply_and_block(dbus->connection, message, instance.config.general.processingTimeout, NULL); + + dbus->lib->ffdbus_message_unref(message); + + return reply; +} + +bool ffDBusGetPropertyString(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property, FFstrbuf* result) { + DBusMessage* reply = ffDBusGetProperty(dbus, busName, objectPath, interface, property); + if (reply == NULL) { + return false; + } + + DBusMessageIter rootIterator; + if (!dbus->lib->ffdbus_message_iter_init(reply, &rootIterator)) { + dbus->lib->ffdbus_message_unref(reply); + return false; + } + + bool ret = ffDBusGetString(dbus, &rootIterator, result); + + dbus->lib->ffdbus_message_unref(reply); + + return ret; +} + +bool ffDBusGetPropertyUint(FFDBusData* dbus, const char* busName, const char* objectPath, const char* interface, const char* property, uint64_t* result) { + DBusMessage* reply = ffDBusGetProperty(dbus, busName, objectPath, interface, property); + if (reply == NULL) { + return false; + } + + DBusMessageIter rootIterator; + if (!dbus->lib->ffdbus_message_iter_init(reply, &rootIterator)) { + dbus->lib->ffdbus_message_unref(reply); + return false; + } + + bool ret = ffDBusGetUint(dbus, &rootIterator, result); + + dbus->lib->ffdbus_message_unref(reply); + + return ret; +} + +#endif // FF_HAVE_DBUS diff --git a/src/common/impl/debug_windows.c b/src/common/impl/debug_windows.c new file mode 100644 index 0000000..fb76a65 --- /dev/null +++ b/src/common/impl/debug_windows.c @@ -0,0 +1,55 @@ +#include "common/debug.h" +#include "common/windows/nt.h" + +#include <windows.h> + +const char* ffDebugWin32Error(DWORD errorCode) { + static char buffer[512]; + + wchar_t bufferW[256]; + ULONG len = FormatMessageW( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + (DWORD) errorCode, + 0, + bufferW, + ARRAY_SIZE(bufferW), + NULL); + + if (len == 0) { + snprintf(buffer, sizeof(buffer), "Unknown error code (%lu)", errorCode); + } else { + // Remove trailing newline + while (len > 0 && (bufferW[len - 1] == '\r' || bufferW[len - 1] == '\n')) { + --len; + } + + if (NT_SUCCESS(RtlUnicodeToUTF8N(buffer, sizeof(buffer), &len, bufferW, len * sizeof(wchar_t)))) { + snprintf(buffer + len, sizeof(buffer) - len, " (%lu)", errorCode); + } else { + snprintf(buffer, sizeof(buffer), "Unknown error (%lu)", errorCode); + } + } + + return buffer; +} + +const char* ffDebugNtStatus(NTSTATUS status) { + return ffDebugWin32Error(RtlNtStatusToDosError(status)); +} + +static inline DWORD HRESULTToWin32Error(HRESULT hr) { + if (SUCCEEDED(hr)) { + return ERROR_SUCCESS; + } + + if (HRESULT_FACILITY(hr) == FACILITY_WIN32) { + return HRESULT_CODE(hr); + } + + return ERROR_INTERNAL_ERROR; +} + +const char* ffDebugHResult(HRESULT hr) { + return ffDebugWin32Error(HRESULTToWin32Error(hr)); +} diff --git a/src/common/impl/duration.c b/src/common/impl/duration.c new file mode 100644 index 0000000..a6a4965 --- /dev/null +++ b/src/common/impl/duration.c @@ -0,0 +1,87 @@ +#include "common/duration.h" + +void ffDurationAppendNum(uint64_t totalSeconds, FFstrbuf* result) { + const FFOptionsDisplay* options = &instance.config.display; + + bool spaceBeforeUnit = options->durationSpaceBeforeUnit != FF_SPACE_BEFORE_UNIT_NEVER; + + if (totalSeconds < 60) { + ffStrbufAppendUInt(result, totalSeconds); + if (spaceBeforeUnit) { + ffStrbufAppendC(result, ' '); + } + ffStrbufAppendS(result, options->durationAbbreviation ? "sec" : "second"); + if (totalSeconds != 1) { + ffStrbufAppendC(result, 's'); + } + return; + } + + uint32_t seconds = (uint32_t) (totalSeconds % 60); + totalSeconds /= 60; + if (seconds >= 30) { + totalSeconds++; + } + + uint32_t minutes = (uint32_t) (totalSeconds % 60); + totalSeconds /= 60; + uint32_t hours = (uint32_t) (totalSeconds % 24); + totalSeconds /= 24; + uint32_t days = (uint32_t) totalSeconds; + + if (days > 0) { + ffStrbufAppendUInt(result, days); + if (spaceBeforeUnit) { + ffStrbufAppendC(result, ' '); + } + if (options->durationAbbreviation) { + ffStrbufAppendC(result, 'd'); + + if (hours > 0 || minutes > 0) { + ffStrbufAppendC(result, ' '); + } + } else { + ffStrbufAppendS(result, days == 1 ? "day" : "days"); + + if (days >= 100) { + ffStrbufAppendS(result, "(!)"); + } + + if (hours > 0 || minutes > 0) { + ffStrbufAppendS(result, ", "); + } + } + } + + if (hours > 0) { + ffStrbufAppendUInt(result, hours); + if (spaceBeforeUnit) { + ffStrbufAppendC(result, ' '); + } + if (options->durationAbbreviation) { + ffStrbufAppendC(result, 'h'); + + if (minutes > 0) { + ffStrbufAppendC(result, ' '); + } + } else { + ffStrbufAppendS(result, hours == 1 ? "hour" : "hours"); + + if (minutes > 0) { + ffStrbufAppendS(result, ", "); + } + } + } + + if (minutes > 0) { + ffStrbufAppendUInt(result, minutes); + if (spaceBeforeUnit) { + ffStrbufAppendC(result, ' '); + } + if (options->durationAbbreviation) { + ffStrbufAppendC(result, 'm'); + } else { + ffStrbufAppendS(result, minutes == 1 ? "min" : "mins"); + } + } +} diff --git a/src/common/impl/edidHelper.c b/src/common/impl/edidHelper.c new file mode 100644 index 0000000..1e3310e --- /dev/null +++ b/src/common/impl/edidHelper.c @@ -0,0 +1,132 @@ +#include "common/edidHelper.h" + +void ffEdidGetPhysicalResolution(const uint8_t edid[128], uint32_t* width, uint32_t* height) { + const int dtd = 54; + *width = (((uint32_t) edid[dtd + 4] >> 4) << 8) | edid[dtd + 2]; + *height = (((uint32_t) edid[dtd + 7] >> 4) << 8) | edid[dtd + 5]; +} + +void ffEdidGetPreferredResolutionAndRefreshRate(const uint8_t edid[128], uint32_t* width, uint32_t* height, double* refreshRate) { + for (uint32_t i = 0x36; i < 0x7E; i += 0x12) { // read through descriptor blocks... + if (edid[i] != 0x00 && edid[i + 1] != 0x00) { // a dtd + uint32_t hactive = edid[i + 2] + (uint32_t) ((edid[i + 4] & 0xf0) << 4); + uint32_t hblank = edid[i + 3] + (uint32_t) ((edid[i + 4] & 0x0f) << 8); + uint32_t vactive = edid[i + 5] + (uint32_t) ((edid[i + 7] & 0xf0) << 4); + uint32_t vblank = edid[i + 6] + (uint32_t) ((edid[i + 7] & 0x0f) << 8); + uint32_t pixclk = ((uint32_t) edid[i + 1] << 8) | (edid[i]); + *width = hactive; + *height = vactive; + *refreshRate = (double) pixclk * 10000 / (double) (hactive + hblank) / (double) (vactive + vblank); + return; + } + } +} + +void ffEdidGetVendorAndModel(const uint8_t edid[128], FFstrbuf* result) { + // https://github.com/jinksong/read_edid/blob/master/parse-edid/parse-edid.c + ffStrbufAppendF(result, "%c%c%c%04X", (char) (((uint32_t) edid[8] >> 2 & 0x1f) + 'A' - 1), (char) (((((uint32_t) edid[8] & 0x3) << 3) | (((uint32_t) edid[9] & 0xe0) >> 5)) + 'A' - 1), (char) (((uint32_t) edid[9] & 0x1f) + 'A' - 1), (uint32_t) (edid[10] + (uint32_t) (edid[11] << 8))); +} + +bool ffEdidGetName(const uint8_t edid[128], FFstrbuf* name) { + // https://github.com/jinksong/read_edid/blob/master/parse-edid/parse-edid.c + for (uint32_t i = 0x36; i < 0x7E; i += 0x12) { // read through descriptor blocks... + if (edid[i] == 0x00) { // not a timing descriptor + if (edid[i + 3] == 0xfc) { // Model Name tag + for (uint32_t j = 0; j < 13; j++) { + if (edid[i + 5 + j] == 0x0a) { + ffStrbufAppendNS(name, j, (const char*) &edid[i + 5]); + return true; + } + } + } + } + } + + // use manufacturer + model number as monitor name + ffEdidGetVendorAndModel(edid, name); + return false; +} + +void ffEdidGetPhysicalSize(const uint8_t edid[128], uint32_t* width, uint32_t* height) { + // Detailed Timing Descriptors + uint32_t dw = (((uint32_t) edid[68] & 0xF0) << 4) + edid[66]; + uint32_t dh = (((uint32_t) edid[68] & 0x0F) << 8) + edid[67]; + + // Basic Display Parameters + uint32_t bw = edid[21] * 10; + uint32_t bh = edid[22] * 10; + + // Some monitors report invalid data in DTD. See #1406 + if (abs((int) dw - (int) bw) < 10 && abs((int) dh - (int) bh) < 10) { + *width = dw; + *height = dh; + } else { + *width = bw; + *height = bh; + } +} + +void ffEdidGetSerialAndManufactureDate(const uint8_t edid[128], uint32_t* serial, uint16_t* year, uint16_t* week) { + if (edid[17] > 0 && edid[17] < 0xFF) { + *year = (uint16_t) (edid[17] + 1990); + *week = (uint16_t) edid[16]; + if (*week == 0xFF) { + *week = 0; + } + } else { + *year = *week = 0; + } + + *serial = *(uint32_t*) &edid[12]; +} + +bool ffEdidGetHdrCompatible(const uint8_t* edid, uint32_t length) { + if (length <= 128) { + return false; + } + for (const uint8_t* cta = &edid[128]; cta < &edid[length]; cta += 128) { + // https://en.wikipedia.org/wiki/Extended_Display_Identification_Data#CTA_EDID_Timing_Extension_Block + if (cta[0] != 0x02 /* CTA EDID */) { + continue; + } + if (cta[1] < 0x03 /* Version 3 */) { + continue; + } + const uint8_t offset = cta[2]; + if (offset <= 4) { + continue; + } + for (uint8_t i = 4; i < offset;) { + uint8_t blkLen = cta[i] & 0x1f; + if (blkLen > 0) { + uint8_t blkTag = (cta[i] & 0xe0) >> 5; + if (blkTag == 0x07 /* Extended Block Type Tag */) { + uint8_t extendedTag = cta[i + 1]; + if (extendedTag == 6 /* HDR SMDB */ || extendedTag == 7 /* HDR DMDB */) { + return true; + } + } + } + i += (uint8_t) (blkLen + 1); + } + } + return false; +} + +bool ffEdidIsValid(const uint8_t edid[128], uint32_t length) { + if (length < 128 || length % 128 != 0) { + return false; + } + + static const uint8_t edidHeader[] = { 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 }; + if (memcmp(edid, edidHeader, sizeof(edidHeader)) != 0) { + return false; + } + + uint8_t sum = 0; + for (uint32_t i = 0; i < 128; i++) { + sum += edid[i]; + } + + return sum == 0; +} diff --git a/src/common/impl/font.c b/src/common/impl/font.c new file mode 100644 index 0000000..93feff1 --- /dev/null +++ b/src/common/impl/font.c @@ -0,0 +1,513 @@ +#include "fastfetch.h" +#include "common/FFlist.h" +#include "common/FFstrbuf.h" +#include "common/strutil.h" +#include "common/font.h" + +#include <string.h> +#include <ctype.h> + +void ffFontInit(FFfont* font) { + // Ensure no memory allocates + ffStrbufInit(&font->pretty); + ffStrbufInit(&font->name); + ffStrbufInit(&font->size); + ffListInit(&font->styles); +} + +static void strbufAppendNSExcludingC(FFstrbuf* strbuf, uint32_t length, const char* value, char exclude) { + if (value == NULL || length == 0) { + return; + } + + ffStrbufEnsureFree(strbuf, length); + + for (uint32_t i = 0; i < length; i++) { + if (value[i] != exclude) { + strbuf->chars[strbuf->length++] = value[i]; + } + } + + strbuf->chars[strbuf->length] = '\0'; +} + +static void fontInitPretty(FFfont* font) { + ffStrbufAppend(&font->pretty, &font->name); + + if (font->size.length == 0 && font->styles.length == 0) { + return; + } else if (font->pretty.length == 0) { + ffStrbufAppendS(&font->pretty, "default"); + } + + ffStrbufAppendS(&font->pretty, " ("); + + if (font->size.length > 0) { + ffStrbufAppend(&font->pretty, &font->size); + if (!ffStrbufEndsWithS(&font->size, "pt") && !ffStrbufEndsWithS(&font->size, "px")) { + ffStrbufAppendS(&font->pretty, "pt"); + } + + if (font->styles.length > 0) { + ffStrbufAppendS(&font->pretty, ", "); + } + } + + for (uint32_t i = 0; i < font->styles.length; i++) { + ffStrbufAppend(&font->pretty, FF_LIST_GET(FFstrbuf, font->styles, i)); + + if (i < font->styles.length - 1) { + ffStrbufAppendS(&font->pretty, ", "); + } + } + + ffStrbufAppendC(&font->pretty, ')'); +} + +void ffFontInitQt(FFfont* font, const char* data) { + ffFontInit(font); + + // See https://doc.qt.io/qt-5/qfont.html#toString + + // Family + data = ffStrbufAppendSUntilC(&font->name, data, ','); + ffStrbufTrim(&font->name, ' '); + if (!data) { + goto exit; + } + data++; + + // Size + data = ffStrbufAppendSUntilC(&font->size, data, ','); + ffStrbufTrim(&font->size, ' '); + if (!data) { + goto exit; + } + data++; + + // Style + data = strrchr(data, ','); + if (!data) { + goto exit; + } + data++; + if (isalpha(*data)) { + do { + FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles); + ffStrbufInit(style); + data = ffStrbufAppendSUntilC(style, data, ' '); + if (data) { + data++; + } + } while (data); + } + +exit: + fontInitPretty(font); +} + +static void fontPangoParseWord(const char** data, FFfont* font, FFstrbuf* alternativeBuffer) { + while (**data == ' ' || **data == '\t' || **data == ',') { + ++(*data); + } + + const char* wordStart = *data; + + while (**data != ' ' && **data != '\t' && **data != ',' && **data != '\0' && **data != '`' && **data != '\\') { + ++(*data); + } + + uint32_t wordLength = (uint32_t) (*data - wordStart); + if (wordLength == 0) { + return; + } + + if (**data == '\0' || **data == '`' || **data == '\\') { + ffStrbufAppendNS(&font->size, wordLength, wordStart); + if (ffStrbufEndsWithS(&font->size, "px")) { + ffStrbufSubstrBefore(&font->size, font->size.length - 2); + } + + double dummy; + if (sscanf(font->size.chars, "%lf", &dummy) == 1) { + return; + } + + ffStrbufClear(&font->size); + } + + if ( + ffStrStartsWithIgnCase(wordStart, "Ultra") || + ffStrStartsWithIgnCase(wordStart, "Extra") || + ffStrStartsWithIgnCase(wordStart, "Semi") || + ffStrStartsWithIgnCase(wordStart, "Demi") || + ffStrStartsWithIgnCase(wordStart, "Normal") || + ffStrStartsWithIgnCase(wordStart, "Roman") || + ffStrStartsWithIgnCase(wordStart, "Oblique") || + ffStrStartsWithIgnCase(wordStart, "Italic") || + ffStrStartsWithIgnCase(wordStart, "Thin") || + ffStrStartsWithIgnCase(wordStart, "Light") || + ffStrStartsWithIgnCase(wordStart, "Bold") || + ffStrStartsWithIgnCase(wordStart, "Black") || + ffStrStartsWithIgnCase(wordStart, "Condensed") || + ffStrStartsWithIgnCase(wordStart, "Expanded")) { + if (alternativeBuffer == NULL) { + alternativeBuffer = FF_LIST_ADD(FFstrbuf, font->styles); + ffStrbufInit(alternativeBuffer); + } + + strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-'); + + if ( + ffStrStartsWithIgnCase(wordStart, "Ultra ") || + ffStrStartsWithIgnCase(wordStart, "Extra ") || + ffStrStartsWithIgnCase(wordStart, "Semi ") || + ffStrStartsWithIgnCase(wordStart, "Demi ")) { + fontPangoParseWord(data, font, alternativeBuffer); + } + + return; + } + + if (alternativeBuffer != NULL) { + strbufAppendNSExcludingC(alternativeBuffer, wordLength, wordStart, '-'); + return; + } + + if (font->name.length > 0) { + ffStrbufAppendC(&font->name, ' '); + } + ffStrbufAppendNS(&font->name, wordLength, wordStart); +} + +void ffFontInitPango(FFfont* font, const char* data) { + ffFontInit(font); + + while (*data != '\0' && *data != '`' && *data != '\\') { + fontPangoParseWord(&data, font, NULL); + } + + fontInitPretty(font); +} + +void ffFontInitValues(FFfont* font, const char* name, const char* size) { + ffFontInit(font); + + ffStrbufAppendS(&font->name, name); + ffStrbufTrim(&font->name, '"'); + ffStrbufAppendS(&font->size, size); + + fontInitPretty(font); +} + +void ffFontInitXlfd(FFfont* font, const char* xlfd) { + assert(xlfd && *xlfd); + + // https://en.wikipedia.org/wiki/X_logical_font_description + ffFontInit(font); + + // XLFD: -foundry-family-weight-slant-setwidth-addstyle-pixelsize-pointsize-xres-yres-spacing-averagewidth-charsetregistry-charsetencoding + // It often starts with '-', which would create an empty first field. Skip it to align indexes. + if (*xlfd == '-') { + xlfd++; + } + + const char* pstart = xlfd; + + for (int field = 0; field < 14; field++) { + const char* pend = strchr(pstart, '-'); + uint32_t length = pend ? (uint32_t) (pend - pstart) : (uint32_t) strlen(pstart); + + if (length > 0) { + if (field == 1) // family + { + ffStrbufAppendNS(&font->name, length, pstart); + } else if (field == 7) // pointsize (decipoints, preferred) + { + // parse positive integer from substring + long deciPt = 0; + bool ok = true; + for (uint32_t i = 0; i < length; i++) { + char c = pstart[i]; + if (c < '0' || c > '9') { + ok = false; + break; + } + deciPt = deciPt * 10 + (c - '0'); + } + + if (ok && deciPt > 0) { + ffStrbufClear(&font->size); + + char tmp[32]; + if (deciPt % 10 == 0) { + snprintf(tmp, sizeof(tmp), "%ldpt", deciPt / 10); + } else { + snprintf(tmp, sizeof(tmp), "%ld.%ldpt", deciPt / 10, deciPt % 10); + } + + ffStrbufAppendS(&font->size, tmp); + } + } else if (field == 6) // pixelsize (fallback if pointsize missing/invalid) + { + if (font->size.length == 0) { + long px = 0; + bool ok = true; + for (uint32_t i = 0; i < length; i++) { + char c = pstart[i]; + if (c < '0' || c > '9') { + ok = false; + break; + } + px = px * 10 + (c - '0'); + } + + if (ok && px > 0) { + ffStrbufAppendNS(&font->size, length, pstart); + ffStrbufAppendS(&font->size, "px"); + } + } + } else if (field >= 2 && field <= 5) // weight/slant/setwidth/addstyle + { + // ignore "normal" (case-insensitive) + if (!(length == 6 && ffStrStartsWithIgnCase(pstart, "normal"))) { + FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles); + ffStrbufInitNS(style, length, pstart); + } + } + } + + if (!pend) { + break; + } + + pstart = pend + 1; + } + + fontInitPretty(font); +} + +void ffFontInitXft(FFfont* font, const char* xft) { + assert(xft); + + // https://en.wikipedia.org/wiki/Xft + // Xft/Fontconfig pattern examples: + // "DejaVu Sans Mono-10" + // "monospace:size=10:weight=bold:slant=italic" + // "Fira Code-12:style=Regular" + // Goal: extract family(name), size, and some common styles. + + ffFontInit(font); + + // 1) Parse "head" part before first ':' => usually "family[-size]" (may include commas) + const char* p = xft; + + while (*p == ' ' || *p == '\t') { + ++p; + } + + const char* headStart = p; + while (*p != '\0' && *p != ':') { + ++p; + } + const char* headEnd = p; + + // trim tail spaces + while (headEnd > headStart && (headEnd[-1] == ' ' || headEnd[-1] == '\t')) { + --headEnd; + } + + // If multiple families are listed, take the first one (up to comma) + for (const char* q = headStart; q < headEnd; ++q) { + if (*q == ',') { + headEnd = q; + while (headEnd > headStart && (headEnd[-1] == ' ' || headEnd[-1] == '\t')) { + --headEnd; + } + break; + } + } + + // Try parse trailing "-<number>" as size, otherwise entire head is name + const char* dashPos = NULL; + const char* sizeStart = NULL; + + for (const char* q = headEnd; q > headStart;) { + --q; + if (*q == '-' && (q + 1) < headEnd && ffCharIsDigit(q[1])) { + dashPos = q; + sizeStart = q + 1; + break; + } + } + + if (dashPos) { + bool ok = true; + bool seenDigit = false; + for (const char* q = sizeStart; q < headEnd; ++q) { + if (ffCharIsDigit(*q)) { + seenDigit = true; + } else if (*q == '.') { + continue; + } else { + ok = false; + break; + } + } + + if (ok && seenDigit) { + const char* nameEnd = dashPos; + while (nameEnd > headStart && (nameEnd[-1] == ' ' || nameEnd[-1] == '\t')) { + --nameEnd; + } + + if (nameEnd > headStart) { + ffStrbufAppendNS(&font->name, (uint32_t) (nameEnd - headStart), headStart); + } + + if (headEnd > sizeStart) { + ffStrbufAppendNS(&font->size, (uint32_t) (headEnd - sizeStart), sizeStart); + } + } else { + if (headEnd > headStart) { + ffStrbufAppendNS(&font->name, (uint32_t) (headEnd - headStart), headStart); + } + } + } else { + if (headEnd > headStart) { + ffStrbufAppendNS(&font->name, (uint32_t) (headEnd - headStart), headStart); + } + } + + ffStrbufTrim(&font->name, ' '); + ffStrbufTrim(&font->name, '"'); + + // 2) Parse key=value fields after ':' (Fontconfig-like). Fields separated by ':'. + // Common keys: size, pixelsize, pointsize, style, weight, slant, width + while (*p == ':') { + ++p; + + // key + const char* keyStart = p; + while (*p != '\0' && *p != '=' && *p != ':') { + ++p; + } + const char* keyEnd = p; + + if (*p != '=') { + continue; // skip tokens without '=' + } + + ++p; // skip '=' + + // value (until next ':', allow backslash-escaping) + FF_STRBUF_AUTO_DESTROY value = ffStrbufCreate(); + + while (*p != '\0' && *p != ':') { + if (*p == '\\' && p[1] != '\0') { + ++p; + ffStrbufAppendC(&value, *p); + ++p; + continue; + } + + ffStrbufAppendC(&value, *p); + ++p; + } + + ffStrbufTrim(&value, ' '); + ffStrbufTrim(&value, '"'); + + uint32_t keyLen = (uint32_t) (keyEnd - keyStart); + + // helper: set numeric size if not set yet + const bool sizeEmpty = (font->size.length == 0); + if (value.length > 0) { + if ( + (keyLen == 4 && ffStrStartsWithIgnCase(keyStart, "size")) || + (keyLen == 9 && (ffStrStartsWithIgnCase(keyStart, "pixelsize") || ffStrStartsWithIgnCase(keyStart, "pointsize")))) { + if (sizeEmpty && ffCharIsDigit(value.chars[0])) { + ffStrbufAppend(&font->size, &value); + ffStrbufAppendS(&font->size, + (keyLen == 9 && ffStrStartsWithIgnCase(keyStart, "pixelsize")) ? "px" : "pt"); + } + } else if (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "style")) { + // style may contain multiple words: "Bold Italic" + const char* s = value.chars; + while (*s != '\0') { + while (*s == ' ' || *s == '\t' || *s == ',') { + ++s; + } + + const char* w = s; + while (*s != '\0' && *s != ' ' && *s != '\t' && *s != ',') { + ++s; + } + + if (s > w) { + FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles); + ffStrbufInitNS(style, (uint32_t) (s - w), w); + } + } + } else if ( + (keyLen == 6 && ffStrStartsWithIgnCase(keyStart, "weight")) || + (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "slant")) || + (keyLen == 5 && ffStrStartsWithIgnCase(keyStart, "width"))) { + // normalize: remove '-' to align with other parsers ("Semi-Bold" -> "SemiBold") + FFstrbuf* style = FF_LIST_ADD(FFstrbuf, font->styles); + ffStrbufInit(style); + strbufAppendNSExcludingC(style, value.length, value.chars, '-'); + ffStrbufTrim(style, ' '); + if (style->length == 0) { + ffStrbufDestroy(style); + --font->styles.length; + } + } + } + } + + fontInitPretty(font); +} + +void ffFontInitMoveValues(FFfont* font, FFstrbuf* name, FFstrbuf* size, FFstrbuf* style) { + ffFontInit(font); + + if (name) { + ffStrbufInitMove(&font->name, name); + } + if (size) { + ffStrbufInitMove(&font->size, size); + } + if (style) { + FFstrbuf* styleBuf = FF_LIST_ADD(FFstrbuf, font->styles); + ffStrbufInitMove(styleBuf, style); + } + + fontInitPretty(font); +} + +void ffFontInitWithSpace(FFfont* font, const char* rawName) { + const char* pspace = strrchr(rawName, ' '); + if (pspace == NULL) { + ffFontInitCopy(font, rawName); + return; + } + + ffFontInit(font); + + ffStrbufAppendNS(&font->name, (uint32_t) (pspace - rawName), rawName); + ffStrbufAppendS(&font->size, pspace + 1); + + fontInitPretty(font); +} + +void ffFontDestroy(FFfont* font) { + ffStrbufDestroy(&font->pretty); + ffStrbufDestroy(&font->name); + ffStrbufDestroy(&font->size); + + FF_LIST_FOR_EACH (FFstrbuf, str, font->styles) { + ffStrbufDestroy(str); + } + ffListDestroy(&font->styles); +} diff --git a/src/common/impl/format.c b/src/common/impl/format.c new file mode 100644 index 0000000..db414bc --- /dev/null +++ b/src/common/impl/format.c @@ -0,0 +1,761 @@ +#include "fastfetch.h" +#include "common/format.h" +#include "common/parsing.h" +#include "common/textModifier.h" +#include "common/strutil.h" +#include "common/library.h" + +#include <inttypes.h> + +void ffFormatAppendFormatArg(FFstrbuf* buffer, const FFformatarg* formatarg) { + switch (formatarg->type) { + case FF_ARG_TYPE_INT: + ffStrbufAppendSInt(buffer, *(int32_t*) formatarg->value); + break; + case FF_ARG_TYPE_UINT: + ffStrbufAppendUInt(buffer, *(uint32_t*) formatarg->value); + break; + case FF_ARG_TYPE_UINT64: + ffStrbufAppendUInt(buffer, *(uint64_t*) formatarg->value); + break; + case FF_ARG_TYPE_UINT16: + ffStrbufAppendUInt(buffer, *(uint16_t*) formatarg->value); + break; + case FF_ARG_TYPE_UINT8: + ffStrbufAppendUInt(buffer, *(uint8_t*) formatarg->value); + break; + case FF_ARG_TYPE_STRING: + ffStrbufAppendS(buffer, (const char*) formatarg->value); + break; + case FF_ARG_TYPE_STRBUF: + ffStrbufAppend(buffer, (const FFstrbuf*) formatarg->value); + break; + case FF_ARG_TYPE_FLOAT: + ffStrbufAppendDouble(buffer, *(float*) formatarg->value, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros != FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER); + break; + case FF_ARG_TYPE_DOUBLE: + ffStrbufAppendDouble(buffer, *(double*) formatarg->value, instance.config.display.fractionNdigits, instance.config.display.fractionTrailingZeros != FF_FRACTION_TRAILING_ZEROS_TYPE_NEVER); + break; + case FF_ARG_TYPE_BOOL: + ffStrbufAppendS(buffer, *(bool*) formatarg->value ? "true" : "false"); + break; + case FF_ARG_TYPE_LIST: { + const FFlist* list = (const FFlist*) formatarg->value; + for (uint32_t i = 0; i < list->length; i++) { + ffStrbufAppend(buffer, FF_LIST_GET(FFstrbuf, *list, i)); + if (i < list->length - 1) { + ffStrbufAppendS(buffer, ", "); + } + } + break; + } + case FF_ARG_TYPE_BUFFER: { + // Placeholder for binary data, just print the size for now + const FFArgBuffer* argBuffer = (const FFArgBuffer*) formatarg->value; + ffStrbufAppendF(buffer, "buffer(%u bytes)", argBuffer->length); + break; + } + default: + if (formatarg->type != FF_ARG_TYPE_NULL) { + fprintf(stderr, "Error: format string \"%s\": argument is not implemented: %i\n", buffer->chars, formatarg->type); + } + break; + } +} + +/** + * @brief parses a string to a uint32_t + * + * If the string can't be parsed, or is < 1, uint32_t max is returned. + * + * @param placeholderValue the string to parse + * @return uint32_t the parsed value + */ +static uint32_t getArgumentIndex(const char* placeholderValue, uint32_t numArgs, const FFformatarg* arguments) { + char firstChar = placeholderValue[0]; + if (firstChar == '\0') { + return 0; // use arg counter + } + + if (firstChar >= '0' && firstChar <= '9') { + char* pEnd = NULL; + uint32_t result = (uint32_t) strtoul(placeholderValue, &pEnd, 10); + if (result > numArgs) { + return UINT32_MAX; + } + if (*pEnd != '\0') { + return UINT32_MAX; + } + return result; + } else if (ffCharIsEnglishAlphabet(firstChar)) { + for (uint32_t i = 0; i < numArgs; ++i) { + const FFformatarg* arg = &arguments[i]; + if (arg->name && ffStrEqualsIgnCase(placeholderValue, arg->name)) { + return i + 1; + } + } + } + + return UINT32_MAX; +} + +static inline void appendInvalidPlaceholder(FFstrbuf* buffer, const char* start, const FFstrbuf* placeholderValue, uint32_t index, uint32_t formatStringLength) { + ffStrbufAppendS(buffer, start); + ffStrbufAppend(buffer, placeholderValue); + + if (index < formatStringLength) { + ffStrbufAppendC(buffer, '}'); + } +} + +static inline bool formatArgSet(const FFformatarg* arg) { + return arg->value != NULL && ((arg->type == FF_ARG_TYPE_DOUBLE && *(double*) arg->value > 0.0) || (arg->type == FF_ARG_TYPE_FLOAT && *(float*) arg->value > 0.0) || (arg->type == FF_ARG_TYPE_INT && *(int32_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_STRBUF && ((FFstrbuf*) arg->value)->length > 0) || (arg->type == FF_ARG_TYPE_STRING && ffStrSet((char*) arg->value)) || (arg->type == FF_ARG_TYPE_UINT8 && *(uint8_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT16 && *(uint16_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT && *(uint32_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_UINT64 && *(uint64_t*) arg->value > 0) || (arg->type == FF_ARG_TYPE_BOOL && *(bool*) arg->value) || (arg->type == FF_ARG_TYPE_LIST && ((FFlist*) arg->value)->length > 0)); +} + +FF_A_UNUSED static inline void normalizeArgName(FFstrbuf* dst, const char* src) { + ffStrbufClear(dst); + bool flag = false; + for (const char* p = src; *p; ++p) { + if (*p == '-') { + flag = true; + } else if (flag) { + ffStrbufAppendC(dst, (char) toupper((unsigned char) *p)); + flag = false; + } else { + ffStrbufAppendC(dst, *p); + } + } +} + +#if FF_HAVE_LUA + #include "common/lua.h" + +static void appendLuaError(FFstrbuf* buffer, const char* prefix, lua_State* L) { + const char* err = lua_tolstring(L, -1, NULL); + if (err) { + const char* tmp = strchr(err, ':'); + if (tmp) { + err = tmp + 1; + while (*err == ' ') { + ++err; + } + } + } + ffStrbufAppendF(buffer, "%s: %s", prefix, err ? err : "unknown"); +} + +static bool parseLuaString(FFstrbuf* buffer, const char* script, uint32_t scriptLen, uint32_t numArgs, const FFformatarg* arguments) { + const char* err = ffLuaLoadState(); + if (err) { + ffStrbufAppendF(buffer, "Lua init error: %s", err); + return false; + } + + FF_STRBUF_AUTO_DESTROY argNameBuf = ffStrbufCreate(); + bool ret = false; + + lua_State* L = luaData.L; + // Clear stack and load chunk + lua_settop(L, 0); + if (luaL_loadbuffer(L, script, scriptLen, "") != LUA_OK) { + appendLuaError(buffer, "Lua load error", L); + } else { + // Build args table for name lookup only. + lua_createtable(L, 0, 0); + + for (uint32_t i = 0; i < numArgs; ++i) { + const FFformatarg* arg = &arguments[i]; + switch (arg->type) { + case FF_ARG_TYPE_INT: + lua_pushinteger(L, (lua_Integer) * (int32_t*) arg->value); + break; + case FF_ARG_TYPE_UINT: + lua_pushinteger(L, (lua_Integer) * (uint32_t*) arg->value); + break; + case FF_ARG_TYPE_UINT64: + lua_pushinteger(L, (lua_Integer) * (uint64_t*) arg->value); + break; + case FF_ARG_TYPE_UINT16: + lua_pushinteger(L, (lua_Integer) * (uint16_t*) arg->value); + break; + case FF_ARG_TYPE_UINT8: + lua_pushinteger(L, (lua_Integer) * (uint8_t*) arg->value); + break; + case FF_ARG_TYPE_FLOAT: + lua_pushnumber(L, (lua_Number) * (float*) arg->value); + break; + case FF_ARG_TYPE_DOUBLE: + lua_pushnumber(L, (lua_Number) * (double*) arg->value); + break; + case FF_ARG_TYPE_BOOL: + lua_pushboolean(L, *(bool*) arg->value); + break; + case FF_ARG_TYPE_STRING: + lua_pushlstring(L, (const char*) arg->value, strlen((const char*) arg->value)); + break; + case FF_ARG_TYPE_STRBUF: { + const FFstrbuf* sb = (const FFstrbuf*) arg->value; + lua_pushlstring(L, sb->chars, sb->length); + break; + } + case FF_ARG_TYPE_LIST: { + const FFlist* list = (const FFlist*) arg->value; + lua_createtable(L, 0, 0); + for (uint32_t li = 0; li < list->length; ++li) { + const FFstrbuf* item = FF_LIST_GET(FFstrbuf, *list, li); + lua_pushlstring(L, item->chars, item->length); + lua_seti(L, -2, (lua_Integer) (li + 1)); + } + break; + } + default: + lua_pushnil(L); + break; + } + if (arg->name && arg->name[0]) { + normalizeArgName(&argNameBuf, arg->name); + } else { + ffStrbufSetF(&argNameBuf, "arg%" PRIu32, i + 1); + } + lua_setfield(L, -2, argNameBuf.chars); + } + + if (lua_pcall(L, 1, LUA_MULTRET, 0) != LUA_OK) { + appendLuaError(buffer, "Lua runtime error", L); + } else { + int nresults = lua_gettop(L); + if (nresults == 0) { + ffStrbufAppendS(buffer, "Lua result error: no result"); + } else { + // Convert first result to string + const char* res = lua_tolstring(L, 1, NULL); + if (res) { + ffStrbufAppendS(buffer, res); + } else { + luaL_tolstring(L, 1, NULL); + const char* sval = lua_tolstring(L, -1, NULL); + if (sval) { + ffStrbufAppendS(buffer, sval); + } + } + ret = true; + } + } + } + lua_settop(L, 0); + return ret; +} +#endif + +#if FF_HAVE_QUICKJS + #include <quickjs.h> + +struct FFQuickJSData { + FF_LIBRARY_SYMBOL(JS_NewRuntime) + FF_LIBRARY_SYMBOL(JS_NewContext) + FF_LIBRARY_SYMBOL(JS_FreeRuntime) + FF_LIBRARY_SYMBOL(JS_EvalThis) + FF_LIBRARY_SYMBOL(JS_GetException) + FF_LIBRARY_SYMBOL(JS_ToCStringLen2) + FF_LIBRARY_SYMBOL(JS_FreeCString) + FF_LIBRARY_SYMBOL(JS_NewStringLen) + FF_LIBRARY_SYMBOL(JS_NewArray) + FF_LIBRARY_SYMBOL(JS_NewBigUint64) + FF_LIBRARY_SYMBOL(JS_SetPropertyUint32) + FF_LIBRARY_SYMBOL(JS_NewObject) + FF_LIBRARY_SYMBOL(JS_SetPropertyStr) + FF_LIBRARY_SYMBOL(JS_FreeValue) + + JSRuntime* rt; + JSContext* ctx; + bool inited; +} qjsData; + +static const char* loadQuickJSState(void) { + if (qjsData.inited) { + if (qjsData.ctx == NULL) { + return "QuickJS is not available"; + } + return NULL; + } + + qjsData.inited = true; + #ifdef _WIN32 + FF_LIBRARY_LOAD_MESSAGE(libqjs, "libqjs-0" FF_LIBRARY_EXTENSION, 0) + #else + FF_LIBRARY_LOAD_MESSAGE(libqjs, "libqjs" FF_LIBRARY_EXTENSION, 0) + #endif + + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewRuntime) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewContext) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeRuntime) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_EvalThis) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_GetException) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_ToCStringLen2) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeCString) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewStringLen) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewArray) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_SetPropertyUint32) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_NewObject) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_SetPropertyStr) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(libqjs, qjsData, JS_FreeValue) + + qjsData.rt = qjsData.ffJS_NewRuntime(); + if (qjsData.rt == NULL) { + return "JS_NewRuntime() failed"; + } + + qjsData.ctx = qjsData.ffJS_NewContext(qjsData.rt); + if (qjsData.ctx == NULL) { + qjsData.ffJS_FreeRuntime(qjsData.rt); + qjsData.rt = NULL; + return "JS_NewContext() failed"; + } + + libqjs = NULL; // don't close quickjs + + return NULL; +} + +static bool parseQuickJSString(FFstrbuf* buffer, const char* script, uint32_t scriptLen, uint32_t numArgs, const FFformatarg* arguments) { + const char* err = loadQuickJSState(); + if (err) { + ffStrbufAppendF(buffer, "Qjs init error: %s", err); + return false; + } + JSContext* ctx = qjsData.ctx; + JSValue argsObj = qjsData.ffJS_NewObject(ctx); + FF_STRBUF_AUTO_DESTROY argNameBuf = ffStrbufCreate(); + + for (uint32_t i = 0; i < numArgs; ++i) { + const FFformatarg* arg = &arguments[i]; + + JSValue value; + switch (arg->type) { + case FF_ARG_TYPE_INT: + value = JS_NewInt32(ctx, *(int32_t*) arg->value); + break; + case FF_ARG_TYPE_UINT: + value = JS_NewUint32(ctx, *(uint32_t*) arg->value); + break; + case FF_ARG_TYPE_UINT64: { + uint64_t val = *(uint64_t*) arg->value; + if (val <= INT32_MAX) { + value = JS_NewInt32(ctx, (int32_t) val); + } else { + value = JS_NewFloat64(ctx, (double) val); + } + break; + } + case FF_ARG_TYPE_UINT16: + value = JS_NewUint32(ctx, *(uint16_t*) arg->value); + break; + case FF_ARG_TYPE_UINT8: + value = JS_NewUint32(ctx, *(uint8_t*) arg->value); + break; + case FF_ARG_TYPE_FLOAT: + value = JS_NewFloat64(ctx, *(float*) arg->value); + break; + case FF_ARG_TYPE_DOUBLE: + value = JS_NewFloat64(ctx, *(double*) arg->value); + break; + case FF_ARG_TYPE_BOOL: + value = JS_NewBool(ctx, *(bool*) arg->value); + break; + case FF_ARG_TYPE_STRING: + value = qjsData.ffJS_NewStringLen(ctx, (const char*) arg->value, strlen((const char*) arg->value)); + break; + case FF_ARG_TYPE_STRBUF: { + const FFstrbuf* sb = (const FFstrbuf*) arg->value; + value = qjsData.ffJS_NewStringLen(ctx, sb->chars, sb->length); + break; + } + case FF_ARG_TYPE_LIST: { + const FFlist* list = (const FFlist*) arg->value; + JSValue arr = qjsData.ffJS_NewArray(ctx); + for (uint32_t li = 0; li < list->length; ++li) { + const FFstrbuf* item = FF_LIST_GET(FFstrbuf, *list, li); + JSValue itemValue = qjsData.ffJS_NewStringLen(ctx, item->chars, item->length); + qjsData.ffJS_SetPropertyUint32(ctx, arr, li, itemValue); + } + + value = arr; + break; + } + default: + value = JS_UNDEFINED; + break; + } + + if (arg->name && arg->name[0]) { + normalizeArgName(&argNameBuf, arg->name); + } else { + ffStrbufSetF(&argNameBuf, "arg%" PRIu32, i + 1); + } + qjsData.ffJS_SetPropertyStr(ctx, argsObj, argNameBuf.chars, value); + } + JSValue result = qjsData.ffJS_EvalThis(ctx, argsObj, script, scriptLen, "", JS_EVAL_TYPE_GLOBAL | JS_EVAL_FLAG_STRICT); + + qjsData.ffJS_FreeValue(ctx, argsObj); + + bool ret = false; + if (JS_IsException(result)) { + JSValue exc = qjsData.ffJS_GetException(ctx); + const char* message = qjsData.ffJS_ToCStringLen2(ctx, NULL, exc, false); + qjsData.ffJS_FreeValue(ctx, exc); + ffStrbufAppendF(buffer, "Qjs runtime error: %s", message ?: "unknown"); + if (message) { + qjsData.ffJS_FreeCString(ctx, message); + } + } else if (JS_IsUndefined(result)) { + ffStrbufAppendS(buffer, "Qjs result error: undefined result"); + } else { + size_t len; + const char* res = qjsData.ffJS_ToCStringLen2(ctx, &len, result, false); + if (res) { + ffStrbufAppendNS(buffer, (uint32_t) len, res); + qjsData.ffJS_FreeCString(ctx, res); + } + ret = true; + } + + qjsData.ffJS_FreeValue(ctx, result); + return ret; +} +#endif + +static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) { + if (__builtin_expect(in->chars[0] == '\e' && in->chars[1] == '[', false)) { + // skip ANSI escape codes at the start of the string for truncation + const char* p = in->chars + 2; + while (*p && (ffCharIsDigit(*p) || *p == ';')) { + ++p; + } + if (*p && isascii(*p)) { + ++p; + } + uint32_t prefixLen = (uint32_t) (p - in->chars); + ffStrbufAppendNS(out, prefixLen, in->chars); + ffStrbufSubstrAfter(in, prefixLen - 1); + + if (trailingEscape) { + // likely have a `CSI m` reset at the end of the string + uint32_t iLastEscape = ffStrbufLastIndexC(in, '\e'); + if (iLastEscape != in->length) { + ffStrbufSetNS(trailingEscape, in->length - iLastEscape, in->chars + iLastEscape); + ffStrbufSubstrBefore(in, iLastEscape); + } + } + return true; + } + return false; +} + +static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) { + uint32_t argCounter = 0; + + uint32_t numOpenIfs = 0; + uint32_t numOpenNotIfs = 0; + + FF_STRBUF_AUTO_DESTROY placeholderValue = ffStrbufCreate(); + + for (uint32_t i = 0; i < formatstr->length; ++i) { + // if we don't have a placeholder start just copy the chars over to output buffer + if (formatstr->chars[i] != '{') { + ffStrbufAppendC(buffer, formatstr->chars[i]); + continue; + } + + // jump to next char, the start of the placeholder value + ++i; + + // unmatched trailing '{' + if (i >= formatstr->length) { + ffStrbufAppendC(buffer, '{'); + break; + } + + // double {{ elvaluates to a single { and doesn't count as start + if (formatstr->chars[i] == '{') { + ffStrbufAppendC(buffer, '{'); + continue; + } + + ffStrbufClear(&placeholderValue); + + { + uint32_t iEnd = ffStrbufNextIndexC(formatstr, i, '}'); + ffStrbufAppendNS(&placeholderValue, iEnd - i, &formatstr->chars[i]); + i = iEnd; + } + + char firstChar = placeholderValue.chars[0]; + + if (placeholderValue.length == 1) { + // test if for stop, if so break the loop + if (firstChar == '-') { + break; + } + + // test for end of an if, if so do nothing + if (firstChar == '?') { + if (numOpenIfs == 0) { + appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); + } else { + --numOpenIfs; + } + + continue; + } + + // test for end of a not if, if so do nothing + if (firstChar == '/') { + if (numOpenNotIfs == 0) { + appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); + } else { + --numOpenNotIfs; + } + + continue; + } + + // test for end of a color, if so do nothing + if (firstChar == '#') { + if (!instance.config.display.pipe) { + ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET); + } + + continue; + } + } + + // test for if, if so evaluate it + if (firstChar == '?') { + ffStrbufSubstrAfter(&placeholderValue, 0); + + uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments); + + // testing for an invalid index + if (index > numArgs || index < 1) { + appendInvalidPlaceholder(buffer, "{?", &placeholderValue, i, formatstr->length); + continue; + } + + // continue normally if an format arg is set and the value is > 0 + if (formatArgSet(&arguments[index - 1])) { + ++numOpenIfs; + continue; + } + + // fastforward to the end of the if without printing the in between + i = ffStrbufNextIndexS(formatstr, i, "{?}") + 2; // 2 is the length of "{?}" - 1 because the loop will increment it again directly after continue + continue; + } + + // test for not if, if so evaluate it + if (firstChar == '/') { + ffStrbufSubstrAfter(&placeholderValue, 0); + + uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments); + + // testing for an invalid index + if (index > numArgs || index < 1) { + appendInvalidPlaceholder(buffer, "{/", &placeholderValue, i, formatstr->length); + continue; + } + + // continue normally if an format arg is not set or the value is 0 + if (!formatArgSet(&arguments[index - 1])) { + ++numOpenNotIfs; + continue; + } + + // fastforward to the end of the if without printing the in between + i = ffStrbufNextIndexS(formatstr, i, "{/}") + 2; // 2 is the length of "{/}" - 1 because the loop will increment it again directly after continue + continue; + } + + // test for color, if so evaluate it + if (firstChar == '#') { + if (!instance.config.display.pipe) { + ffStrbufAppendS(buffer, "\e["); + ffOptionParseColorNoClear(placeholderValue.chars + 1, buffer); + ffStrbufAppendC(buffer, 'm'); + } + continue; + } + + // test for constant or env var, if so evaluate it + if (firstChar == '$') { + char* pend = NULL; + int32_t indexSigned = (int32_t) strtol(placeholderValue.chars + 1, &pend, 10); + if (pend == placeholderValue.chars + 1) { + // treat placeholder as an environment variable + char* envValue = getenv(placeholderValue.chars + 1); + if (envValue) { + ffStrbufAppendS(buffer, envValue); + } else { + appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); + } + } else { + // treat placeholder as a constant + uint32_t index = (uint32_t) (indexSigned < 0 ? (int32_t) instance.config.display.constants.length + indexSigned : indexSigned - 1); + + if (*pend != '\0' || instance.config.display.constants.length <= index) { + appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); + } else { + FFstrbuf* item = FF_LIST_GET(FFstrbuf, instance.config.display.constants, index); + ffStrbufAppend(buffer, item); + } + } + continue; + } + + char* pSep = placeholderValue.chars; + char cSep = '\0'; + while (*pSep && *pSep != ':' && *pSep != '<' && *pSep != '>' && *pSep != '|' && *pSep != '~') { + ++pSep; + } + if (*pSep) { + cSep = *pSep; + *pSep = '\0'; + } else { + pSep = NULL; + } + + uint32_t index = getArgumentIndex(placeholderValue.chars, numArgs, arguments); + + // test for invalid index + if (index == 0) { + index = ++argCounter; + } + + if (index > numArgs) { + if (pSep) { + *pSep = cSep; + } + appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); + continue; + } + + if (!cSep) { + ffFormatAppendFormatArg(buffer, &arguments[index - 1]); + } else if (cSep == '~') { + FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate(); + ffFormatAppendFormatArg(&tempString, &arguments[index - 1]); + FF_STRBUF_AUTO_DESTROY trailingEscape = ffStrbufCreate(); + skipAnsiEscape(&tempString, buffer, &trailingEscape); + + char* pEnd = NULL; + int32_t start = (int32_t) strtol(pSep + 1, &pEnd, 10); + if (start < 0) { + start = (int32_t) tempString.length + start; + } + if (start >= 0 && (uint32_t) start < tempString.length) { + if (*pEnd == '\0') { + ffStrbufAppendNS(buffer, tempString.length - (uint32_t) start, &tempString.chars[start]); + } else if (*pEnd == ',') { + int32_t end = (int32_t) strtol(pEnd + 1, &pEnd, 10); + if (!*pEnd) { + if (end < 0) { + end = (int32_t) tempString.length + end; + } + if ((uint32_t) end > tempString.length) { + end = (int32_t) tempString.length; + } + if (end > start) { + ffStrbufAppendNS(buffer, (uint32_t) (end - start), &tempString.chars[start]); + } + } + } + } + + if (trailingEscape.length > 0) { + ffStrbufAppend(buffer, &trailingEscape); + } + + if (*pEnd) { + *pSep = cSep; + appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); + continue; + } + } else { + char* pEnd = NULL; + int32_t truncLength = (int32_t) strtol(pSep + 1, &pEnd, 10); + if (*pEnd != '\0') { + *pSep = cSep; + appendInvalidPlaceholder(buffer, "{", &placeholderValue, i, formatstr->length); + continue; + } + + bool ellipsis = false; + if (truncLength < 0) { + ellipsis = true; + truncLength = -truncLength; + } + + FF_STRBUF_AUTO_DESTROY tempString = ffStrbufCreate(); + ffFormatAppendFormatArg(&tempString, &arguments[index - 1]); + FF_STRBUF_AUTO_DESTROY trailingEscape = ffStrbufCreate(); + skipAnsiEscape(&tempString, buffer, &trailingEscape); + + if (tempString.length == (uint32_t) truncLength) { + ffStrbufAppend(buffer, &tempString); + } else if (tempString.length > (uint32_t) truncLength) { + if (cSep == ':') { + ffStrbufSubstrBefore(&tempString, (uint32_t) truncLength); + ffStrbufTrimRightSpace(&tempString); + } else { + ffStrbufSubstrBefore(&tempString, (uint32_t) (!ellipsis ? truncLength : truncLength - 1)); + } + ffStrbufAppend(buffer, &tempString); + + if (ellipsis) { + ffStrbufAppendS(buffer, "…"); + } + } else if (cSep == ':') { + ffStrbufAppend(buffer, &tempString); + } else { + if (cSep == '<') { // left align + ffStrbufAppend(buffer, &tempString); + ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length, ' '); + } else if (cSep == '>') { // right align + ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length, ' '); + ffStrbufAppend(buffer, &tempString); + } else if (cSep == '|') { // center align + uint32_t padding = ((uint32_t) truncLength - tempString.length) / 2; + ffStrbufAppendNC(buffer, padding, ' '); + ffStrbufAppend(buffer, &tempString); + ffStrbufAppendNC(buffer, (uint32_t) truncLength - tempString.length - padding, ' '); + } + } + + if (trailingEscape.length > 0) { + ffStrbufAppend(buffer, &trailingEscape); + } + } + } + + if (!instance.config.display.pipe) { + ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET); + } + + return true; +} + +bool ffParseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) { +#if FF_HAVE_QUICKJS + if (ffStrbufStartsWithS(formatstr, "qjs:")) { + return parseQuickJSString(buffer, formatstr->chars + 4, formatstr->length - 4, numArgs, arguments); + } +#endif + +#if FF_HAVE_LUA + if (ffStrbufStartsWithS(formatstr, "lua:")) { + // If outputFormat starts with "lua:", treat the rest as a Lua script + return parseLuaString(buffer, formatstr->chars + 4, formatstr->length - 4, numArgs, arguments); + } +#endif + + return parseFormatString(buffer, formatstr, numArgs, arguments); +} diff --git a/src/common/impl/frequency.c b/src/common/impl/frequency.c new file mode 100644 index 0000000..4838df8 --- /dev/null +++ b/src/common/impl/frequency.c @@ -0,0 +1,26 @@ +#include "common/frequency.h" + +bool ffFreqAppendNum(uint32_t mhz, FFstrbuf* result) { + if (mhz == 0) { + return false; + } + + const FFOptionsDisplay* options = &instance.config.display; + bool spaceBeforeUnit = options->freqSpaceBeforeUnit != FF_SPACE_BEFORE_UNIT_NEVER; + int8_t ndigits = options->freqNdigits; + + if (ndigits >= 0) { + ffStrbufAppendDouble(result, mhz / 1000., ndigits, true); + if (spaceBeforeUnit) { + ffStrbufAppendC(result, ' '); + } + ffStrbufAppendS(result, "GHz"); + } else { + ffStrbufAppendUInt(result, mhz); + if (spaceBeforeUnit) { + ffStrbufAppendC(result, ' '); + } + ffStrbufAppendS(result, "MHz"); + } + return true; +} diff --git a/src/common/impl/init.c b/src/common/impl/init.c new file mode 100644 index 0000000..f0fc0a2 --- /dev/null +++ b/src/common/impl/init.c @@ -0,0 +1,300 @@ +#include "fastfetch.h" +#include "common/init.h" +#include "common/parsing.h" +#include "common/thread.h" +#include "common/textModifier.h" +#include "detection/displayserver/displayserver.h" +#include "detection/terminaltheme/terminaltheme.h" +#include "logo/logo.h" + +#include <stdlib.h> +#include <unistd.h> +#include <locale.h> +#ifdef _WIN32 + #include <windows.h> + #include "common/windows/unicode.h" +#else + #include <signal.h> +#endif + +FFinstance instance; // Global singleton + +static void initState(FFstate* state) { + state->logoWidth = 0; + state->logoHeight = 0; + state->keysHeight = 0; + state->terminalLightTheme = false; + state->titleFqdn = false; + + ffPlatformInit(&state->platform); + state->dynamicInterval = 0; + + #if !FF_MODULE_DISABLE_TERMINALTHEME + { + // don't enable bright color if the terminal is in light mode + FFTerminalThemeResult result; + if (ffDetectTerminalTheme(&result, true /* forceEnv for performance */) && !result.bg.dark) { + state->terminalLightTheme = true; + } + } + #endif +} + +static void defaultConfig(void) { + ffOptionsInitLogo(&instance.config.logo); + ffOptionsInitGeneral(&instance.config.general); + ffOptionsInitDisplay(&instance.config.display); +} + +void ffInitInstance(void) { +#ifdef _WIN32 + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/setlocale-wsetlocale?source=recommendat> + setlocale(LC_ALL, ".UTF8"); +#else + // Never use `setlocale(LC_ALL, "")` + setlocale(LC_TIME, ""); +#endif + + defaultConfig(); + initState(&instance.state); +} + +static volatile bool ffDisableLinewrap = false; +static volatile bool ffHideCursor = false; +#ifdef _WIN32 +static volatile UINT oldCp = CP_UTF8; +#endif + +static void resetConsole(void) { + if (ffDisableLinewrap) { + fputs("\033[?7h", stdout); + } + + if (ffHideCursor) { + fputs("\033[?25h", stdout); + } + + if (instance.state.dynamicInterval > 0) { + fputs("\033[?1049l", stdout); // Disable alternate buffer + } + +#if defined(_WIN32) + fflush(stdout); + + if (oldCp != CP_UTF8) { + SetConsoleOutputCP(oldCp); + } +#endif +} + +#ifdef _WIN32 +BOOL WINAPI consoleHandler(FF_A_UNUSED DWORD signal) { + resetConsole(); + exit(0); +} +#else +static void exitSignalHandler(FF_A_UNUSED int signal) { + resetConsole(); + exit(0); +} +#endif + +void ffStart(void) { + ffDisableLinewrap = instance.config.display.disableLinewrap && !instance.config.display.pipe; + ffHideCursor = instance.config.display.hideCursor && !instance.config.display.pipe; + +#ifdef _WIN32 + SetErrorMode(SEM_FAILCRITICALERRORS); + if (instance.config.display.noBuffer) { + setvbuf(stdout, NULL, _IONBF, 0); + } else { + setvbuf(stdout, NULL, _IOFBF, 4096); + } + SetConsoleCtrlHandler(consoleHandler, TRUE); + HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD mode = 0; + if (GetConsoleMode(hStdout, &mode)) { + SetConsoleMode(hStdout, mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + oldCp = GetConsoleOutputCP(); + if (oldCp != CP_UTF8) { + SetConsoleOutputCP(CP_UTF8); + } + } +#else + if (instance.config.display.noBuffer) { + setvbuf(stdout, NULL, _IONBF, 0); + } + struct sigaction action; + sigemptyset(&action.sa_mask); + action.sa_flags = 0; + action.sa_handler = exitSignalHandler; + sigaction(SIGINT, &action, NULL); + sigaction(SIGTERM, &action, NULL); + sigaction(SIGQUIT, &action, NULL); + sigset_t newmask; + sigemptyset(&newmask); + sigaddset(&newmask, SIGCHLD); + sigprocmask(SIG_BLOCK, &newmask, NULL); +#endif + + // reset everything to default before we start printing + if (!instance.config.display.pipe) { + fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout); + } + + if (ffHideCursor) { + fputs("\033[?25l", stdout); + } + + if (ffDisableLinewrap) { + fputs("\033[?7l", stdout); + } + + if (instance.state.dynamicInterval > 0) { + fputs("\033[?1049h\033[H", stdout); // Enable alternate buffer + fflush(stdout); + } +} + +void ffFinish(void) { + resetConsole(); +} + +static void destroyConfig(void) { + ffOptionsDestroyLogo(&instance.config.logo); + ffOptionsDestroyGeneral(&instance.config.general); + ffOptionsDestroyDisplay(&instance.config.display); +} + +static void destroyState(void) { + ffPlatformDestroy(&instance.state.platform); +} + +void ffDestroyInstance(void) { + destroyConfig(); + destroyState(); +} + +#if FF_HAVE_LUA + #include <lua.h> +#endif +#if FF_HAVE_QUICKJS + #include <quickjs.h> + #define FF_STR_INDIR(x) #x + #define FF_STR(x) FF_STR_INDIR(x) +#endif + +// Must be in a file compiled with the libfastfetch target, because the FF_HAVE* macros are not defined for the executable targets +void ffListFeatures(void) { + fputs( +#if FF_HAVE_THREADS + "threads\n" +#endif +#if FF_HAVE_VULKAN + "vulkan\n" +#endif +#if FF_HAVE_WAYLAND + "wayland\n" +#endif +#if FF_HAVE_XCB_RANDR + "xcb-randr\n" +#endif +#if FF_HAVE_XRANDR + "xrandr\n" +#endif +#if FF_HAVE_DRM + "drm\n" +#endif +#if FF_HAVE_DRM_AMDGPU + "drm_amdgpu\n" +#endif +#if FF_HAVE_GIO + "gio\n" +#endif +#if FF_HAVE_DCONF + "dconf\n" +#endif +#if FF_HAVE_EET + "eet\n" +#endif +#if FF_HAVE_DBUS + "dbus\n" +#endif +#if FF_HAVE_IMAGEMAGICK7 + "imagemagick7\n" +#endif +#if FF_HAVE_IMAGEMAGICK6 + "imagemagick6\n" +#endif +#if FF_HAVE_CHAFA + "chafa\n" +#endif +#if FF_HAVE_ZLIB + "zlib\n" +#endif +#if FF_HAVE_SQLITE3 + "sqlite3\n" +#endif +#if FF_HAVE_RPM + "rpm\n" +#endif +#if FF_HAVE_EGL + "egl\n" +#endif +#if FF_HAVE_GLX + "glx\n" +#endif +#if FF_HAVE_OPENCL + "opencl\n" +#endif +#if FF_HAVE_FREETYPE + "freetype\n" +#endif +#if FF_HAVE_PULSE + "libpulse\n" +#endif +#if FF_HAVE_DDCUTIL + "libddcutil\n" +#endif +#if FF_HAVE_ELF || __sun || (__FreeBSD__ && !__DragonFly__) || __OpenBSD__ || __NetBSD__ + "libelf\n" +#endif +#if FF_HAVE_LIBZFS + "libzfs\n" +#endif +#if FF_HAVE_VA + "va\n" +#endif +#if FF_HAVE_VDPAU + "vdpau\n" +#endif +#if FF_USE_SYSTEM_YYJSON + "System yyjson\n" +#endif +#if FF_HAVE_LINUX_VIDEODEV2 + "linux/videodev2\n" +#endif +#if FF_HAVE_EMBEDDED_PCIIDS + "Embedded pciids\n" +#endif +#if FF_ENABLE_WCWIDTH + "Embedded wcwidth\n" +#endif +#if FF_HAVE_WINRT + "WinRT headers\n" +#endif +#if FF_WIN81_COMPAT + "Windows 8.1 Compatibility\n" +#endif +#if FF_APPLE_MEMSIZE_USABLE + "Apple memsize_usable\n" +#endif +#if FF_HAVE_LUA + LUA_VERSION "\n" +#endif +#if FF_HAVE_QUICKJS + "QuickJS " FF_STR(QJS_VERSION_MAJOR) "." FF_STR(QJS_VERSION_MINOR) "." FF_STR(QJS_VERSION_PATCH) QJS_VERSION_SUFFIX "\n" +#endif + "", + stdout); +} diff --git a/src/common/impl/io_unix.c b/src/common/impl/io_unix.c new file mode 100644 index 0000000..e603186 --- /dev/null +++ b/src/common/impl/io_unix.c @@ -0,0 +1,333 @@ +#include "common/io.h" +#include "fastfetch.h" +#include "common/strutil.h" +#include "common/time.h" + +#include <fcntl.h> +#include <termios.h> +#include <dirent.h> +#include <errno.h> +#ifndef __APPLE__ + #include <poll.h> +#else + #include <sys/select.h> +#endif + +#if FF_HAVE_WORDEXP + #include <wordexp.h> +#else + #include <glob.h> +#endif + +static void createSubfolders(const char* fileName) { + FF_STRBUF_AUTO_DESTROY path = ffStrbufCreate(); + + const char* token = NULL; + while ((token = strchr(fileName, '/')) != NULL) { + ffStrbufAppendNS(&path, (uint32_t) (token - fileName + 1), fileName); + mkdir(path.chars, S_IRWXU | S_IRGRP | S_IROTH); + fileName = token + 1; + } +} + +bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) { + int openFlagsModes = O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC; + mode_t openFlagsRights = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; + + int FF_AUTO_CLOSE_FD fd = open(fileName, openFlagsModes, openFlagsRights); + if (fd == -1) { + if (errno == ENOENT) { + createSubfolders(fileName); + fd = open(fileName, openFlagsModes, openFlagsRights); + if (fd == -1) { + return false; + } + } else { + return false; + } + } + + return write(fd, data, dataSize) > 0; +} + +static inline void readWithLength(int fd, FFstrbuf* buffer, uint32_t length) { + ffStrbufEnsureFixedLengthFree(buffer, length); + ssize_t bytesRead = 0; + while ( + length > 0 && (bytesRead = read(fd, buffer->chars + buffer->length, length)) > 0) { + buffer->length += (uint32_t) bytesRead; + length -= (uint32_t) bytesRead; + } +} + +static inline void readUntilEOF(int fd, FFstrbuf* buffer) { + ffStrbufEnsureFree(buffer, 31); + uint32_t available = ffStrbufGetFree(buffer); + ssize_t bytesRead = 0; + while ( + (bytesRead = read(fd, buffer->chars + buffer->length, available)) > 0) { + buffer->length += (uint32_t) bytesRead; + if ((uint32_t) bytesRead == available) { + ffStrbufEnsureFree(buffer, buffer->allocated - 1); // Doubles capacity every round. -1 for the null byte. + } + available = ffStrbufGetFree(buffer); + } +} + +bool ffAppendFDBuffer(int fd, FFstrbuf* buffer) { + struct stat fileInfo; + if (fstat(fd, &fileInfo) != 0) { + return false; + } + + if (fileInfo.st_size > 0) { + readWithLength(fd, buffer, (uint32_t) fileInfo.st_size); + } else { + readUntilEOF(fd, buffer); + } + + buffer->chars[buffer->length] = '\0'; + + return buffer->length > 0; +} + +bool ffPathExpandEnv(const char* in, FFstrbuf* out) { + bool result = false; + +#if FF_HAVE_WORDEXP + + wordexp_t exp; + if (wordexp(in, &exp, 0) != 0) { // WARN: 0 = no safety flags; command substitution allowed + return false; + } + + if (exp.we_wordc >= 1) { + result = true; + ffStrbufSetS(out, exp.we_wordv[exp.we_wordc > 1 ? ffTimeGetNow() % exp.we_wordc : 0]); + } + + wordfree(&exp); + +#else + + glob_t gb; + if (glob(in, GLOB_NOSORT + #ifdef GLOB_TILDE + | GLOB_TILDE + #endif + #ifdef GLOB_BRACE + | GLOB_BRACE + #endif + , + NULL, + &gb) != 0) + return false; + + if (gb.gl_pathc >= 1) { + result = true; + ffStrbufSetS(out, gb.gl_pathv[gb.gl_pathc > 1 ? ffTimeGetNow() % (unsigned) gb.gl_pathc : 0]); + } + + globfree(&gb); + +#endif + + return result; +} + +static int ftty = -1; +static struct termios oldTerm; +void restoreTerm(void) { + tcsetattr(ftty, TCSAFLUSH, &oldTerm); +} + +const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) { + if (ftty < 0) { + ftty = open("/dev/tty", O_RDWR | O_NOCTTY | O_CLOEXEC); + if (ftty < 0) { + return "open(\"/dev/tty\", O_RDWR | O_NOCTTY | O_CLOEXEC) failed"; + } + + if (tcgetattr(ftty, &oldTerm) == -1) { + return "tcgetattr(STDIN_FILENO, &oldTerm) failed"; + } + + struct termios newTerm = oldTerm; + newTerm.c_lflag &= (tcflag_t) ~(ICANON | ECHO); + if (tcsetattr(ftty, TCSAFLUSH, &newTerm) == -1) { + return "tcsetattr(STDIN_FILENO, TCSAFLUSH, &newTerm)"; + } + atexit(restoreTerm); + } + + ffWriteFDData(ftty, strlen(request), request); + +// Give the terminal some time to respond +#ifndef __APPLE__ + if (poll(&(struct pollfd) { .fd = ftty, .events = POLLIN }, 1, FF_IO_TERM_RESP_WAIT_MS) <= 0) { + return "poll(/dev/tty) timeout or failed"; + } +#else + { + // On macOS, poll(/dev/tty) always returns immediately + // See also https://nathancraddock.com/blog/macos-dev-tty-polling/ + fd_set rd; + FD_ZERO(&rd); + FD_SET(ftty, &rd); + if (select(ftty + 1, &rd, NULL, NULL, &(struct timeval) { .tv_sec = FF_IO_TERM_RESP_WAIT_MS / 1000, .tv_usec = (FF_IO_TERM_RESP_WAIT_MS % 1000) * 1000 }) <= 0) { + return "select(/dev/tty) timeout or failed"; + } + } +#endif + + char buffer[1024]; + size_t bytesRead = 0; + + va_list args; + va_start(args, format); + + while (true) { + ssize_t nRead = read(ftty, buffer + bytesRead, sizeof(buffer) - bytesRead - 1); + + if (nRead <= 0) { + va_end(args); + return "read(STDIN_FILENO, buffer, sizeof(buffer) - 1) failed"; + } + + bytesRead += (size_t) nRead; + buffer[bytesRead] = '\0'; + + va_list cargs; + va_copy(cargs, args); + int ret = vsscanf(buffer, format, cargs); + va_end(cargs); + + if (ret <= 0) { + va_end(args); + return "vsscanf(buffer, format, args) failed"; + } + if (ret >= nParams) { + break; + } + } + + va_end(args); + + return NULL; +} + +bool ffSuppressIO(bool suppress) { +#ifndef NDEBUG + if (instance.config.display.debugMode) { + return false; + } +#endif + + static bool init = false; + static int origOut = -1; + static int origErr = -1; + static int nullFile = -1; + + if (!init) { + if (!suppress) { + return true; + } + + origOut = dup(STDOUT_FILENO); + origErr = dup(STDERR_FILENO); + nullFile = open("/dev/null", O_WRONLY | O_CLOEXEC); + init = true; + } + + if (nullFile == -1) { + return false; + } + + fflush(stdout); + fflush(stderr); + + dup2(suppress ? nullFile : origOut, STDOUT_FILENO); + dup2(suppress ? nullFile : origErr, STDERR_FILENO); + return true; +} + +void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indentation, const char* folderName, bool pretty) { + int dfd = open(folder->chars, O_RDONLY | O_CLOEXEC | O_DIRECTORY); // Ownership of dfd will be transformed to dir + if (dfd < 0) { + return; + } + + FF_AUTO_CLOSE_DIR DIR* dir = fdopendir(dfd); + if (dir == NULL) { + close(dfd); + return; // Should not happen + } + + uint32_t folderLength = folder->length; + + if (pretty && folderName != NULL) { + for (uint8_t i = 0; i < indentation - 1; i++) { + fputs(" | ", stdout); + } + printf("%s/\n", folderName); + } + + struct dirent* entry; + + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') { // skip hidden files + continue; + } + + bool isDir = false; +#if !defined(__sun) && !defined(__HAIKU__) + if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK) { + isDir = entry->d_type == DT_DIR; + } else +#endif + { + struct stat stbuf; + if (fstatat(dfd, entry->d_name, &stbuf, 0) < 0) { + isDir = false; + } else { + isDir = S_ISDIR(stbuf.st_mode); + } + } + if (isDir) { + ffStrbufAppendS(folder, entry->d_name); + ffStrbufAppendC(folder, '/'); + listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry->d_name, pretty); + ffStrbufSubstrBefore(folder, folderLength); + continue; + } + + if (pretty) { + for (uint8_t i = 0; i < indentation; i++) { + fputs(" | ", stdout); + } + } else { + fputs(folder->chars + baseLength, stdout); + } + + puts(entry->d_name); + } +} + +void ffListFilesRecursively(const char* path, bool pretty) { + FF_STRBUF_AUTO_DESTROY folder = ffStrbufCreateS(path); + ffStrbufEnsureEndsWithC(&folder, '/'); + listFilesRecursively(folder.length, &folder, 0, NULL, pretty); +} + +FFNativeFD ffGetNullFD(void) { + static FFNativeFD hNullFile = -1; + if (hNullFile != -1) { + return hNullFile; + } + hNullFile = open("/dev/null", O_WRONLY | O_CLOEXEC); + return hNullFile; +} + +bool ffRemoveFile(const char* fileName) { + return unlink(fileName) == 0; +} diff --git a/src/common/impl/io_windows.c b/src/common/impl/io_windows.c new file mode 100644 index 0000000..24b8d05 --- /dev/null +++ b/src/common/impl/io_windows.c @@ -0,0 +1,501 @@ +#include "fastfetch.h" +#include "common/io.h" +#include "common/strutil.h" +#include "common/windows/nt.h" +#include "common/windows/unicode.h" + +#include <windows.h> + +static bool createSubfolders(wchar_t* fileName) { + HANDLE hRoot = ffGetPeb()->ProcessParameters->CurrentDirectory.Handle; + bool closeRoot = false; + wchar_t* ptr = fileName; + + // Absolute drive path: C:\... + if (ffCharIsEnglishAlphabet((char) ptr[0]) && ptr[1] == L':' && ptr[2] == L'\\') { + wchar_t saved = ptr[3]; + ptr[3] = L'\0'; + + hRoot = CreateFileW( + fileName, + FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + ptr[3] = saved; + if (hRoot == INVALID_HANDLE_VALUE) { + return false; + } + + closeRoot = true; + ptr += 3; // skip "C:\" + } + // UNC path: \\server\share\... + else if (ptr[0] == L'\\' && ptr[1] == L'\\') { + wchar_t* serverEnd = wcschr(ptr + 2, L'\\'); + if (serverEnd == NULL) { + return false; + } + + wchar_t* shareEnd = wcschr(serverEnd + 1, L'\\'); + if (shareEnd == NULL) { + return true; // no parent subfolder exists before file name + } + + wchar_t saved = *shareEnd; + *shareEnd = L'\0'; + + hRoot = CreateFileW( + fileName, + FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + *shareEnd = saved; + if (hRoot == INVALID_HANDLE_VALUE) { + return false; + } + + closeRoot = true; + ptr = shareEnd + 1; // first component under share + } + // Rooted path on current drive: \foo\bar + else if (ptr[0] == L'\\') { + UNICODE_STRING* dosPath = &ffGetPeb()->ProcessParameters->CurrentDirectory.DosPath; + wchar_t driveRoot[] = { dosPath->Buffer[0], L':', L'\\', L'\0' }; + hRoot = CreateFileW( + driveRoot, + FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_FLAG_BACKUP_SEMANTICS, + NULL); + if (hRoot == INVALID_HANDLE_VALUE) { + return false; + } + closeRoot = true; + ptr++; // skip leading '\' + } + + while (true) { + wchar_t* token = wcschr(ptr, L'\\'); + if (token == NULL) { + break; + } + + // Skip empty path segments caused by duplicated '\' + if (token == ptr) { + ptr = token + 1; + continue; + } + + HANDLE hNew = INVALID_HANDLE_VALUE; + IO_STATUS_BLOCK iosb = {}; + + NTSTATUS status = NtCreateFile( + &hNew, + FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE, + &(OBJECT_ATTRIBUTES) { + .Length = sizeof(OBJECT_ATTRIBUTES), + .RootDirectory = hRoot, + .ObjectName = &(UNICODE_STRING) { + .Buffer = ptr, + .Length = (USHORT) ((USHORT) (token - ptr) * sizeof(wchar_t)), + .MaximumLength = (USHORT) ((USHORT) (token - ptr) * sizeof(wchar_t)), + }, + .Attributes = OBJ_CASE_INSENSITIVE, + }, + &iosb, + NULL, + FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN_IF, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, + NULL, + 0); + + if (!NT_SUCCESS(status)) { + if (closeRoot && hRoot != INVALID_HANDLE_VALUE) { + NtClose(hRoot); + } + return false; + } + + if (closeRoot && hRoot != INVALID_HANDLE_VALUE) { + NtClose(hRoot); + } + hRoot = hNew; + closeRoot = true; + + ptr = token + 1; + } + + if (closeRoot && hRoot != INVALID_HANDLE_VALUE) { + NtClose(hRoot); + } + + return true; +} + +bool ffWriteFileData(const char* fileName, size_t dataSize, const void* data) { + wchar_t fileNameW[MAX_PATH]; + ULONG len = 0; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) { + return false; + } + + for (ULONG i = 0; i < len / sizeof(wchar_t); ++i) { + if (fileNameW[i] == L'/') { + fileNameW[i] = L'\\'; + } + } + + HANDLE FF_AUTO_CLOSE_FD handle = CreateFileW(fileNameW, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (handle == INVALID_HANDLE_VALUE) { + if (GetLastError() == ERROR_PATH_NOT_FOUND) { + if (!createSubfolders(fileNameW)) { + return false; + } + handle = CreateFileW(fileNameW, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (handle == INVALID_HANDLE_VALUE) { + return false; + } + } else { + return false; + } + } + + DWORD written; + return !!WriteFile(handle, data, (DWORD) dataSize, &written, NULL); +} + +static inline void readWithLength(HANDLE handle, FFstrbuf* buffer, uint32_t length) { + ffStrbufEnsureFree(buffer, length); + DWORD bytesRead = 0; + while ( + length > 0 && + ReadFile(handle, buffer->chars + buffer->length, length, &bytesRead, NULL) != FALSE && + bytesRead > 0) { + buffer->length += (uint32_t) bytesRead; + length -= (uint32_t) bytesRead; + } +} + +static inline void readUntilEOF(HANDLE handle, FFstrbuf* buffer) { + ffStrbufEnsureFree(buffer, 31); + uint32_t available = ffStrbufGetFree(buffer); + DWORD bytesRead = 0; + while ( + ReadFile(handle, buffer->chars + buffer->length, available, &bytesRead, NULL) != FALSE && + bytesRead > 0) { + buffer->length += (uint32_t) bytesRead; + if ((uint32_t) bytesRead == available) { + ffStrbufEnsureFree(buffer, buffer->allocated - 1); // Doubles capacity every round. -1 for the null byte. + } + available = ffStrbufGetFree(buffer); + } +} + +bool ffAppendFDBuffer(HANDLE handle, FFstrbuf* buffer) { + FILE_STANDARD_INFORMATION fileInfo; + IO_STATUS_BLOCK iosb; + if (!NT_SUCCESS(NtQueryInformationFile(handle, &iosb, &fileInfo, sizeof(fileInfo), FileStandardInformation))) { + fileInfo.EndOfFile.QuadPart = 0; + } + + if (fileInfo.EndOfFile.QuadPart > 0) { + readWithLength(handle, buffer, (uint32_t) fileInfo.EndOfFile.QuadPart); + } else { + readUntilEOF(handle, buffer); + } + + buffer->chars[buffer->length] = '\0'; + + return buffer->length > 0; +} + +HANDLE openatW(HANDLE dfd, const wchar_t* fileName, uint16_t fileNameLen, bool directory) { + assert(fileNameLen <= 0x7FFF); + + HANDLE hFile; + IO_STATUS_BLOCK iosb = {}; + if (!NT_SUCCESS(NtOpenFile(&hFile, + (directory ? FILE_LIST_DIRECTORY | FILE_TRAVERSE : FILE_READ_DATA | FILE_READ_EA) | FILE_READ_ATTRIBUTES | SYNCHRONIZE, + &(OBJECT_ATTRIBUTES) { + .Length = sizeof(OBJECT_ATTRIBUTES), + .RootDirectory = dfd, + .ObjectName = &(UNICODE_STRING) { + .Buffer = (PWSTR) fileName, + .Length = fileNameLen * (USHORT) sizeof(wchar_t), + .MaximumLength = (fileNameLen + 1) * (USHORT) sizeof(wchar_t), + }, + .Attributes = OBJ_CASE_INSENSITIVE, + }, + &iosb, + FILE_SHARE_READ | (directory ? FILE_SHARE_WRITE | FILE_SHARE_DELETE : 0), + FILE_SYNCHRONOUS_IO_NONALERT | (directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE)))) { + return INVALID_HANDLE_VALUE; + } + + return hFile; +} + +HANDLE openat(HANDLE dfd, const char* fileName, int oflag) { + wchar_t fileNameW[MAX_PATH]; + ULONG len; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &len, fileName, (ULONG) strlen(fileName) + 1))) { + return INVALID_HANDLE_VALUE; + } + // Implies `fileNameW[len] = L'\0';` and `len` includes the null terminator + len /= sizeof(wchar_t); // convert from bytes to characters + + for (uint32_t i = 0; i < len - 1; ++i) { + if (fileNameW[i] == L'/') { + fileNameW[i] = L'\\'; + } + } + + return openatW(dfd, fileNameW, (uint16_t) (len - 1), !!(oflag & O_DIRECTORY)); +} + +bool ffPathExpandEnv(const char* in, FFstrbuf* out) { + if (in[0] == '~') { + if ((in[1] == '/' || in[1] == '\\' || in[1] == '\0') && !ffStrContainsC(in, '%')) { + ffStrbufSet(out, &instance.state.platform.homeDir); + ffStrbufAppendS(out, in + 1); + return true; + } + } + + wchar_t pathInW[MAX_PATH], pathOutW[MAX_PATH]; + ULONG len = (ULONG) strlen(in); + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathInW, (ULONG) sizeof(pathInW), &len, in, len))) { + return false; + } + len /= sizeof(wchar_t); // convert from bytes to characters + + SIZE_T outLen; // in characters, including null terminator + if (!NT_SUCCESS(RtlExpandEnvironmentStrings(NULL, pathInW, len, pathOutW, ARRAY_SIZE(pathOutW), &outLen))) { + return false; + } + + ffStrbufSetNWS(out, (uint32_t) outLen - 1, pathOutW); + return true; +} + +bool ffSuppressIO(bool suppress) { +#ifndef NDEBUG + if (instance.config.display.debugMode) { + return false; + } +#endif + + static bool init = false; + static HANDLE hOrigOut = INVALID_HANDLE_VALUE; + static HANDLE hOrigErr = INVALID_HANDLE_VALUE; + HANDLE hNullFile = ffGetNullFD(); + static int fOrigOut = -1; + static int fOrigErr = -1; + static int fNullFile = -1; + + if (!init) { + if (!suppress) { + return true; + } + + hOrigOut = GetStdHandle(STD_OUTPUT_HANDLE); + hOrigErr = GetStdHandle(STD_ERROR_HANDLE); + fOrigOut = _dup(STDOUT_FILENO); + fOrigErr = _dup(STDERR_FILENO); + fNullFile = _open_osfhandle((intptr_t) hNullFile, 0); + + init = true; + } + if (hNullFile == INVALID_HANDLE_VALUE || fNullFile == -1) { + return false; + } + + fflush(stdout); + fflush(stderr); + + SetStdHandle(STD_OUTPUT_HANDLE, suppress ? hNullFile : hOrigOut); + SetStdHandle(STD_ERROR_HANDLE, suppress ? hNullFile : hOrigErr); + _dup2(suppress ? fNullFile : fOrigOut, STDOUT_FILENO); + _dup2(suppress ? fNullFile : fOrigErr, STDERR_FILENO); + + return true; +} + +void listFilesRecursively(uint32_t baseLength, FFstrbuf* folder, uint8_t indentation, const char* folderName, bool pretty) { + uint32_t folderLength = folder->length; + + if (pretty && folderName != NULL) { + for (uint8_t i = 0; i < indentation - 1; i++) { + fputs(" | ", stdout); + } + printf("%s/\n", folderName); + } + + ffStrbufAppendC(folder, '*'); + WIN32_FIND_DATAA entry; + HANDLE hFind = FindFirstFileA(folder->chars, &entry); + ffStrbufTrimRight(folder, '*'); + if (hFind == INVALID_HANDLE_VALUE) { + return; + } + + do { + if (entry.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + if (ffStrEquals(entry.cFileName, ".") || ffStrEquals(entry.cFileName, "..")) { + continue; + } + + ffStrbufSubstrBefore(folder, folderLength); + ffStrbufAppendS(folder, entry.cFileName); + ffStrbufAppendC(folder, '/'); + listFilesRecursively(baseLength, folder, (uint8_t) (indentation + 1), entry.cFileName, pretty); + ffStrbufSubstrBefore(folder, folderLength); + continue; + } + + if (pretty) { + for (uint8_t i = 0; i < indentation; i++) { + fputs(" | ", stdout); + } + } else { + fputs(folder->chars + baseLength, stdout); + } + + puts(entry.cFileName); + } while (FindNextFileA(hFind, &entry)); + FindClose(hFind); +} + +void ffListFilesRecursively(const char* path, bool pretty) { + FF_STRBUF_AUTO_DESTROY folder = ffStrbufCreateS(path); + ffStrbufEnsureEndsWithC(&folder, '/'); + listFilesRecursively(folder.length, &folder, 0, NULL, pretty); +} + +const char* ffGetTerminalResponse(const char* request, int nParams, const char* format, ...) { + HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE); + FF_AUTO_CLOSE_FD HANDLE hConin = INVALID_HANDLE_VALUE; + DWORD inputMode = 0; + bool hasInputMode = !!GetConsoleMode(hInput, &inputMode); + if (!hasInputMode) { + hConin = CreateFileW(L"CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, NULL); + hInput = hConin; + hasInputMode = !!GetConsoleMode(hInput, &inputMode); + } + SetConsoleMode(hInput, 0); + + FlushConsoleInputBuffer(hInput); + + { + DWORD bytes = 0; + HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); + FF_AUTO_CLOSE_FD HANDLE hConout = INVALID_HANDLE_VALUE; + DWORD outputMode; + if (!GetConsoleMode(hOutput, &outputMode)) { + hConout = CreateFileW(L"CONOUT$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, NULL); + hOutput = hConout; + } + WriteFile(hOutput, request, (DWORD) strlen(request), &bytes, NULL); + } + + while (true) { + if (NtWaitForSingleObject(hInput, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) FF_IO_TERM_RESP_WAIT_MS * -10000 }) != STATUS_WAIT_0) { + SetConsoleMode(hInput, inputMode); + return "NtWaitForSingleObject() failed or timeout"; + } + + // Ignore all unexpected input events + INPUT_RECORD record; + DWORD len = 0; + if (!PeekConsoleInputW(hInput, &record, 1, &len)) { + break; + } + + if ( + record.EventType == KEY_EVENT && + record.Event.KeyEvent.uChar.UnicodeChar != L'\r' && + record.Event.KeyEvent.uChar.UnicodeChar != L'\n') { + break; + } else { + ReadConsoleInputW(hInput, &record, 1, &len); + } + } + + va_list args; + va_start(args, format); + + char buffer[1024]; + uint32_t bytesRead = 0; + + while (true) { + DWORD bytes = 0; + if (!ReadFile(hInput, buffer + bytesRead, (DWORD) (sizeof(buffer) - 1 - bytesRead), &bytes, NULL) || bytes == 0) { + va_end(args); + return "ReadFile() failed"; + } + + bytesRead += bytes; + if (__builtin_expect(bytesRead >= sizeof(buffer) - 1, false)) { + va_end(args); + return "terminal response buffer overflow"; + } + buffer[bytesRead] = '\0'; + + va_list cargs; + va_copy(cargs, args); + int ret = vsscanf(buffer, format, cargs); + va_end(cargs); + + if (ret <= 0) { + va_end(args); + return "vsscanf(buffer, format, args) failed"; + } + if (ret >= nParams) { + break; + } + } + + if (hasInputMode) { + SetConsoleMode(hInput, inputMode); + } + + va_end(args); + + return NULL; +} + +FFNativeFD ffGetNullFD(void) { + static FFNativeFD hNullFile = INVALID_HANDLE_VALUE; + if (hNullFile != INVALID_HANDLE_VALUE) { + return hNullFile; + } + hNullFile = CreateFileW( + L"NUL", + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_WRITE, + 0, + OPEN_EXISTING, + 0, + &(SECURITY_ATTRIBUTES) { + .nLength = sizeof(SECURITY_ATTRIBUTES), + .lpSecurityDescriptor = NULL, + .bInheritHandle = TRUE, + }); + return hNullFile; +} + +bool ffRemoveFile(const char* fileName) { + return DeleteFileA(fileName) != FALSE; +} diff --git a/src/common/impl/jsonconfig.c b/src/common/impl/jsonconfig.c new file mode 100644 index 0000000..fccc56c --- /dev/null +++ b/src/common/impl/jsonconfig.c @@ -0,0 +1,365 @@ +#include "fastfetch.h" +#include "common/color.h" +#include "common/jsonconfig.h" +#include "common/printing.h" +#include "common/io.h" +#include "common/time.h" +#include "common/strutil.h" +#include "detection/version/version.h" +#include "modules/modules.h" + +#include <assert.h> +#include <ctype.h> +#include <inttypes.h> + +bool ffJsonConfigParseModuleArgs(yyjson_val* key, yyjson_val* val, FFModuleArgs* moduleArgs) { + if (unsafe_yyjson_equals_str(key, "type") || unsafe_yyjson_equals_str(key, "condition")) { + return true; + } + + if (unsafe_yyjson_equals_str(key, "key")) { + ffStrbufSetJsonVal(&moduleArgs->key, val); + return true; + } else if (unsafe_yyjson_equals_str(key, "format")) { + ffStrbufSetJsonVal(&moduleArgs->outputFormat, val); + return true; + } else if (unsafe_yyjson_equals_str(key, "outputColor")) { + ffOptionParseColor(yyjson_get_str(val), &moduleArgs->outputColor); + return true; + } else if (unsafe_yyjson_equals_str(key, "keyColor")) { + ffOptionParseColor(yyjson_get_str(val), &moduleArgs->keyColor); + return true; + } else if (unsafe_yyjson_equals_str(key, "keyWidth")) { + moduleArgs->keyWidth = (uint32_t) yyjson_get_uint(val); + return true; + } else if (unsafe_yyjson_equals_str(key, "keyIcon")) { + ffStrbufSetJsonVal(&moduleArgs->keyIcon, val); + return true; + } + return false; +} + +void ffJsonConfigGenerateModuleArgsConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFModuleArgs* moduleArgs) { + if (moduleArgs->key.length > 0) { + yyjson_mut_obj_add_strbuf(doc, module, "key", &moduleArgs->key); + } + if (moduleArgs->outputFormat.length > 0) { + yyjson_mut_obj_add_strbuf(doc, module, "format", &moduleArgs->outputFormat); + } + if (moduleArgs->outputColor.length > 0) { + yyjson_mut_obj_add_strbuf(doc, module, "outputColor", &moduleArgs->outputColor); + } + if (moduleArgs->keyColor.length > 0) { + yyjson_mut_obj_add_strbuf(doc, module, "keyColor", &moduleArgs->keyColor); + } + if (moduleArgs->keyWidth > 0) { + yyjson_mut_obj_add_uint(doc, module, "keyWidth", moduleArgs->keyWidth); + } + if (moduleArgs->keyIcon.length > 0) { + yyjson_mut_obj_add_strbuf(doc, module, "keyIcon", &moduleArgs->keyIcon); + } +} + +const char* ffJsonConfigParseEnum(yyjson_val* val, int* result, FFKeyValuePair pairs[]) { + if (yyjson_is_int(val)) { + int intVal = yyjson_get_int(val); + + for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) { + if (intVal == pPair->value) { + *result = pPair->value; + return NULL; + } + } + + return "Invalid enum integer"; + } else if (yyjson_is_str(val)) { + const char* strVal = yyjson_get_str(val); + for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) { + if (ffStrEqualsIgnCase(strVal, pPair->key)) { + *result = pPair->value; + return NULL; + } + } + + return "Invalid enum string"; + } else { + return "Invalid enum value type; must be a string or integer"; + } +} + +static bool parseModuleJsonObject(const char* type, yyjson_val* jsonVal, yyjson_mut_doc* jsonDoc) { + if (!ffCharIsEnglishAlphabet(type[0])) { + return false; + } + + for (FFModuleBaseInfo** modules = ffModuleInfos[toupper(type[0]) - 'A']; *modules; ++modules) { + FFModuleBaseInfo* baseInfo = *modules; + if (ffStrEqualsIgnCase(type, baseInfo->name)) { + uint8_t optionBuf[FF_OPTION_MAX_SIZE]; + baseInfo->initOptions(optionBuf); + if (jsonVal) { + baseInfo->parseJsonObject(optionBuf, jsonVal); + } + bool succeeded; + if (jsonDoc) { + yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root); + yyjson_mut_obj_add_str(jsonDoc, module, "type", baseInfo->name); + if (baseInfo->generateJsonResult) { + succeeded = baseInfo->generateJsonResult(optionBuf, jsonDoc, module); + } else { + yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unsupported for JSON format"); + succeeded = false; + } + } else { + succeeded = baseInfo->printModule(optionBuf); + } + baseInfo->destroyOptions(optionBuf); + return succeeded; + } + } + + if (jsonDoc) { + yyjson_mut_val* module = yyjson_mut_arr_add_obj(jsonDoc, jsonDoc->root); + yyjson_mut_obj_add_strcpy(jsonDoc, module, "type", type); + yyjson_mut_obj_add_str(jsonDoc, module, "error", "Unknown module type"); + } else { + FFModuleArgs moduleArgs; + ffOptionInitModuleArg(&moduleArgs, ""); + ffPrintError(type, 0, &moduleArgs, FF_PRINT_TYPE_DEFAULT, "Unknown module type"); + ffOptionDestroyModuleArg(&moduleArgs); + } + return false; +} + +static void prepareModuleJsonObject(const char* type, yyjson_val* module) { + switch (type[0]) { + #if !FF_MODULE_DISABLE_CPUUSAGE + case 'c': + case 'C': { + if (ffStrEqualsIgnCase(type, FF_CPUUSAGE_MODULE_NAME)) { + ffPrepareCPUUsage(); + } else if (ffStrEqualsIgnCase(type, FF_COMMAND_MODULE_NAME)) { + FF_A_CLEANUP(ffDestroyCommandOptions) FFCommandOptions options; + ffInitCommandOptions(&options); + if (module) { + ffCommandModuleInfo.parseJsonObject(&options, module); + } + ffPrepareCommand(&options); + } + break; + } + #endif + + #if !FF_MODULE_DISABLE_DISKIO + case 'd': + case 'D': { + if (ffStrEqualsIgnCase(type, FF_DISKIO_MODULE_NAME)) { + FF_A_CLEANUP(ffDestroyDiskIOOptions) FFDiskIOOptions options; + ffInitDiskIOOptions(&options); + if (module) { + ffDiskIOModuleInfo.parseJsonObject(&options, module); + } + ffPrepareDiskIO(&options); + } + break; + } + #endif + + #if !FF_MODULE_DISABLE_NETIO + case 'n': + case 'N': { + if (ffStrEqualsIgnCase(type, FF_NETIO_MODULE_NAME)) { + FF_A_CLEANUP(ffDestroyNetIOOptions) FFNetIOOptions options; + ffInitNetIOOptions(&options); + if (module) { + ffNetIOModuleInfo.parseJsonObject(&options, module); + } + ffPrepareNetIO(&options); + } + break; + } + #endif + + #if !FF_MODULE_DISABLE_PUBLICIP + case 'p': + case 'P': { + if (ffStrEqualsIgnCase(type, FF_PUBLICIP_MODULE_NAME)) { + FF_A_CLEANUP(ffDestroyPublicIpOptions) FFPublicIPOptions options; + ffInitPublicIpOptions(&options); + if (module) { + ffPublicIPModuleInfo.parseJsonObject(&options, module); + } + ffPreparePublicIp(&options); + } + break; + } + #endif + + #if !FF_MODULE_DISABLE_WEATHER + case 'w': + case 'W': { + if (ffStrEqualsIgnCase(type, FF_WEATHER_MODULE_NAME)) { + FF_A_CLEANUP(ffDestroyWeatherOptions) FFWeatherOptions options; + ffInitWeatherOptions(&options); + if (module) { + ffWeatherModuleInfo.parseJsonObject(&options, module); + } + ffPrepareWeather(&options); + } + break; + } + #endif + } +} + +static bool matchesJsonArray(const char* str, yyjson_val* val) { + assert(val); + + if (unsafe_yyjson_is_str(val)) { + return ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(val)); + } + + if (!unsafe_yyjson_is_arr(val)) { + return false; + } + + size_t idx, max; + yyjson_val* item; + yyjson_arr_foreach (val, idx, max, item) { + if (yyjson_is_str(item) && ffStrEqualsIgnCase(str, unsafe_yyjson_get_str(item))) { + return true; + } + } + return false; +} + +static const char* printJsonConfig(FFdata* data, bool prepare) { + yyjson_mut_doc* jsonDoc = data->resultDoc; + yyjson_val* const root = yyjson_doc_get_root(data->configDoc); + assert(root); + + if (!yyjson_is_obj(root)) { + return "Invalid JSON config format. Root value must be an object"; + } + + yyjson_val* modules = yyjson_obj_get(root, "modules"); + if (!modules) { + return NULL; + } + if (!yyjson_is_arr(modules)) { + return "Property 'modules' must be an array of strings or objects"; + } + + bool succeeded = true; + int32_t thres = instance.config.display.stat; + yyjson_val* item; + size_t idx, max; + yyjson_arr_foreach (modules, idx, max, item) { + double ms = 0; + if (!prepare && thres >= 0) { + ms = ffTimeGetTick(); + } + + yyjson_val* module = item; + const char* type = yyjson_get_str(module); + if (type) { + module = NULL; + } else if (yyjson_is_obj(module)) { + yyjson_val* conditions = yyjson_obj_get(module, "condition"); + if (conditions) { + if (!yyjson_is_obj(conditions)) { + return "Property 'condition' must be an object"; + } + + yyjson_val* system = yyjson_obj_get(conditions, "system"); + if (system && !matchesJsonArray(ffVersionResult.sysName, system)) { + continue; + } + + system = yyjson_obj_get(conditions, "!system"); + if (system && matchesJsonArray(ffVersionResult.sysName, system)) { + continue; + } + + yyjson_val* arch = yyjson_obj_get(conditions, "arch"); + if (arch && !matchesJsonArray(ffVersionResult.architecture, arch)) { + continue; + } + + arch = yyjson_obj_get(conditions, "!arch"); + if (arch && matchesJsonArray(ffVersionResult.architecture, arch)) { + continue; + } + + yyjson_val* previousSucceeded = yyjson_obj_get(conditions, "succeeded"); + if (previousSucceeded && !unsafe_yyjson_is_null(previousSucceeded)) { + if (!unsafe_yyjson_is_bool(previousSucceeded)) { + return "Property 'succeeded' in 'condition' must be a boolean"; + } + if (succeeded != unsafe_yyjson_get_bool(previousSucceeded)) { + continue; + } + } + } + + type = yyjson_get_str(yyjson_obj_get(module, "type")); + if (!type) { + return "module object must contain a \"type\" key ( case sensitive )"; + } + if (yyjson_obj_size(module) == 1) { // contains only Property type + module = NULL; + } + } else { + return "modules must be an array of strings or objects"; + } + + if (ffStrbufSeparatedContainIgnCaseS(&data->structureDisabled, type, ':')) { + continue; + } + + if (prepare) { + prepareModuleJsonObject(type, module); + } else { + succeeded = parseModuleJsonObject(type, module, jsonDoc); + } + + if (!prepare && thres >= 0) { + ms = ffTimeGetTick() - ms; + if (jsonDoc) { + yyjson_mut_val* moduleJson = yyjson_mut_arr_get_last(jsonDoc->root); + yyjson_mut_obj_add_real(jsonDoc, moduleJson, "stat", ms); + } else { + char str[64]; + int len = snprintf(str, sizeof str, "%.3fms", ms); + if (thres > 0) { + snprintf(str, sizeof str, "\e[%sm%.3fms\e[m", (ms <= thres ? FF_COLOR_FG_GREEN : ms <= 2 * thres ? FF_COLOR_FG_YELLOW + : FF_COLOR_FG_RED), + ms); + } + printf("\e7\e[1A\e[9999999C\e[%dD%s\e8", len - 1, str); // Save; Up 1; Right 9999999; Left <len - 1>; Print <str>; Load + } + } + +#if defined(_WIN32) + if (!instance.config.display.noBuffer && !jsonDoc) { + fflush(stdout); + } +#endif + } + + return NULL; +} + +void ffPrintJsonConfig(FFdata* data, bool prepare) { + yyjson_mut_doc* jsonDoc = data->resultDoc; + const char* error = printJsonConfig(data, prepare); + if (error) { + if (jsonDoc) { + yyjson_mut_val* obj = yyjson_mut_obj(jsonDoc); + yyjson_mut_obj_add_str(jsonDoc, obj, "error", error); + yyjson_mut_doc_set_root(jsonDoc, obj); + } else { + ffPrintError("JsonConfig", 0, NULL, FF_PRINT_TYPE_NO_CUSTOM_KEY, "%s", error); + } + } +} diff --git a/src/common/impl/kmod_apple.c b/src/common/impl/kmod_apple.c new file mode 100644 index 0000000..82edc49 --- /dev/null +++ b/src/common/impl/kmod_apple.c @@ -0,0 +1,12 @@ +#include "common/kmod.h" +#include "common/apple/cf_helpers.h" +#include <IOKit/kext/KextManager.h> +#include <CoreFoundation/CoreFoundation.h> + +bool ffKmodLoaded(const char* modName) { + FF_CFTYPE_AUTO_RELEASE CFStringRef name = CFStringCreateWithCString(kCFAllocatorDefault, modName, kCFStringEncodingUTF8); + FF_CFTYPE_AUTO_RELEASE CFArrayRef identifiers = CFArrayCreate(kCFAllocatorDefault, (const void**) &name, 1, &kCFTypeArrayCallBacks); + FF_CFTYPE_AUTO_RELEASE CFArrayRef keys = CFArrayCreate(kCFAllocatorDefault, NULL, 0, NULL); + FF_CFTYPE_AUTO_RELEASE CFDictionaryRef kextInfo = KextManagerCopyLoadedKextInfo(identifiers, keys); + return CFDictionaryContainsKey(kextInfo, name); +} diff --git a/src/common/impl/kmod_bsd.c b/src/common/impl/kmod_bsd.c new file mode 100644 index 0000000..996d4ab --- /dev/null +++ b/src/common/impl/kmod_bsd.c @@ -0,0 +1,7 @@ +#include "common/kmod.h" +#include <sys/param.h> +#include <sys/module.h> + +bool ffKmodLoaded(const char* modName) { + return modfind(modName) >= 0; +} diff --git a/src/common/impl/kmod_linux.c b/src/common/impl/kmod_linux.c new file mode 100644 index 0000000..204ab35 --- /dev/null +++ b/src/common/impl/kmod_linux.c @@ -0,0 +1,25 @@ +#include "common/kmod.h" +#include "common/io.h" + +bool ffKmodLoaded(const char* modName) { + static FFstrbuf modules; + if (modules.chars == NULL) { + ffStrbufInitS(&modules, "\n"); + ffAppendFileBuffer("/proc/modules", &modules); + } + + if (modules.length == 0) { + return false; + } + + uint32_t len = (uint32_t) strlen(modName); + if (len > 250) { + return false; + } + + char temp[256]; + temp[0] = '\n'; + memcpy(temp + 1, modName, len); + temp[1 + len] = ' '; + return memmem(modules.chars, modules.length, temp, len + 2) != NULL; +} diff --git a/src/common/impl/kmod_nbsd.c b/src/common/impl/kmod_nbsd.c new file mode 100644 index 0000000..f1b450a --- /dev/null +++ b/src/common/impl/kmod_nbsd.c @@ -0,0 +1,40 @@ +#include "common/kmod.h" +#include "common/strutil.h" + +#include <sys/module.h> +#include <sys/param.h> + +typedef struct FF_A_PACKED FFNbsdModList { + int len; + modstat_t mods[]; +} FFNbsdModList; + +bool ffKmodLoaded(const char* modName) { + static FFNbsdModList* list = NULL; + + if (list == NULL) { + struct iovec iov = {}; + + for (size_t len = 8192;; len = iov.iov_len) { + iov.iov_len = len; + iov.iov_base = realloc(iov.iov_base, len); + if (modctl(MODCTL_STAT, &iov) < 0) { + free(iov.iov_base); + return true; // ignore errors + } + + if (len >= iov.iov_len) { + break; + } + } + list = (FFNbsdModList*) iov.iov_base; + } + + for (int i = 0; i < list->len; i++) { + if (ffStrEquals(list->mods[i].ms_name, modName)) { + return true; + } + } + + return false; +} diff --git a/src/common/impl/kmod_nosupport.c b/src/common/impl/kmod_nosupport.c new file mode 100644 index 0000000..c254ba7 --- /dev/null +++ b/src/common/impl/kmod_nosupport.c @@ -0,0 +1,5 @@ +#include "common/kmod.h" + +bool ffKmodLoaded(FF_A_UNUSED const char* modName) { + return true; // Don't generate kernel module related errors +} diff --git a/src/common/impl/kmod_sunos.c b/src/common/impl/kmod_sunos.c new file mode 100644 index 0000000..da392e2 --- /dev/null +++ b/src/common/impl/kmod_sunos.c @@ -0,0 +1,23 @@ +#include "common/kmod.h" +#include "common/strutil.h" + +#include <sys/modctl.h> +#include <errno.h> + +bool ffKmodLoaded(const char* modName) { + struct modinfo modinfo = { + .mi_id = -1, + .mi_nextid = -1, + .mi_info = MI_INFO_ALL, + }; + + for (int id = -1; modctl(MODINFO, id, &modinfo) == 0; id = modinfo.mi_id) { + modinfo.mi_name[MODMAXNAMELEN - 1] = '\0'; + + if (ffStrEquals(modinfo.mi_name, modName)) { + return true; + } + } + + return !(errno == EINVAL || errno == ENOENT); +} diff --git a/src/common/impl/kmod_windows.c b/src/common/impl/kmod_windows.c new file mode 100644 index 0000000..081e5a7 --- /dev/null +++ b/src/common/impl/kmod_windows.c @@ -0,0 +1,28 @@ +#include "common/kmod.h" +#include "common/windows/nt.h" +#include "common/mallocHelper.h" +#include "common/strutil.h" + +bool ffKmodLoaded(const char* modName) { + ULONG bufferSize = 0; + NtQuerySystemInformation(SystemModuleInformation, NULL, 0, &bufferSize); + if (bufferSize == 0) { + return true; // ignore errors + } + + FF_AUTO_FREE RTL_PROCESS_MODULES* buffer = malloc(bufferSize); + + if (!NT_SUCCESS(NtQuerySystemInformation(SystemModuleInformation, buffer, bufferSize, &bufferSize))) { + return true; // ignore errors + } + + for (ULONG i = 0; i < buffer->NumberOfModules; i++) { + const char* name = (const char*) buffer->Modules[i].FullPathName + buffer->Modules[i].OffsetToFileName; + + if (ffStrEqualsIgnCase(name, modName)) { + return true; + } + } + + return false; +} diff --git a/src/common/impl/library.c b/src/common/impl/library.c new file mode 100644 index 0000000..6f96e96 --- /dev/null +++ b/src/common/impl/library.c @@ -0,0 +1,180 @@ +#include "fastfetch.h" +#include "common/library.h" +#include "common/debug.h" + +#if _WIN32 + #include "common/windows/nt.h" + #include <errno.h> + #include <ntstatus.h> +#endif + +#ifndef FF_DISABLE_DLOPEN + + #include <stdarg.h> + + // Clang doesn't define __SANITIZE_ADDRESS__ but defines __has_feature(address_sanitizer) + #if !defined(__SANITIZE_ADDRESS__) && defined(__has_feature) + #if __has_feature(address_sanitizer) + #define __SANITIZE_ADDRESS__ + #endif + #endif + + #ifndef FF_DLOPEN_FLAGS + #ifdef __SANITIZE_ADDRESS__ + #define FF_DLOPEN_FLAGS RTLD_LAZY | RTLD_NODELETE + #else + #define FF_DLOPEN_FLAGS RTLD_LAZY + #endif + #endif + +static void* libraryLoad(const char* path, int maxVersion) { + void* result = dlopen(path, FF_DLOPEN_FLAGS); + + #if _WIN32 + + // libX.dll.1 never exists on Windows, while libX-1.dll may exist + FF_UNUSED(maxVersion) + + if (result != NULL) { + return result; + } + + uint32_t pathLen = ffStrbufLastIndexC(&instance.state.platform.exePath, '/'); + if (pathLen == instance.state.platform.exePath.length) { + return result; + } + + char absPath[MAX_PATH * 2]; + strcpy(mempcpy(absPath, instance.state.platform.exePath.chars, pathLen + 1), path); + return dlopen(absPath, FF_DLOPEN_FLAGS); + + #else + + if (result == NULL) { + FF_DEBUG("dlopen(\"%s\"): %s", path, dlerror()); + } + + if (result != NULL || maxVersion < 0) { + return result; + } + + FF_STRBUF_AUTO_DESTROY pathbuf = ffStrbufCreateA(64); + ffStrbufAppendS(&pathbuf, path); + ffStrbufAppendC(&pathbuf, '.'); + + for (int i = maxVersion; i >= 0; --i) { + uint32_t originalLength = pathbuf.length; + ffStrbufAppendSInt(&pathbuf, i); + + result = dlopen(pathbuf.chars, FF_DLOPEN_FLAGS); + if (result != NULL) { + break; + } else { + FF_DEBUG("dlopen(\"%s\"): %s", pathbuf.chars, dlerror()); + } + + ffStrbufSubstrBefore(&pathbuf, originalLength); + } + + #endif + + return result; +} + +void* ffLibraryLoad(const char* path, int maxVersion, ...) { + void* result = libraryLoad(path, maxVersion); + + if (!result) { + va_list defaultNames; + va_start(defaultNames, maxVersion); + + do { + const char* pathRest = va_arg(defaultNames, const char*); + if (pathRest == NULL) { + break; + } + + int maxVersionRest = va_arg(defaultNames, int); + result = libraryLoad(pathRest, maxVersionRest); + } while (!result); + + va_end(defaultNames); + } + + return result; +} + +#endif + +#if _WIN32 + +void* dlopen(const char* path, FF_A_UNUSED int mode) { + wchar_t pathW[MAX_PATH + 1]; + ULONG pathWBytes = 0; + + NTSTATUS status = RtlUTF8ToUnicodeN(pathW, sizeof(pathW), &pathWBytes, path, (uint32_t) strlen(path) + 1); + if (!NT_SUCCESS(status)) { + FF_DEBUG("RtlUTF8ToUnicodeN failed for path %s with status 0x%08lX: %s", path, status, ffDebugNtStatus(status)); + return NULL; + } + + PVOID module = NULL; + status = LdrLoadDll(NULL, NULL, &(UNICODE_STRING) { + .Length = (USHORT) (pathWBytes - sizeof(wchar_t)), // Exclude null terminator + .MaximumLength = (USHORT) pathWBytes, + .Buffer = pathW, + }, + &module); + + if (!NT_SUCCESS(status)) { + FF_DEBUG("LdrLoadDll failed for path %s with status 0x%08lX: %s", path, status, ffDebugNtStatus(status)); + return NULL; + } + + return module; +} + +int dlclose(void* handle) { + NTSTATUS status = LdrUnloadDll(handle); + if (!NT_SUCCESS(status)) { + FF_DEBUG("LdrUnloadDll failed for handle %p with status 0x%08lX: %s", handle, status, ffDebugNtStatus(status)); + return -1; + } + return 0; +} + +void* dlsym(void* handle, const char* symbol) { + void* address; + USHORT symbolBytes = (USHORT) (strlen(symbol) + 1); + NTSTATUS status = LdrGetProcedureAddress(handle, &(ANSI_STRING) { + .Length = symbolBytes - sizeof(char), + .MaximumLength = symbolBytes, + .Buffer = (char*) symbol, + }, + 0, + &address); + if (!NT_SUCCESS(status)) { + FF_DEBUG("LdrGetProcedureAddress failed for symbol %s with status 0x%08lX: %s", symbol, status, ffDebugNtStatus(status)); + return NULL; + } + return address; +} + +void* ffLibraryGetModule(const wchar_t* libraryFileName) { + assert(libraryFileName != NULL && "Use \"ffGetPeb()->ImageBaseAddress\" instead"); + + void* module = NULL; + USHORT libraryFileNameBytes = (USHORT) (wcslen(libraryFileName) * sizeof(wchar_t) + sizeof(wchar_t)); + NTSTATUS status = LdrGetDllHandle(NULL, NULL, &(UNICODE_STRING) { + .Length = libraryFileNameBytes - sizeof(wchar_t), + .MaximumLength = libraryFileNameBytes, + .Buffer = (wchar_t*) libraryFileName, + }, + &module); + if (!NT_SUCCESS(status)) { + FF_DEBUG("LdrGetDllHandle failed for library %ls with status 0x%08lX: %s", libraryFileName, status, ffDebugNtStatus(status)); + return NULL; + } + return module; +} +#endif diff --git a/src/common/impl/lua.c b/src/common/impl/lua.c new file mode 100644 index 0000000..ffc1d83 --- /dev/null +++ b/src/common/impl/lua.c @@ -0,0 +1,233 @@ +#if FF_HAVE_LUA + + #include "common/lua.h" + #include "common/mallocHelper.h" + +struct FFLuaData luaData; + +static yyjson_mut_val* lua2yyjson(lua_State* L, int idx, yyjson_mut_doc* doc, int depth) { + if (__builtin_expect(depth > 15, false)) { + yyjson_mut_doc_free(doc); + lua_pushlstring( + L, "yyjson: recursion depth exceeded; possible circular reference", strlen("yyjson: recursion depth exceeded; possible circular reference")); + lua_error(L); // noreturn + __builtin_unreachable(); + } + + if (idx < 0) { + idx = lua_gettop(L) + idx + 1; + } + int type = lua_type(L, idx); + + switch (type) { + case LUA_TNIL: + return yyjson_mut_null(doc); + + case LUA_TBOOLEAN: + return yyjson_mut_bool(doc, lua_toboolean(L, idx)); + + case LUA_TNUMBER: { + if (lua_isinteger(L, idx)) { + lua_Integer i = lua_tointeger(L, idx); + return yyjson_mut_sint(doc, (int64_t) i); + } else { + return yyjson_mut_real(doc, lua_tonumber(L, idx)); + } + } + + case LUA_TSTRING: { + size_t len; + const char* str = lua_tolstring(L, idx, &len); + return yyjson_mut_strncpy(doc, str, len); + } + + case LUA_TTABLE: { + lua_Unsigned len = lua_rawlen(L, idx); + int is_array = 1; + + if (len == 0) { + lua_pushnil(L); + is_array = 0; + if (lua_next(L, idx) != 0) { + lua_pop(L, 2); + } + } else { + lua_pushnil(L); + while (lua_next(L, idx) != 0) { + if (lua_type(L, -2) != LUA_TNUMBER) { + is_array = 0; + lua_pop(L, 2); + break; + } + int isnum = false; + lua_Integer k = lua_tointegerx(L, -2, &isnum); + if (!isnum || (lua_Unsigned) k > len) { + is_array = 0; + lua_pop(L, 2); + break; + } + lua_pop(L, 1); + } + } + + if (is_array) { + yyjson_mut_val* arr = yyjson_mut_arr(doc); + for (lua_Unsigned i = 1; i <= len; i++) { + lua_rawgeti(L, idx, (lua_Integer) i); + yyjson_mut_val* val = lua2yyjson(L, -1, doc, depth + 1); + yyjson_mut_arr_append(arr, val); + lua_pop(L, 1); + } + return arr; + } else { + yyjson_mut_val* obj = yyjson_mut_obj(doc); + lua_pushnil(L); + while (lua_next(L, idx) != 0) { + size_t klen; + const char* key_str = luaL_tolstring(L, -2, &klen); + yyjson_mut_val* key = yyjson_mut_strncpy(doc, key_str, klen); + lua_pop(L, 1); + + yyjson_mut_val* val = lua2yyjson(L, -1, doc, depth + 1); + yyjson_mut_obj_add(obj, key, val); + lua_pop(L, 1); + } + return obj; + } + } + + default: + return yyjson_mut_null(doc); + } +} + +static int yyjsonEncode(lua_State* L) { + luaL_checkany(L, 1); + + bool pretty = false; + if (lua_isboolean(L, 2)) { + pretty = (int) lua_toboolean(L, 2); + } + + yyjson_mut_doc* doc = yyjson_mut_doc_new(NULL); + if (__builtin_expect(!doc, false)) { + lua_pushlstring(L, "yyjson: yyjson_mut_doc_new() failed", strlen("yyjson: yyjson_mut_doc_new() failed")); + return lua_error(L); + } + + yyjson_mut_val* root = lua2yyjson(L, 1, doc, 0); + yyjson_mut_doc_set_root(doc, root); + + size_t jsonLen; + yyjson_write_err err = {}; + FF_AUTO_FREE const char* jsonStr = yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INF_AND_NAN | (pretty ? YYJSON_WRITE_PRETTY_TWO_SPACES : 0), NULL, &jsonLen, &err); + + if (__builtin_expect(jsonStr != NULL, true)) { + lua_pushlstring(L, jsonStr, jsonLen); + yyjson_mut_doc_free(doc); + return 1; + } else { + yyjson_mut_doc_free(doc); + { + FF_STRBUF_AUTO_DESTROY errBuf = ffStrbufCreateStatic("yyjson: yyjson_mut_write_opts() failed: "); + ffStrbufAppendS(&errBuf, err.msg); + lua_pushlstring(L, errBuf.chars, errBuf.length); + } + + return lua_error(L); // longjmp + } +} + +const char* ffLuaLoadState(void) { + if (luaData.inited) { + if (luaData.L == NULL) { + return "Lua library is not available"; + } + return NULL; + } + + luaData.inited = true; + // clang-format off + #ifdef _WIN32 + #define FF_LOAD_LIBLUA(version) FF_LIBRARY_LOAD_MESSAGE(liblua, \ + "lua5" #version FF_LIBRARY_EXTENSION, 0) + #else + #define FF_LOAD_LIBLUA(version) FF_LIBRARY_LOAD_MESSAGE(liblua, \ + "liblua5." #version FF_LIBRARY_EXTENSION, 0, \ + "liblua-5." #version FF_LIBRARY_EXTENSION, 0, \ + "liblua5." #version FF_LIBRARY_EXTENSION ".5." #version, 0) + #endif + // clang-format on + #if LUA_VERSION_NUM == 505 + FF_LOAD_LIBLUA(5) + #elif LUA_VERSION_NUM == 504 + FF_LOAD_LIBLUA(4) + #elif LUA_VERSION_NUM == 503 + FF_LOAD_LIBLUA(3) + #else + #error "Unsupported Lua version" + #endif + #undef FF_LOAD_LIBLUA + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaL_newstate) + #if LUA_VERSION_NUM >= 505 + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaL_openselectedlibs) + #else + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaL_requiref) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaopen_base) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaopen_math) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaopen_string) + FF_LIBRARY_LOAD_SYMBOL_MESSAGE(liblua, luaopen_table) + #endif + + #if !FF_DISABLE_DLOPEN + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, luaL_checkany) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, luaL_loadbufferx) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, luaL_tolstring) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_callk) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_createtable) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_error) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_gettop) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_isinteger) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_next) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pcallk) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushboolean) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushcclosure) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushinteger) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushlstring) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushnil) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushnumber) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_pushvalue) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_rawgeti) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_rawlen) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_setfield) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_setglobal) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_seti) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_settop) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_toboolean) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_tointegerx) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_tolstring) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_tonumberx) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(liblua, luaData, lua_type) + #endif + + lua_State* L = ffluaL_newstate(); + if (L == NULL) { + return "luaL_newstate() failed"; + } + #if LUA_VERSION_NUM >= 505 + ffluaL_openselectedlibs(L, LUA_GLIBK | LUA_MATHLIBK | LUA_STRLIBK | LUA_TABLIBK, 0); + #else + ffluaL_requiref(L, LUA_GNAME, ffluaopen_base, 1); + ffluaL_requiref(L, LUA_MATHLIBNAME, ffluaopen_math, 1); + ffluaL_requiref(L, LUA_STRLIBNAME, ffluaopen_string, 1); + ffluaL_requiref(L, LUA_TABLIBNAME, ffluaopen_table, 1); + lua_settop(L, 0); + #endif + lua_pushcfunction(L, yyjsonEncode); + lua_setglobal(L, "json_encode"); + luaData.L = L; + liblua = NULL; // don't close lua + return NULL; +} + +#endif diff --git a/src/common/impl/memrchr.c b/src/common/impl/memrchr.c new file mode 100644 index 0000000..aaf97e6 --- /dev/null +++ b/src/common/impl/memrchr.c @@ -0,0 +1,21 @@ +#include "common/memrchr.h" +#include <stddef.h> +#include <stdint.h> + +void* memrchr(const void* s, int c, size_t n) { + if (n == 0) { + return NULL; + } + + const uint8_t uc = (uint8_t) c; + + const uint8_t* p = (const uint8_t*) s + n; + + while (n--) { + if (*--p == uc) { + return (void*) p; + } + } + + return NULL; +} diff --git a/src/common/impl/netif.c b/src/common/impl/netif.c new file mode 100644 index 0000000..1525a18 --- /dev/null +++ b/src/common/impl/netif.c @@ -0,0 +1,22 @@ +#include "common/netif.h" + +#ifndef _WIN32 + #include <net/if.h> + #include <netinet/in.h> +#endif + +const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV4(void) { + static FFNetifDefaultRouteResult result; + if (result.status == FF_NETIF_UNINITIALIZED) { + result.status = ffNetifGetDefaultRouteImplV4(&result) ? FF_NETIF_OK : FF_NETIF_INVALID; + } + return &result; +} + +const FFNetifDefaultRouteResult* ffNetifGetDefaultRouteV6(void) { + static FFNetifDefaultRouteResult result; + if (result.status == FF_NETIF_UNINITIALIZED) { + result.status = ffNetifGetDefaultRouteImplV6(&result) ? FF_NETIF_OK : FF_NETIF_INVALID; + } + return &result; +} diff --git a/src/common/impl/netif_apple.c b/src/common/impl/netif_apple.c new file mode 100644 index 0000000..2d42735 --- /dev/null +++ b/src/common/impl/netif_apple.c @@ -0,0 +1,218 @@ +#include "common/netif.h" +#include "common/io.h" + +#include <net/if.h> +#include <net/if_dl.h> +#include <net/route.h> +#include <netinet/in.h> +#include <sys/socket.h> + +#define ROUNDUP2(a, n) ((a) > 0 ? (1 + (((a) - 1U) | ((n) - 1))) : (n)) + +#if __APPLE__ + // https://github.com/apple-oss-distributions/network_cmds/blob/8f38231438e6a4d16ef8015e97e12c2c05105644/rtsol.tproj/if.c#L243 + #define ROUNDUP(a) ROUNDUP2((a), sizeof(uint32_t)) +#elif __sun + // https://github.com/illumos/illumos-gate/blob/95b8c88950fa7b19af46bc63230137cf96b0bff7/usr/src/cmd/cmd-inet/usr.sbin/route.c#L339 + #define ROUNDUP(a) ROUNDUP2((a), sizeof(long)) +#else + #error unknown platform +#endif + +static struct sockaddr* +get_rt_address(struct rt_msghdr* rtm, int desired) { + struct sockaddr* sa = (struct sockaddr*) (rtm + 1); + + for (int i = 0; i < RTAX_MAX; i++) { + if (rtm->rtm_addrs & (1 << i)) { + if ((1 << i) == desired) { + return sa; + } + +#ifndef __sun + uint32_t salen = sa->sa_len; +#else + uint32_t salen; + // https://github.com/illumos/illumos-gate/blob/95b8c88950fa7b19af46bc63230137cf96b0bff7/usr/src/cmd/cmd-inet/usr.sbin/route.c#L2941 + switch (sa->sa_family) { + case AF_INET: + salen = sizeof(struct sockaddr_in); + break; + case AF_LINK: + salen = sizeof(struct sockaddr_dl); + break; + case AF_INET6: + salen = sizeof(struct sockaddr_in6); + break; + default: + salen = sizeof(struct sockaddr); + break; + } +#endif + sa = (struct sockaddr*) (ROUNDUP(salen) + (char*) sa); + } + } + return NULL; +} + +bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) { + // https://github.com/hashPirate/copenheimer-masscan-fork/blob/36f1ed9f7b751a7dccd5ed27874e2e703db7d481/src/rawsock-getif.c#L104 + + FF_AUTO_CLOSE_FD int pfRoute = socket(PF_ROUTE, SOCK_RAW, AF_INET); + if (pfRoute < 0) { + return false; + } + + { + struct timeval timeout = { 1, 0 }; + setsockopt(pfRoute, SOL_SOCKET, SO_RCVTIMEO, (char*) &timeout, sizeof(timeout)); + setsockopt(pfRoute, SOL_SOCKET, SO_SNDTIMEO, (char*) &timeout, sizeof(timeout)); + } + + uint32_t pid = instance.state.platform.pid; + + struct { + struct rt_msghdr hdr; + struct sockaddr_in dst; + uint8_t data[512]; + } rtmsg = { + .hdr = { + .rtm_type = RTM_GET, + .rtm_flags = RTF_UP | RTF_GATEWAY, + .rtm_version = RTM_VERSION, + .rtm_addrs = RTA_DST | RTA_IFP | RTA_IFA, + .rtm_msglen = sizeof(rtmsg.hdr) + sizeof(rtmsg.dst), + .rtm_pid = (pid_t) pid, + .rtm_seq = 1, + }, + .dst = { + .sin_family = AF_INET, +#ifndef __sun + .sin_len = sizeof(rtmsg.dst), +#endif + }, + }; + + if (send(pfRoute, &rtmsg, rtmsg.hdr.rtm_msglen, 0) != rtmsg.hdr.rtm_msglen) { + return false; + } + + bool gotResponse = false; + while (recv(pfRoute, &rtmsg, sizeof(rtmsg), 0) > 0) { + if (rtmsg.hdr.rtm_seq == 1 && rtmsg.hdr.rtm_pid == (pid_t) pid) { + gotResponse = true; + break; + } + } + if (!gotResponse) { + return false; + } + +#ifndef __sun // On Solaris, the RTF_GATEWAY flag is not set for default routes for some reason + if ((rtmsg.hdr.rtm_flags & (RTF_UP | RTF_GATEWAY)) == (RTF_UP | RTF_GATEWAY)) +#endif + { + struct sockaddr_dl* sdl = (struct sockaddr_dl*) get_rt_address(&rtmsg.hdr, RTA_IFP); + if (sdl +#ifndef __sun + && sdl->sdl_len +#endif + && sdl->sdl_family == AF_LINK) { + if (sdl->sdl_nlen > IF_NAMESIZE) { + return false; + } + memcpy(result->ifName, sdl->sdl_data, sdl->sdl_nlen); + result->ifName[sdl->sdl_nlen] = '\0'; + result->ifIndex = sdl->sdl_index; + + // Get the preferred source address + struct sockaddr_in* src = (struct sockaddr_in*) get_rt_address(&rtmsg.hdr, RTA_IFA); + if (src && src->sin_family == AF_INET) { + result->preferredSourceAddrV4 = src->sin_addr.s_addr; + } + + return true; + } + return false; + } + + return false; +} + +bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) { + // https://github.com/hashPirate/copenheimer-masscan-fork/blob/36f1ed9f7b751a7dccd5ed27874e2e703db7d481/src/rawsock-getif.c#L104 + + FF_AUTO_CLOSE_FD int pfRoute = socket(PF_ROUTE, SOCK_RAW, AF_INET6); + if (pfRoute < 0) { + return false; + } + + { + struct timeval timeout = { 1, 0 }; + setsockopt(pfRoute, SOL_SOCKET, SO_RCVTIMEO, (char*) &timeout, sizeof(timeout)); + setsockopt(pfRoute, SOL_SOCKET, SO_SNDTIMEO, (char*) &timeout, sizeof(timeout)); + } + + uint32_t pid = instance.state.platform.pid; + + struct { + struct rt_msghdr hdr; + struct sockaddr_in6 dst; + uint8_t data[512]; + } rtmsg = { + .hdr = { + .rtm_type = RTM_GET, + .rtm_flags = RTF_UP | RTF_GATEWAY, + .rtm_version = RTM_VERSION, + .rtm_addrs = RTA_DST | RTA_IFP, + .rtm_msglen = sizeof(rtmsg.hdr) + sizeof(rtmsg.dst), + .rtm_pid = (pid_t) pid, + .rtm_seq = 2, + }, + .dst = { + .sin6_family = AF_INET6, +#ifndef __sun + .sin6_len = sizeof(rtmsg.dst), +#endif + }, + }; + + if (send(pfRoute, &rtmsg, rtmsg.hdr.rtm_msglen, 0) != rtmsg.hdr.rtm_msglen) { + return false; + } + + bool gotResponse = false; + while (recv(pfRoute, &rtmsg, sizeof(rtmsg), 0) > 0) { + if (rtmsg.hdr.rtm_seq == 2 && rtmsg.hdr.rtm_pid == (pid_t) pid) { + gotResponse = true; + break; + } + } + if (!gotResponse) { + return false; + } + +#ifndef __sun // On Solaris, the RTF_GATEWAY flag is not set for default routes for some reason + if ((rtmsg.hdr.rtm_flags & (RTF_UP | RTF_GATEWAY)) == (RTF_UP | RTF_GATEWAY)) +#endif + { + struct sockaddr_dl* sdl = (struct sockaddr_dl*) get_rt_address(&rtmsg.hdr, RTA_IFP); + if (sdl +#ifndef __sun + && sdl->sdl_len +#endif + && sdl->sdl_family == AF_LINK) { + if (sdl->sdl_nlen > IF_NAMESIZE) { + return false; + } + memcpy(result->ifName, sdl->sdl_data, sdl->sdl_nlen); + result->ifName[sdl->sdl_nlen] = '\0'; + result->ifIndex = sdl->sdl_index; + + return true; + } + return false; + } + + return false; +} diff --git a/src/common/impl/netif_bsd.c b/src/common/impl/netif_bsd.c new file mode 100644 index 0000000..2b59c6d --- /dev/null +++ b/src/common/impl/netif_bsd.c @@ -0,0 +1,124 @@ +#include "common/netif.h" +#include "common/io.h" +#include "common/mallocHelper.h" + +#include <net/if.h> +#include <net/if_dl.h> +#include <net/route.h> +#include <netinet/in.h> +#include <sys/socket.h> +#include <sys/sysctl.h> + +#define ROUNDUP2(a, n) ((a) > 0 ? (1 + (((a) - 1U) | ((n) - 1))) : (n)) + +#if __DragonFly__ + // https://github.com/DragonFlyBSD/DragonFlyBSD/blob/cf0aa2f1e47a3f0a6055fe427563cb3f3e627064/sys/net/route.h#L315C9-L315C19 + #define ROUNDUP(a) ROUNDUP2((a), sizeof(long)) +#elif __FreeBSD__ + // https://github.com/freebsd/freebsd-src/blob/e4c0ecba44b20ebb2e4d80978c2cb6d16b730cb9/sys/net/route.h#L368C9-L368C16 + #define ROUNDUP(a) ROUNDUP2((a), sizeof(long)) +#elif __NetBSD__ + // https://github.com/NetBSD/src/blob/29beb637d057520c0ed37ac2cde966f7cc0cadf4/sys/net/route.h#L330 + #define ROUNDUP(a) ROUNDUP2((a), sizeof(uint64_t)) +#elif __OpenBSD__ + // https://github.com/openbsd/src/blob/ca647cfa4ec3ccb8360714bc0ebc32a394f7fb6a/regress/sys/netinet/bindconnect/bindconnect.c#L250 + #define ROUNDUP(a) ROUNDUP2((a), sizeof(long)) +#else + #error unknown platform +#endif + +static struct sockaddr* +get_rt_address(struct rt_msghdr* rtm, int desired) { + struct sockaddr* sa = (struct sockaddr*) (rtm + 1); + + for (int i = 0; i < RTAX_MAX; i++) { + if (rtm->rtm_addrs & (1 << i)) { + if ((1 << i) == desired) { + return sa; + } + sa = (struct sockaddr*) (ROUNDUP(sa->sa_len) + (char*) sa); + } + } + return NULL; +} + +bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) { + int mib[6] = { CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_FLAGS, RTF_GATEWAY }; + size_t needed; + + if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0 || needed == 0) { + return false; + } + + FF_AUTO_FREE char* buf = malloc(needed); + + if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0) { + return false; + } + + char* lim = buf + needed; + struct rt_msghdr* rtm; + for (char* next = buf; next < lim; next += rtm->rtm_msglen) { + rtm = (struct rt_msghdr*) next; + struct sockaddr* sa = (struct sockaddr*) (rtm + 1); + + if ((rtm->rtm_flags & RTF_GATEWAY) && !(rtm->rtm_flags & RTF_REJECT) && (sa->sa_family == AF_INET)) { + struct sockaddr_dl* sdl = (struct sockaddr_dl*) get_rt_address(rtm, RTA_IFP); + if (sdl && sdl->sdl_family == AF_LINK) { + if (sdl->sdl_nlen > IF_NAMESIZE) { + continue; + } + memcpy(result->ifName, sdl->sdl_data, sdl->sdl_nlen); + result->ifName[sdl->sdl_nlen] = '\0'; + result->ifIndex = sdl->sdl_index; + + // Get the preferred source address + struct sockaddr_in* src = (struct sockaddr_in*) get_rt_address(rtm, RTA_IFA); + if (src && src->sin_family == AF_INET) { + result->preferredSourceAddrV4 = src->sin_addr.s_addr; + } + + return true; + } + } + } + return false; +} + +bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) { + int mib[6] = { CTL_NET, PF_ROUTE, 0, AF_INET6, NET_RT_FLAGS, RTF_GATEWAY }; + size_t needed; + + if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0 || needed == 0) { + return false; + } + + FF_AUTO_FREE char* buf = malloc(needed); + + if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0) { + return false; + } + + char* lim = buf + needed; + struct rt_msghdr* rtm; + for (char* next = buf; next < lim; next += rtm->rtm_msglen) { + rtm = (struct rt_msghdr*) next; + struct sockaddr* sa = (struct sockaddr*) (rtm + 1); + + if ((rtm->rtm_flags & RTF_GATEWAY) && !(rtm->rtm_flags & RTF_REJECT) && (sa->sa_family == AF_INET6)) { + struct sockaddr_dl* sdl = (struct sockaddr_dl*) get_rt_address(rtm, RTA_IFP); + if (sdl && sdl->sdl_family == AF_LINK) { + if (sdl->sdl_nlen > IF_NAMESIZE) { + continue; + } + memcpy(result->ifName, sdl->sdl_data, sdl->sdl_nlen); + result->ifName[sdl->sdl_nlen] = '\0'; + result->ifIndex = sdl->sdl_index; + + return true; + } + } + } + + return false; +} diff --git a/src/common/impl/netif_gnu.c b/src/common/impl/netif_gnu.c new file mode 100644 index 0000000..d2feed5 --- /dev/null +++ b/src/common/impl/netif_gnu.c @@ -0,0 +1,36 @@ +#include "common/netif.h" +#include "common/io.h" + +#include <net/if.h> +#include <stdio.h> + +#define FF_STR_INDIR(x) #x +#define FF_STR(x) FF_STR_INDIR(x) + +bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) { + FILE* FF_AUTO_CLOSE_FILE netRoute = fopen("/proc/route", "r"); + + if (!netRoute) { + return false; + } + + // skip first line + FF_UNUSED(fscanf(netRoute, "%*[^\n]\n")); + unsigned long long destination; //, gateway, flags, refCount, use, metric, mask, mtu, ... + while (fscanf(netRoute, "%" FF_STR(IF_NAMESIZE) "s%llx%*[^\n]", result->ifName, &destination) == 2) { + if (destination != 0) { + continue; + } + result->ifIndex = if_nametoindex(result->ifName); + // TODO: Get the preferred source address + return true; + } + result->ifName[0] = '\0'; + return false; +} + +bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) { + // TODO: AF_INET6 + FF_UNUSED(result); + return false; +} diff --git a/src/common/impl/netif_haiku.c b/src/common/impl/netif_haiku.c new file mode 100644 index 0000000..a32138c --- /dev/null +++ b/src/common/impl/netif_haiku.c @@ -0,0 +1,125 @@ +#include "common/netif.h" +#include "common/io.h" +#include "common/mallocHelper.h" + +#include <arpa/inet.h> +#include <net/if.h> +#include <net/route.h> +#include <sys/socket.h> +#include <sys/sockio.h> +#include <stdio.h> + +// loosely based on Haiku's src/bin/network/route/route.cpp + +bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) { + FF_AUTO_CLOSE_FD int pfRoute = socket(AF_INET, SOCK_RAW, AF_INET); + if (pfRoute < 0) { + return false; + } + + struct ifconf config; + config.ifc_len = sizeof(config.ifc_value); + if (ioctl(pfRoute, SIOCGRTSIZE, &config, sizeof(struct ifconf)) < 0) { + return false; + } + + int size = config.ifc_value; + if (size <= 0) { + return false; + } + + FF_AUTO_FREE void* buffer = malloc((size_t) size); + if (buffer == NULL) { + return false; + } + + config.ifc_len = size; + config.ifc_buf = buffer; + if (ioctl(pfRoute, SIOCGRTTABLE, &config, sizeof(struct ifconf)) < 0) { + return false; + } + + struct ifreq* interface = (struct ifreq*) buffer; + struct ifreq* end = (struct ifreq*) ((uint8_t*) buffer + size); + + while (interface < end) { + if (interface->ifr_route.flags & RTF_DEFAULT) { + // interface->ifr_metric? + strlcpy(result->ifName, interface->ifr_name, IF_NAMESIZE); + result->ifIndex = if_nametoindex(interface->ifr_name); + if (interface->ifr_route.source) { + result->preferredSourceAddrV4 = ((struct sockaddr_in*) interface->ifr_route.source)->sin_addr.s_addr; + } + return true; + } + + size_t addressSize = 0; + if (interface->ifr_route.destination != NULL) { + addressSize += interface->ifr_route.destination->sa_len; + } + if (interface->ifr_route.mask != NULL) { + addressSize += interface->ifr_route.mask->sa_len; + } + if (interface->ifr_route.gateway != NULL) { + addressSize += interface->ifr_route.gateway->sa_len; + } + + interface = (struct ifreq*) ((addr_t) interface + IF_NAMESIZE + sizeof(struct route_entry) + addressSize); + } + + return false; +} + +bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) { + FF_AUTO_CLOSE_FD int pfRoute = socket(AF_INET6, SOCK_RAW, AF_INET6); + if (pfRoute < 0) { + return false; + } + + struct ifconf config; + config.ifc_len = sizeof(config.ifc_value); + if (ioctl(pfRoute, SIOCGRTSIZE, &config, sizeof(struct ifconf)) < 0) { + return false; + } + + int size = config.ifc_value; + if (size <= 0) { + return false; + } + + FF_AUTO_FREE void* buffer = malloc((size_t) size); + if (buffer == NULL) { + return false; + } + + config.ifc_len = size; + config.ifc_buf = buffer; + if (ioctl(pfRoute, SIOCGRTTABLE, &config, sizeof(struct ifconf)) < 0) { + return false; + } + + struct ifreq* interface = (struct ifreq*) buffer; + struct ifreq* end = (struct ifreq*) ((uint8_t*) buffer + size); + + while (interface < end) { + if (interface->ifr_route.flags & RTF_DEFAULT) { + strlcpy(result->ifName, interface->ifr_name, IF_NAMESIZE); + result->ifIndex = if_nametoindex(interface->ifr_name); + return true; + } + + size_t addressSize = 0; + if (interface->ifr_route.destination != NULL) { + addressSize += interface->ifr_route.destination->sa_len; + } + if (interface->ifr_route.mask != NULL) { + addressSize += interface->ifr_route.mask->sa_len; + } + if (interface->ifr_route.gateway != NULL) { + addressSize += interface->ifr_route.gateway->sa_len; + } + + interface = (struct ifreq*) ((addr_t) interface + IF_NAMESIZE + sizeof(struct route_entry) + addressSize); + } + return false; +} diff --git a/src/common/impl/netif_linux.c b/src/common/impl/netif_linux.c new file mode 100644 index 0000000..67e4868 --- /dev/null +++ b/src/common/impl/netif_linux.c @@ -0,0 +1,443 @@ +#include "common/netif.h" +#include "common/io.h" +#include "common/mallocHelper.h" +#include "common/debug.h" + +#include <arpa/inet.h> +#include <linux/rtnetlink.h> +#include <net/if.h> + +static uint32_t ffNetifGetNetlinkPortId(int sock_fd) { + struct sockaddr_nl addr = {}; + socklen_t addrLen = sizeof(addr); + if (getsockname(sock_fd, (struct sockaddr*) &addr, &addrLen) < 0) { + FF_DEBUG("Failed to query netlink socket address (use PID instead): %s", strerror(errno)); + return instance.state.platform.pid; + } else { + FF_DEBUG("Netlink port ID: %u", addr.nl_pid); + return addr.nl_pid; + } +} + +bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) { + FF_DEBUG("Starting IPv4 default route detection"); + + FF_AUTO_CLOSE_FD int sock_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (sock_fd < 0) { + FF_DEBUG("Failed to create netlink socket: %s", strerror(errno)); + return false; + } + FF_DEBUG("Created netlink socket: fd=%d", sock_fd); + + // Bind socket + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel choose PID + .nl_groups = 0, // No multicast groups + }; + + if (bind(sock_fd, (struct sockaddr*) &addr, sizeof(addr)) < 0) { + FF_DEBUG("Failed to bind socket: %s", strerror(errno)); + return false; + } + FF_DEBUG("Successfully bound socket"); + + uint32_t pid = ffNetifGetNetlinkPortId(sock_fd); + + struct FF_A_PACKED { + struct nlmsghdr nlh; + struct rtmsg rtm; + struct rtattr rta; + uint32_t table; + } req = { + // Netlink message header + .nlh = { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_GETROUTE, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .nlmsg_seq = 1, + .nlmsg_pid = pid, + }, + // Route message + .rtm = { + .rtm_family = AF_INET, + .rtm_dst_len = 0, // Match all destinations + .rtm_src_len = 0, // Match all sources + .rtm_tos = 0, + .rtm_table = RT_TABLE_UNSPEC, + .rtm_protocol = RTPROT_UNSPEC, + .rtm_scope = RT_SCOPE_UNIVERSE, + .rtm_type = RTN_UNSPEC, + .rtm_flags = 0, + }, + // Route attribute for main table + .rta = { + .rta_len = RTA_LENGTH(sizeof(uint32_t)), + .rta_type = RTA_TABLE, + }, + .table = RT_TABLE_MAIN, + }; + + struct sockaddr_nl dest_addr = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Kernel + .nl_groups = 0, // No multicast groups + }; + + ssize_t sent = sendto(sock_fd, &req, sizeof(req), 0, (struct sockaddr*) &dest_addr, sizeof(dest_addr)); + + if (sent != sizeof(req)) { + FF_DEBUG("Failed to send netlink request: sent=%zd, expected=%zu", sent, sizeof(req)); + return false; + } + FF_DEBUG("Sent netlink request: %zd bytes", sent); + + struct sockaddr_nl src_addr = {}; + socklen_t src_addr_len = sizeof(src_addr); + + uint8_t buffer[1024 * 16]; // 16 KB buffer should be sufficient + uint32_t minMetric = UINT32_MAX; + FF_A_UNUSED int routeCount = 0; + + while (true) { + ssize_t received = recvfrom(sock_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &src_addr, &src_addr_len); + + if (received < 0) { + FF_DEBUG("Failed to receive netlink response: %s", strerror(errno)); + return false; + } + + if (received >= (ssize_t) sizeof(buffer)) { + FF_DEBUG("Received truncated message: received %zd, bufsize %zu", received, sizeof(buffer)); + return false; + } + FF_DEBUG("Received netlink response: %zd bytes", received); + if (received == 0) { + FF_DEBUG("Received zero-length netlink response, ending processing"); + break; + } + + struct { + uint32_t metric; + uint32_t ifindex; + uint32_t prefsrc; + } entry; + + for (const struct nlmsghdr* nlh = (struct nlmsghdr*) buffer; + NLMSG_OK(nlh, received); + nlh = NLMSG_NEXT(nlh, received)) { + if (nlh->nlmsg_seq != 1 || nlh->nlmsg_pid != pid) { + continue; + } + if (nlh->nlmsg_type == NLMSG_DONE) { + FF_DEBUG("Received NLMSG_DONE, processed %d routes", routeCount); + goto exit; + } + + if (nlh->nlmsg_type == NLMSG_ERROR) { + FF_DEBUG("Netlink reports error: %s", strerror(-((struct nlmsgerr*) NLMSG_DATA(nlh))->error)); + continue; + } + + if (nlh->nlmsg_type != RTM_NEWROUTE) { + FF_DEBUG("Skipping non-route message: type=%d", nlh->nlmsg_type); + continue; + } + + routeCount++; + struct rtmsg* rtm = (struct rtmsg*) NLMSG_DATA(nlh); + if (rtm->rtm_family != AF_INET) { + FF_DEBUG("Skipping non-IPv4 route #%d (family=%d)", routeCount, rtm->rtm_family); + continue; + } + + if (rtm->rtm_dst_len != 0) { + FF_DEBUG("Skipping non-default route #%d (dst_len=%d)", routeCount, rtm->rtm_dst_len); + continue; + } + + // Skip local/loopback routes + if (rtm->rtm_scope == RT_SCOPE_HOST || rtm->rtm_type == RTN_LOCAL) { + FF_DEBUG("Skipping local route #%d (scope=%d, type=%d)", routeCount, rtm->rtm_scope, rtm->rtm_type); + continue; + } + + FF_DEBUG("Processing IPv4 default route candidate #%d", routeCount); + entry = (__typeof__(entry)) {}; // Default to zero metric (no RTA_PRIORITY found) + + // Parse route attributes + size_t rtm_len = RTM_PAYLOAD(nlh); + for (struct rtattr* rta = RTM_RTA(rtm); + RTA_OK(rta, rtm_len); + rta = RTA_NEXT(rta, rtm_len)) { + if (RTA_PAYLOAD(rta) < sizeof(uint32_t)) { + continue; // Skip invalid attributes + } + + uint32_t rta_data = *(uint32_t*) RTA_DATA(rta); + switch (rta->rta_type) { + case RTA_DST: + FF_DEBUG("Unexpected RTA_DST: %s (len=%u)", inet_ntoa((struct in_addr) { .s_addr = rta_data }), rtm->rtm_dst_len); + goto next; + case RTA_OIF: + entry.ifindex = rta_data; + FF_DEBUG("Found interface index: %u", entry.ifindex); + break; + case RTA_GATEWAY: + FF_DEBUG("Found gateway: %s", inet_ntoa(*(struct in_addr*) &rta_data)); + if (rta_data == 0) { + goto next; + } + break; + case RTA_PRIORITY: + FF_DEBUG("Found metric: %u", rta_data); + if (rta_data >= minMetric) { + goto next; + } + entry.metric = rta_data; + break; + case RTA_PREFSRC: + entry.prefsrc = rta_data; + FF_DEBUG("Found preferred source: %s", inet_ntoa(*(struct in_addr*) &rta_data)); + break; + } + } + + if (entry.ifindex == 0 || entry.metric >= minMetric) { + next: + FF_DEBUG("Skipping route: ifindex=%u, metric=%u", entry.ifindex, entry.metric); + continue; + } + minMetric = entry.metric; + result->ifIndex = entry.ifindex; + FF_DEBUG("Updated best route: ifindex=%u, metric=%u, prefsrc=%x", entry.ifindex, entry.metric, entry.prefsrc); + result->preferredSourceAddrV4 = entry.prefsrc; + if (minMetric == 0) { + FF_DEBUG("Found zero metric route, stopping further processing"); + break; // Stop processing if we found a zero metric route + } + } + } + +exit: + if (minMetric < UINT32_MAX) { + if_indextoname(result->ifIndex, result->ifName); + FF_DEBUG("Found default IPv4 route: interface=%s, index=%u, metric=%u", result->ifName, result->ifIndex, minMetric); + return true; + } + FF_DEBUG("No IPv4 default route found"); + return false; +} + +bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) { + FF_DEBUG("Starting IPv6 default route detection"); + + FF_AUTO_CLOSE_FD int sock_fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (sock_fd < 0) { + FF_DEBUG("Failed to create netlink socket: %s", strerror(errno)); + return false; + } + FF_DEBUG("Created netlink socket: fd=%d", sock_fd); + + // Bind socket + struct sockaddr_nl addr = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Let kernel choose PID + .nl_groups = 0, // No multicast groups + }; + + if (bind(sock_fd, (struct sockaddr*) &addr, sizeof(addr)) < 0) { + FF_DEBUG("Failed to bind socket: %s", strerror(errno)); + return false; + } + FF_DEBUG("Successfully bound socket"); + + uint32_t pid = ffNetifGetNetlinkPortId(sock_fd); + + struct FF_A_PACKED { + struct nlmsghdr nlh; + struct rtmsg rtm; + struct rtattr rta; + uint32_t table; + } req = { + // Netlink message header + .nlh = { + .nlmsg_len = sizeof(req), + .nlmsg_type = RTM_GETROUTE, + .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP, + .nlmsg_seq = 1, + .nlmsg_pid = pid, + }, + // Route message + .rtm = { + .rtm_family = AF_INET6, // IPv6 instead of IPv4 + .rtm_dst_len = 0, // Match all destinations + .rtm_src_len = 0, // Match all sources + .rtm_tos = 0, + .rtm_table = RT_TABLE_UNSPEC, + .rtm_protocol = RTPROT_UNSPEC, + .rtm_scope = RT_SCOPE_UNIVERSE, + .rtm_type = RTN_UNSPEC, + .rtm_flags = 0, + }, + // Route attribute for main table + .rta = { + .rta_len = RTA_LENGTH(sizeof(uint32_t)), + .rta_type = RTA_TABLE, + }, + .table = RT_TABLE_MAIN, + }; + + struct sockaddr_nl dest_addr = { + .nl_family = AF_NETLINK, + .nl_pid = 0, // Kernel + .nl_groups = 0, // No multicast groups + }; + + ssize_t sent = sendto(sock_fd, &req, sizeof(req), 0, (struct sockaddr*) &dest_addr, sizeof(dest_addr)); + + if (sent != sizeof(req)) { + FF_DEBUG("Failed to send netlink request: sent=%zd, expected=%zu", sent, sizeof(req)); + return false; + } + FF_DEBUG("Sent netlink request: %zd bytes", sent); + + struct sockaddr_nl src_addr = {}; + socklen_t src_addr_len = sizeof(src_addr); + + uint8_t buffer[1024 * 16]; // 16 KB buffer should be sufficient + uint32_t minMetric = UINT32_MAX; + FF_A_UNUSED int routeCount = 0; + + while (true) { + ssize_t received = recvfrom(sock_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &src_addr, &src_addr_len); + + if (received < 0) { + FF_DEBUG("Failed to receive netlink response: %s", strerror(errno)); + return false; + } + + if (received >= (ssize_t) sizeof(buffer)) { + FF_DEBUG("Received truncated message: received %zd, bufsize %zu", received, sizeof(buffer)); + return false; + } + FF_DEBUG("Received netlink response: %zd bytes", received); + if (received == 0) { + FF_DEBUG("Received zero-length netlink response, ending processing"); + break; + } + + struct { + uint32_t metric; + uint32_t ifindex; + } entry; + + for (const struct nlmsghdr* nlh = (struct nlmsghdr*) buffer; + NLMSG_OK(nlh, received); + nlh = NLMSG_NEXT(nlh, received)) { + if (nlh->nlmsg_seq != 1 || nlh->nlmsg_pid != pid) { + continue; + } + if (nlh->nlmsg_type == NLMSG_DONE) { + FF_DEBUG("Received NLMSG_DONE, processed %d routes", routeCount); + goto exit; + } + + if (nlh->nlmsg_type == NLMSG_ERROR) { + FF_DEBUG("Netlink reports error: %s", strerror(-((struct nlmsgerr*) NLMSG_DATA(nlh))->error)); + continue; + } + + if (nlh->nlmsg_type != RTM_NEWROUTE) { + FF_DEBUG("Skipping non-route message: type=%d", nlh->nlmsg_type); + continue; + } + + routeCount++; + struct rtmsg* rtm = (struct rtmsg*) NLMSG_DATA(nlh); + if (rtm->rtm_family != AF_INET6) { + FF_DEBUG("Skipping non-IPv6 route #%d (family=%d)", routeCount, rtm->rtm_family); + continue; + } + + if (rtm->rtm_dst_len != 0) { + FF_DEBUG("Skipping non-default route #%d (dst_len=%d)", routeCount, rtm->rtm_dst_len); + continue; + } + + // Skip local/loopback routes + if (rtm->rtm_scope == RT_SCOPE_HOST || rtm->rtm_type == RTN_LOCAL) { + FF_DEBUG("Skipping local route #%d (scope=%d, type=%d)", routeCount, rtm->rtm_scope, rtm->rtm_type); + continue; + } + + FF_DEBUG("Processing IPv6 default route candidate #%d", routeCount); + entry = (__typeof__(entry)) {}; // Default to zero metric (no RTA_PRIORITY found) + + // Parse route attributes + size_t rtm_len = RTM_PAYLOAD(nlh); + for (struct rtattr* rta = RTM_RTA(rtm); + RTA_OK(rta, rtm_len); + rta = RTA_NEXT(rta, rtm_len)) { + switch (rta->rta_type) { + case RTA_DST: + if (RTA_PAYLOAD(rta) >= sizeof(struct in6_addr)) { + FF_A_UNUSED char str[INET6_ADDRSTRLEN]; + FF_DEBUG("Unexpected RTA_DST: %s", inet_ntop(AF_INET6, RTA_DATA(rta), str, sizeof(str))); + goto next; + } + break; + case RTA_OIF: + if (RTA_PAYLOAD(rta) >= sizeof(uint32_t)) { + entry.ifindex = *(uint32_t*) RTA_DATA(rta); + FF_DEBUG("Found interface index: %u", entry.ifindex); + } + break; + case RTA_GATEWAY: + if (RTA_PAYLOAD(rta) >= sizeof(struct in6_addr)) { + struct in6_addr* gw = (struct in6_addr*) RTA_DATA(rta); + if (IN6_IS_ADDR_UNSPECIFIED(gw)) { + goto next; + } + FF_A_UNUSED char str[INET6_ADDRSTRLEN]; + FF_DEBUG("Found gateway: %s", inet_ntop(AF_INET6, gw, str, sizeof(str))); + } + break; + case RTA_PRIORITY: + if (RTA_PAYLOAD(rta) >= sizeof(uint32_t)) { + uint32_t metric = *(uint32_t*) RTA_DATA(rta); + FF_DEBUG("Found metric: %u", metric); + if (metric >= minMetric) { + goto next; + } + entry.metric = metric; + } + break; + } + } + + if (entry.ifindex == 0 || entry.metric >= minMetric) { + next: + FF_DEBUG("Skipping route: ifindex=%u, metric=%u", entry.ifindex, entry.metric); + continue; + } + minMetric = entry.metric; + result->ifIndex = entry.ifindex; + FF_DEBUG("Updated best route: ifindex=%u, metric=%u", entry.ifindex, entry.metric); + + if (minMetric == 0) { + FF_DEBUG("Found zero metric route, stopping further processing"); + break; // Stop processing if we found a zero metric route + } + } + } + +exit: + if (minMetric < UINT32_MAX) { + if_indextoname(result->ifIndex, result->ifName); + FF_DEBUG("Found default IPv6 route: interface=%s, index=%u, metric=%u", result->ifName, result->ifIndex, minMetric); + return true; + } + FF_DEBUG("No IPv6 default route found"); + return false; +} diff --git a/src/common/impl/netif_windows.c b/src/common/impl/netif_windows.c new file mode 100644 index 0000000..7531ae0 --- /dev/null +++ b/src/common/impl/netif_windows.c @@ -0,0 +1,92 @@ +#include "common/netif.h" + +#include <ws2tcpip.h> // AF_INET6, IN6_IS_ADDR_UNSPECIFIED +#include <iphlpapi.h> + +bool ffNetifGetDefaultRouteImplV4(FFNetifDefaultRouteResult* result) { + PMIB_IPFORWARD_TABLE2 pIpForwardTable = NULL; + + if (!NETIO_SUCCESS(GetIpForwardTable2(AF_INET, &pIpForwardTable))) { + return false; + } + + bool foundDefault = false; + uint32_t smallestMetric = UINT32_MAX; + + for (ULONG i = 0; i < pIpForwardTable->NumEntries; ++i) { + MIB_IPFORWARD_ROW2* row = &pIpForwardTable->Table[i]; + + if (row->DestinationPrefix.PrefixLength == 0 && + row->DestinationPrefix.Prefix.Ipv4.sin_addr.S_un.S_addr == 0) { + MIB_IF_ROW2 ifRow = { + .InterfaceIndex = row->InterfaceIndex, + }; + if (NETIO_SUCCESS(GetIfEntry2(&ifRow)) && ifRow.OperStatus == IfOperStatusUp) { + MIB_IPINTERFACE_ROW ipInterfaceRow = { + .Family = AF_INET, + .InterfaceIndex = row->InterfaceIndex, + }; + + uint32_t realMetric = row->Metric /* Metric offset */; + + if (NETIO_SUCCESS(GetIpInterfaceEntry(&ipInterfaceRow))) { + realMetric += ipInterfaceRow.Metric /* Interface metric */; + } + + if (realMetric < smallestMetric) { + smallestMetric = realMetric; + result->ifIndex = row->InterfaceIndex; + foundDefault = true; + } + } + } + } + + FreeMibTable(pIpForwardTable); + + return foundDefault; +} + +bool ffNetifGetDefaultRouteImplV6(FFNetifDefaultRouteResult* result) { + PMIB_IPFORWARD_TABLE2 pIpForwardTable = NULL; + + if (!NETIO_SUCCESS(GetIpForwardTable2(AF_INET6, &pIpForwardTable))) { + return false; + } + + bool foundDefault = false; + uint32_t smallestMetric = UINT32_MAX; + + for (ULONG i = 0; i < pIpForwardTable->NumEntries; ++i) { + MIB_IPFORWARD_ROW2* row = &pIpForwardTable->Table[i]; + + if (row->DestinationPrefix.PrefixLength == 0 && + IN6_IS_ADDR_UNSPECIFIED(&row->DestinationPrefix.Prefix.Ipv6.sin6_addr)) { + MIB_IF_ROW2 ifRow = { + .InterfaceIndex = row->InterfaceIndex, + }; + if (NETIO_SUCCESS(GetIfEntry2(&ifRow)) && ifRow.OperStatus == IfOperStatusUp) { + MIB_IPINTERFACE_ROW ipInterfaceRow = { + .Family = AF_INET6, + .InterfaceIndex = row->InterfaceIndex, + }; + + uint32_t realMetric = row->Metric /* Metric offset */; + + if (NETIO_SUCCESS(GetIpInterfaceEntry(&ipInterfaceRow))) { + realMetric += ipInterfaceRow.Metric /* Interface metric */; + } + + if (realMetric < smallestMetric) { + smallestMetric = realMetric; + result->ifIndex = row->InterfaceIndex; + foundDefault = true; + } + } + } + } + + FreeMibTable(pIpForwardTable); + + return foundDefault; +} diff --git a/src/common/impl/networking_common.c b/src/common/impl/networking_common.c new file mode 100644 index 0000000..e63979b --- /dev/null +++ b/src/common/impl/networking_common.c @@ -0,0 +1,192 @@ +#include "fastfetch.h" +#include "common/library.h" +#include "common/networking.h" +#include "common/strutil.h" +#include "common/debug.h" + +#ifdef FF_HAVE_ZLIB + #include <zlib.h> + +struct FFZlibLibrary { + FF_LIBRARY_SYMBOL(inflateInit2_) + FF_LIBRARY_SYMBOL(inflate) + FF_LIBRARY_SYMBOL(inflateEnd) + + bool inited; +} zlibData; + +const char* ffNetworkingLoadZlibLibrary(void) { + if (!zlibData.inited) { + zlibData.inited = true; + FF_LIBRARY_LOAD_MESSAGE(zlib, + #ifdef _WIN32 + "zlib1" + #else + "libz" + #endif + FF_LIBRARY_EXTENSION, + 2) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateInit2_) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflate) + FF_LIBRARY_LOAD_SYMBOL_VAR_MESSAGE(zlib, zlibData, inflateEnd) + zlib = NULL; // don't auto dlclose + } + return zlibData.ffinflateEnd == NULL ? "Failed to load libz" : NULL; +} + +// Try to pre-read gzip header to determine uncompressed size +static uint32_t guessGzipOutputSize(const void* data, uint32_t dataSize) { + // gzip file format: http://www.zlib.org/rfc-gzip.html + if (dataSize < 10 || ((const uint8_t*) data)[0] != 0x1f || ((const uint8_t*) data)[1] != 0x8b) { + return 0; + } + + // Uncompressed size in gzip format is stored in the last 4 bytes, but only valid if data is less than 4GB + if (dataSize > 18) { + // Get ISIZE value from the end of file (little endian) + const uint8_t* tail = (const uint8_t*) data + dataSize - 4; + uint32_t uncompressedSize = (uint32_t) tail[0] | ((uint32_t) tail[1] << 8u) | ((uint32_t) tail[2] << 16u) | ((uint32_t) tail[3] << 24u); + + // For valid gzip files, this value is the length of the uncompressed data modulo 2^32 + if (uncompressedSize > 0) { + FF_DEBUG("Read uncompressed size from GZIP trailer: %u bytes", uncompressedSize); + // Add some margin to the estimated size for safety + return uncompressedSize + 64; + } + } + + // If unable to get size from trailer or size is 0, use estimated value + // Typically, text data compression ratio is between 3-5x, we use the larger value + uint32_t estimatedSize = dataSize * 5; + FF_DEBUG("Unable to read exact uncompressed size, estimated as 5x of compressed data: %u bytes", estimatedSize); + return estimatedSize; +} + +// Decompress gzip content +bool ffNetworkingDecompressGzip(FFstrbuf* buffer, char* headerEnd) { + assert(headerEnd != NULL && *headerEnd == '\r'); + + // Calculate header size + uint32_t headerSize = (uint32_t) (headerEnd - buffer->chars); + + *headerEnd = '\0'; // Replace delimiter with null character for easier processing + // Ensure Content-Encoding is in response headers, not in response body + bool hasGzip = strcasestr(buffer->chars, "\nContent-Encoding: gzip") != NULL; + *headerEnd = '\r'; // Restore delimiter + + if (!hasGzip) { + FF_DEBUG("No gzip compressed content detected, skipping decompression"); + return true; + } + + FF_DEBUG("Gzip compressed content detected, preparing for decompression"); + + const char* bodyStart = headerEnd + 4; // Skip delimiter + + if (buffer->length <= headerSize + 4) { + // No content to decompress + FF_DEBUG("Compressed content size is 0, skipping decompression"); + return true; + } + + // Calculate compressed content size + uint32_t compressedSize = buffer->length - headerSize - 4; + + // Check if content is actually in gzip format (gzip header magic is 0x1f 0x8b) + if (compressedSize < 2 || (uint8_t) bodyStart[0] != 0x1f || (uint8_t) bodyStart[1] != 0x8b) { + FF_DEBUG("Content is not valid gzip format, skipping decompression"); + return false; + } + + // Predict uncompressed size + uint32_t estimatedSize = guessGzipOutputSize(bodyStart, compressedSize); + + // Create decompression buffer with estimated size + FF_STRBUF_AUTO_DESTROY decompressedBuffer = ffStrbufCreateA(estimatedSize > 0 ? estimatedSize : compressedSize * 5); + FF_DEBUG("Created decompression buffer: %u bytes", decompressedBuffer.allocated); + + // Initialize decompression + z_stream zs = { + .zalloc = Z_NULL, + .zfree = Z_NULL, + .opaque = Z_NULL, + .avail_in = (uInt) compressedSize, + .next_in = (Bytef*) bodyStart, + .avail_out = (uInt) ffStrbufGetFree(&decompressedBuffer), + .next_out = (Bytef*) decompressedBuffer.chars, + }; + + // Initialize decompression engine + if (zlibData.ffinflateInit2_(&zs, 16 + MAX_WBITS, ZLIB_VERSION, (int) sizeof(z_stream)) != Z_OK) { + FF_DEBUG("Failed to initialize decompression engine"); + return false; + } + uInt availableOut = zs.avail_out; + + // Perform decompression + int result = zlibData.ffinflate(&zs, Z_FINISH); + + // If output buffer is insufficient, try to extend buffer + while (result == Z_BUF_ERROR || (result != Z_STREAM_END && zs.avail_out == 0)) { + FF_DEBUG("Output buffer insufficient, trying to extend"); + + // Save already decompressed data amount + uint32_t alreadyDecompressed = (uint32_t) (availableOut - zs.avail_out); + decompressedBuffer.length += alreadyDecompressed; + decompressedBuffer.chars[decompressedBuffer.length] = '\0'; + + ffStrbufEnsureFree(&decompressedBuffer, decompressedBuffer.length / 2); + + // Set output parameters to point to new buffer + zs.avail_out = (uInt) ffStrbufGetFree(&decompressedBuffer); + zs.next_out = (Bytef*) (decompressedBuffer.chars + decompressedBuffer.length); + availableOut = zs.avail_out; + + // Decompress again + result = zlibData.ffinflate(&zs, Z_FINISH); + } + + // Check for decompression errors before using result + if (result != Z_STREAM_END) { + FF_DEBUG("Decompression failed with zlib error: %d", result); + zlibData.ffinflateEnd(&zs); + return false; + } + + zlibData.ffinflateEnd(&zs); + + // Calculate decompressed size (from the last inflate call) + uint32_t decompressedSize = (uint32_t) (availableOut - zs.avail_out); + decompressedBuffer.length += decompressedSize; + decompressedBuffer.chars[decompressedBuffer.length] = '\0'; + FF_DEBUG("Successfully decompressed %u bytes compressed data to %u bytes", compressedSize, decompressedBuffer.length); + + // Modify Content-Length header and remove Content-Encoding header + // Use decompressedBuffer.length (total) not decompressedSize (last chunk only) + FF_STRBUF_AUTO_DESTROY newBuffer = ffStrbufCreateA(headerSize + decompressedBuffer.length + 64); + + char* line = NULL; + size_t len = 0; + while (ffStrbufGetline(&line, &len, buffer)) { + if (ffStrStartsWithIgnCase(line, "Content-Encoding:")) { + continue; + } else if (ffStrStartsWithIgnCase(line, "Content-Length:")) { + ffStrbufAppendF(&newBuffer, "Content-Length: %u\r\n", decompressedBuffer.length); + continue; + } else if (line[0] == '\r') { + ffStrbufAppendS(&newBuffer, "\r\n"); + ffStrbufGetlineRestore(&line, &len, buffer); + break; + } + + ffStrbufAppendS(&newBuffer, line); // Including the trailing \r + ffStrbufAppendC(&newBuffer, '\n'); + } + + ffStrbufAppend(&newBuffer, &decompressedBuffer); + ffStrbufDestroy(buffer); + ffStrbufInitMove(buffer, &newBuffer); + + return true; +} +#endif // FF_HAVE_ZLIB diff --git a/src/common/impl/networking_linux.c b/src/common/impl/networking_linux.c new file mode 100644 index 0000000..e253451 --- /dev/null +++ b/src/common/impl/networking_linux.c @@ -0,0 +1,502 @@ +#include "fastfetch.h" +#include "common/networking.h" +#include "common/time.h" +#include "common/library.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" +#include "common/debug.h" + +#include <unistd.h> +#include <poll.h> +#include <sys/time.h> +#include <sys/socket.h> +#include <netdb.h> +#include <netinet/in.h> // For FreeBSD +#include <netinet/tcp.h> +#include <errno.h> +#include <fcntl.h> + +static const char* tryNonThreadingFastPath(FFNetworkingState* state) { +#if defined(TCP_FASTOPEN) || __APPLE__ + + if (!state->tfo) { + #if __linux__ || __GNU__ + // Linux doesn't support sendto() on unconnected sockets + FF_DEBUG("TCP Fast Open disabled, skipping"); + return "TCP Fast Open disabled"; + #endif + } else { + FF_DEBUG("Attempting to use TCP Fast Open to connect"); + + #ifndef __APPLE__ // On macOS, TCP_FASTOPEN doesn't seem to be needed + // Set TCP Fast Open + int flag = 1; + if (setsockopt(state->sockfd, IPPROTO_TCP, + #ifdef __APPLE__ + // https://github.com/rust-lang/libc/pull/3135 + 0x218 // TCP_FASTOPEN_FORCE_ENABLE + #else + TCP_FASTOPEN + #endif + , + &flag, + sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_FASTOPEN option: %s", strerror(errno)); + return "setsockopt(TCP_FASTOPEN) failed"; + } else { + #if __linux__ || __GNU__ + FF_DEBUG("Successfully set TCP_FASTOPEN option, queue length: %d", flag); + #elif defined(__APPLE__) + FF_DEBUG("Successfully set TCP_FASTOPEN_FORCE_ENABLE option"); + #else + FF_DEBUG("Successfully set TCP_FASTOPEN option"); + #endif + } + #endif + } + + #ifndef __APPLE__ + FF_DEBUG("Using sendto() + MSG_DONTWAIT to send %u bytes of data", state->command.length); + ssize_t sent = sendto(state->sockfd, + state->command.chars, + state->command.length, + #ifdef MSG_FASTOPEN + MSG_FASTOPEN | + #endif + #ifdef MSG_NOSIGNAL + MSG_NOSIGNAL | + #endif + MSG_DONTWAIT, + state->addr->ai_addr, + state->addr->ai_addrlen); + #else + if (fcntl(state->sockfd, F_SETFL, O_NONBLOCK) == -1) { + FF_DEBUG("fcntl(F_SETFL) failed: %s", strerror(errno)); + return "fcntl(F_SETFL) failed"; + } + FF_DEBUG("Using connectx() to send %u bytes of data", state->command.length); + // Use connectx to establish connection and send data in one call + size_t sent; + if (connectx(state->sockfd, + &(sa_endpoints_t) { + .sae_dstaddr = state->addr->ai_addr, + .sae_dstaddrlen = state->addr->ai_addrlen, + }, + SAE_ASSOCID_ANY, + state->tfo ? CONNECT_DATA_IDEMPOTENT : 0, + &(struct iovec) { + .iov_base = state->command.chars, + .iov_len = state->command.length, + }, + 1, + &sent, + NULL) != 0) { + sent = 0; + } + if (fcntl(state->sockfd, F_SETFL, 0) == -1) { + FF_DEBUG("fcntl(F_SETFL) failed: %s", strerror(errno)); + return "fcntl(F_SETFL) failed"; + } + #endif + if (sent > 0 || (errno == EAGAIN || errno == EWOULDBLOCK + #ifdef __APPLE__ + // On macOS EINPROGRESS means the connection cannot be completed immediately + // On Linux, it means the TFO cookie is not available locally + || errno == EINPROGRESS + #endif + )) { + FF_DEBUG( + #ifdef __APPLE__ + "connectx()" + #else + "sendto()" + #endif + " %s (sent=%zd, %s)", + errno == 0 ? "succeeded" : "was in progress", + sent, + strerror(errno)); + freeaddrinfo(state->addr); + state->addr = NULL; + ffStrbufDestroy(&state->command); + return NULL; + } + + FF_DEBUG( + #ifdef __APPLE__ + "connectx()" + #else + "sendto()" + #endif + " failed: %s", + strerror(errno)); + #ifdef __APPLE__ + return "connectx() failed"; + #else + return "sendto() failed"; + #endif +#else + FF_UNUSED(state); + return "TFO support is not available"; +#endif +} + +// Traditional connect and send function +static const char* connectAndSend(FFNetworkingState* state) { + const char* ret = NULL; + FF_DEBUG("Using traditional connection method to connect"); + + FF_DEBUG("Attempting connect() to server..."); + if (connect(state->sockfd, state->addr->ai_addr, state->addr->ai_addrlen) == -1) { + FF_DEBUG("connect() failed: %s", strerror(errno)); + ret = "connect() failed"; + goto error; + } + FF_DEBUG("connect() succeeded"); + + FF_DEBUG("Attempting to send %u bytes of data...", state->command.length); + if (send(state->sockfd, state->command.chars, state->command.length, 0) < 0) { + FF_DEBUG("send() failed: %s", strerror(errno)); + ret = "send() failed"; + goto error; + } + FF_DEBUG("Data sent successfully"); + + goto exit; + +error: + FF_DEBUG("Error occurred, closing socket"); + close(state->sockfd); + state->sockfd = -1; + +exit: + FF_DEBUG("Releasing address info and other resources"); + freeaddrinfo(state->addr); + state->addr = NULL; + ffStrbufDestroy(&state->command); + + return ret; +} + +FF_THREAD_ENTRY_DECL_WRAPPER(connectAndSend, FFNetworkingState*); + +// Parallel DNS resolution and socket creation +static const char* initNetworkingState(FFNetworkingState* state, const char* host, const char* path, const char* headers) { + FF_DEBUG("Initializing network connection state: host=%s, path=%s", host, path); + + // Initialize command and host information + ffStrbufInitA(&state->command, 128); + ffStrbufAppendS(&state->command, "GET "); + ffStrbufAppendS(&state->command, path); + ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: "); + ffStrbufAppendS(&state->command, host); + ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly tell the server we don't need to keep the connection + + // If compression needs to be enabled + if (state->compression) { + FF_DEBUG("Enabling HTTP content compression"); + ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n"); + } + + ffStrbufAppendS(&state->command, headers); + ffStrbufAppendS(&state->command, "\r\n"); + +#ifdef FF_HAVE_THREADS + state->thread = 0; + FF_DEBUG("Thread ID initialized to 0"); +#endif + + const char* ret = NULL; + + struct addrinfo hints = { + .ai_family = state->ipv6 ? AF_INET6 : AF_INET, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_NUMERICSERV + }; + + FF_DEBUG("Resolving address: %s (%s)", host, state->ipv6 ? "IPv6" : "IPv4"); + // Use AI_NUMERICSERV flag to indicate the service is a numeric port, reducing parsing time + + int gaiRes = getaddrinfo(host, "80", &hints, &state->addr); + if (gaiRes != 0) { + FF_DEBUG("getaddrinfo() failed: %s (res=%d)", gai_strerror(gaiRes), gaiRes); + ret = "getaddrinfo() failed"; + goto error; + } + FF_DEBUG("Address resolution successful"); + + FF_DEBUG("Creating socket"); + state->sockfd = socket(state->addr->ai_family, state->addr->ai_socktype, state->addr->ai_protocol); + if (state->sockfd == -1) { + FF_DEBUG("socket() failed: %s", strerror(errno)); + ret = "socket() failed"; + goto error; + } + FF_DEBUG("Socket creation successful: fd=%d", state->sockfd); + + int flag = 1; +#ifdef TCP_NODELAY + // Disable Nagle's algorithm to reduce small packet transmission delay + if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_NODELAY: %s", strerror(errno)); + } else { + FF_DEBUG("Successfully disabled Nagle's algorithm"); + } +#endif + +#ifdef TCP_QUICKACK + // Set TCP_QUICKACK option to avoid delayed acknowledgments + if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_QUICKACK, &flag, sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_QUICKACK: %s", strerror(errno)); + } else { + FF_DEBUG("Successfully enabled TCP quick acknowledgment"); + } +#endif + + if (state->timeout > 0) { + FF_DEBUG("Setting connection timeout: %u ms", state->timeout); + FF_A_UNUSED uint32_t sec = state->timeout / 1000; + if (sec == 0) { + sec = 1; + } + +#ifdef TCP_CONNECTIONTIMEOUT + FF_DEBUG("Using TCP_CONNECTIONTIMEOUT: %u seconds", sec); + setsockopt(state->sockfd, IPPROTO_TCP, TCP_CONNECTIONTIMEOUT, &sec, sizeof(sec)); +#elif defined(TCP_KEEPINIT) + FF_DEBUG("Using TCP_KEEPINIT: %u seconds", sec); + setsockopt(state->sockfd, IPPROTO_TCP, TCP_KEEPINIT, &sec, sizeof(sec)); +#elif defined(TCP_USER_TIMEOUT) + FF_DEBUG("Using TCP_USER_TIMEOUT: %u milliseconds", state->timeout); + setsockopt(state->sockfd, IPPROTO_TCP, TCP_USER_TIMEOUT, &state->timeout, sizeof(state->timeout)); +#else + FF_DEBUG("Current platform does not support TCP connection timeout"); +#endif + } + + return NULL; + +error: + FF_DEBUG("Error occurred during initialization"); + if (state->addr != NULL) { + FF_DEBUG("Releasing address information"); + freeaddrinfo(state->addr); + state->addr = NULL; + } + + if (state->sockfd > 0) { + FF_DEBUG("Closing socket: fd=%d", state->sockfd); + close(state->sockfd); + state->sockfd = -1; + } + return ret; +} + +const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers) { + FF_DEBUG("Preparing to send HTTP request: host=%s, path=%s", host, path); + + if (state->compression) { + FF_DEBUG("Compression enabled, checking if zlib is available"); + +#ifdef FF_HAVE_ZLIB + const char* zlibError = ffNetworkingLoadZlibLibrary(); + // Only enable compression if zlib library is successfully loaded + if (zlibError == NULL) { + FF_DEBUG("Successfully loaded zlib library, compression enabled"); + } else { + FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError); + state->compression = false; + } +#else + FF_DEBUG("zlib not supported at build time, compression disabled"); + state->compression = false; +#endif + } else { + FF_DEBUG("Compression disabled"); + } + + const char* initResult = initNetworkingState(state, host, path, headers); + if (initResult != NULL) { + FF_DEBUG("Initialization failed: %s", initResult); + return initResult; + } + FF_DEBUG("Network state initialization successful"); + + const char* tfoResult = tryNonThreadingFastPath(state); + if (tfoResult == NULL) { + FF_DEBUG("TryNonThreadingFastPath() succeeded or in progress"); + return NULL; + } + FF_DEBUG("TryNonThreadingFastPath() failed: %s, trying traditional connection", tfoResult); + +#ifdef FF_HAVE_THREADS + if (instance.config.general.multithreading) { + FF_DEBUG("Multithreading mode enabled, creating connection thread"); + state->thread = ffThreadCreate(connectAndSendThreadMain, state); + if (state->thread) { + FF_DEBUG("Thread creation successful: thread=%p", (void*) (uintptr_t) state->thread); + return NULL; + } + FF_DEBUG("Thread creation failed"); + } else { + FF_DEBUG("Multithreading mode disabled, connecting in main thread"); + } +#endif + + return connectAndSend(state); +} + +const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer) { + assert(buffer->allocated > 0); + FF_DEBUG("Preparing to receive HTTP response"); + uint32_t timeout = state->timeout; + +#ifdef FF_HAVE_THREADS + if (state->thread) { + FF_DEBUG("Connection thread is running, waiting for it to complete (timeout=%u ms)", timeout); + if (!ffThreadJoin(state->thread, timeout)) { + FF_DEBUG("Thread join failed or timed out"); + return "ffThreadJoin() failed or timeout"; + } + FF_DEBUG("Thread completed successfully"); + state->thread = 0; + } +#endif + + if (state->sockfd == -1) { + FF_DEBUG("Invalid socket, HTTP request might have failed"); + return "ffNetworkingSendHttpRequest() failed"; + } + + // Set larger initial receive buffer instead of small repeated receives + int rcvbuf = 65536; // 64KB + setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + +#ifdef __APPLE__ + // poll for the socket to be readable. + // Because of the non-blocking connectx() call, the connection might not be established yet + FF_DEBUG("Using poll() to check if socket is readable"); + { + int pollRes = poll(&(struct pollfd) { + .fd = state->sockfd, + .events = POLLIN }, + 1, + timeout > 0 ? (int) timeout : -1); + if (pollRes == 0) { + FF_DEBUG("poll() timed out after %u ms", timeout); + close(state->sockfd); + state->sockfd = -1; + return "poll() timeout"; + } else if (pollRes == -1) { + FF_DEBUG("poll() failed: %s", strerror(errno)); + close(state->sockfd); + state->sockfd = -1; + return "poll() failed"; + } + } + FF_DEBUG("Socket is readable, proceeding to receive data"); +#else + if (timeout > 0) { + FF_DEBUG("Setting receive timeout: %u ms", timeout); + struct timeval timev; + timev.tv_sec = timeout / 1000; + timev.tv_usec = (__typeof__(timev.tv_usec)) ((timeout % 1000) * 1000); // milliseconds to microseconds + setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, &timev, sizeof(timev)); + } +#endif + + if (shutdown(state->sockfd, SHUT_WR) == -1) { + FF_DEBUG("Failed to shutdown socket send: %s", strerror(errno)); + // Not a critical error, continue anyway + } + + FF_DEBUG("Starting data reception"); + FF_A_UNUSED int recvCount = 0; + uint32_t contentLength = 0; + uint32_t headerEnd = 0; + + do { + FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u", + ++recvCount, + buffer->length, + ffStrbufGetFree(buffer)); + + // We set `Connection: close`, so the server will close the connection when done. + // Thus we can use MSG_WAITALL to wait until the buffer is full or the connection is closed. + ssize_t received = recv(state->sockfd, buffer->chars + buffer->length, ffStrbufGetFree(buffer), MSG_WAITALL); + + if (received <= 0) { + if (received == 0) { + FF_DEBUG("Connection closed (received=0)"); + } else { + FF_DEBUG("Reception failed: %s", strerror(errno)); + } + break; + } + + buffer->length += (uint32_t) received; + buffer->chars[buffer->length] = '\0'; + + FF_DEBUG("Successfully received %zd bytes of data, total: %u bytes", received, buffer->length); + + // Check if HTTP header end marker is found + if (headerEnd == 0) { + char* pHeaderEnd = memmem(buffer->chars, buffer->length, "\r\n\r\n", 4); + if (pHeaderEnd) { + headerEnd = (uint32_t) (pHeaderEnd - buffer->chars); + FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd); + + // Check for Content-Length header to pre-allocate enough memory + const char* clHeader = strcasestr(buffer->chars, "Content-Length:"); + if (clHeader) { + contentLength = (uint32_t) strtoul(clHeader + 15, NULL, 10); + if (contentLength > 0) { + FF_DEBUG("Detected Content-Length: %u, pre-allocating buffer", contentLength); + // Ensure buffer is large enough, adding header size and some margin + ffStrbufEnsureFree(buffer, contentLength + 16); + FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated); + } + } + } + } + } while (ffStrbufGetFree(buffer) > 0); + + FF_DEBUG("Closing socket: fd=%d", state->sockfd); + close(state->sockfd); + state->sockfd = -1; + + if (buffer->length == 0) { + FF_DEBUG("Server response is empty"); + return "Empty server response received"; + } + + if (headerEnd == 0) { + FF_DEBUG("No HTTP header end marker found"); + return "No HTTP header end found"; + } + + if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n")) { + FF_DEBUG("Invalid response: %.40s...", buffer->chars); + return "Invalid response"; + } + FF_DEBUG("Received valid HTTP 200 response, content %u bytes, total %u bytes", contentLength, buffer->length); + + if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) { + FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4); + return "Content length mismatch"; + } + +// If compression was used, try to decompress +#ifdef FF_HAVE_ZLIB + if (state->compression) { + FF_DEBUG("Content received, checking if compressed"); + if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) { + FF_DEBUG("Decompression failed or invalid compression format"); + return "Failed to decompress or invalid format"; + } else { + FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length); + } + } +#endif + + return NULL; +} diff --git a/src/common/impl/networking_windows.c b/src/common/impl/networking_windows.c new file mode 100644 index 0000000..d3a7752 --- /dev/null +++ b/src/common/impl/networking_windows.c @@ -0,0 +1,376 @@ +#include <mswsock.h> +#include <ws2tcpip.h> + +// Must be included after <mswsock.h> +#include "fastfetch.h" +#include "common/networking.h" +#include "common/strutil.h" +#include "common/debug.h" + +static LPFN_CONNECTEX ConnectEx; + +static const char* initWsaData(WSADATA* wsaData) { + FF_DEBUG("Initializing WinSock"); + if (WSAStartup(MAKEWORD(2, 2), wsaData) != 0) { + FF_DEBUG("WSAStartup() failed"); + return "WSAStartup() failed"; + } + + if (LOBYTE(wsaData->wVersion) != 2 || HIBYTE(wsaData->wVersion) != 2) { + FF_DEBUG("Invalid wsaData version found: %d.%d", LOBYTE(wsaData->wVersion), HIBYTE(wsaData->wVersion)); + WSACleanup(); + return "Invalid wsaData version found"; + } + + // Dummy socket needed for WSAIoctl + SOCKET sockfd = WSASocketW(AF_INET, SOCK_STREAM, 0, NULL, 0, 0); + if (sockfd == INVALID_SOCKET) { + FF_DEBUG("WSASocketW(AF_INET, SOCK_STREAM) failed"); + WSACleanup(); + return "WSASocketW(AF_INET, SOCK_STREAM) failed"; + } + + DWORD dwBytes; + GUID guid = WSAID_CONNECTEX; + if (WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &ConnectEx, sizeof(ConnectEx), &dwBytes, NULL, NULL) != 0) { + FF_DEBUG("WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed"); + closesocket(sockfd); + WSACleanup(); + return "WSAIoctl(sockfd, SIO_GET_EXTENSION_FUNCTION_POINTER) failed"; + } + + closesocket(sockfd); + FF_DEBUG("WinSock initialized successfully"); + + return NULL; +} + +const char* ffNetworkingSendHttpRequest(FFNetworkingState* state, const char* host, const char* path, const char* headers) { + FF_DEBUG("Preparing to send HTTP request: host=%s, path=%s", host, path); + + if (state->compression) { +#ifdef FF_HAVE_ZLIB + const char* zlibError = ffNetworkingLoadZlibLibrary(); + // Only enable compression if zlib library is successfully loaded + if (zlibError == NULL) { + FF_DEBUG("Successfully loaded zlib library, compression enabled"); + } else { + FF_DEBUG("Failed to load zlib library, compression disabled: %s", zlibError); + state->compression = false; + } +#else + FF_DEBUG("zlib not supported at build time, compression disabled"); + state->compression = false; +#endif + } else { + FF_DEBUG("Compression disabled"); + } + + static WSADATA wsaData; + if (wsaData.wVersion == 0) { + const char* error = initWsaData(&wsaData); + if (error != NULL) { + wsaData.wVersion = (WORD) -1; + FF_DEBUG("WinSock initialization failed: %s", error); + return error; + } + } else if (wsaData.wVersion == (WORD) -1) { + FF_DEBUG("WinSock initialization previously failed"); + return "initWsaData() failed before"; + } + + ADDRINFOW* addr; + ADDRINFOW hints = { + .ai_flags = AI_NUMERICSERV, + .ai_family = state->ipv6 ? AF_INET6 : AF_INET, + .ai_socktype = SOCK_STREAM, + }; + + wchar_t hostW[256]; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(hostW, (ULONG) sizeof(hostW), NULL, host, (ULONG) strlen(host) + 1))) { + FF_DEBUG("Failed to convert host to wide string: %s", host); + return "Failed to convert host to wide string"; + } + + FF_DEBUG("Resolving address: %s (%s)", host, state->ipv6 ? "IPv6" : "IPv4"); + if (GetAddrInfoW(hostW, L"80", &hints, &addr) != 0) { + FF_DEBUG("GetAddrInfoW() failed"); + return "GetAddrInfoW() failed"; + } + + state->sockfd = WSASocketW(addr->ai_family, addr->ai_socktype, addr->ai_protocol, NULL, 0, 0); + if (state->sockfd == INVALID_SOCKET) { + FF_DEBUG("WSASocketW() failed"); + FreeAddrInfoW(addr); + return "WSASocketW() failed"; + } + + DWORD flag = 1; +#ifdef TCP_NODELAY + // Enable TCP_NODELAY to disable Nagle's algorithm + if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_NODELAY: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + } else { + FF_DEBUG("Successfully disabled Nagle's algorithm"); + } +#endif + + // Set timeout if needed + if (state->timeout > 0) { + FF_DEBUG("Setting connection timeout: %u ms", state->timeout); + setsockopt(state->sockfd, SOL_SOCKET, SO_SNDTIMEO, (const char*) &state->timeout, sizeof(state->timeout)); + } + + // ConnectEx requires the socket to be initially bound + if ((state->ipv6 + ? bind(state->sockfd, (SOCKADDR*) &(struct sockaddr_in6) { + .sin6_family = AF_INET6, + .sin6_addr = in6addr_any, + }, + sizeof(struct sockaddr_in6)) + : bind(state->sockfd, (SOCKADDR*) &(struct sockaddr_in) { + .sin_family = AF_INET, + .sin_addr.s_addr = INADDR_ANY, + }, + sizeof(struct sockaddr_in))) != 0) { + FF_DEBUG("bind() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + closesocket(state->sockfd); + FreeAddrInfoW(addr); + state->sockfd = INVALID_SOCKET; + return "bind() failed"; + } + + // Initialize overlapped structure with WSA event for asynchronous I/O + state->overlapped = (OVERLAPPED) { + .hEvent = WSACreateEvent() + }; + + if (state->overlapped.hEvent == WSA_INVALID_EVENT) { + FF_DEBUG("WSACreateEvent() failed"); + closesocket(state->sockfd); + FreeAddrInfoW(addr); + state->sockfd = INVALID_SOCKET; + return "WSACreateEvent() failed"; + } + + // Build HTTP command + ffStrbufInitA(&state->command, 128); + ffStrbufAppendS(&state->command, "GET "); + ffStrbufAppendS(&state->command, path); + ffStrbufAppendS(&state->command, " HTTP/1.0\r\nHost: "); + ffStrbufAppendS(&state->command, host); + ffStrbufAppendS(&state->command, "\r\nConnection: close\r\n"); // Explicitly request connection closure + + // Add compression support if enabled + if (state->compression) { + FF_DEBUG("Enabling HTTP content compression"); + ffStrbufAppendS(&state->command, "Accept-Encoding: gzip\r\n"); + } + + ffStrbufAppendS(&state->command, headers); + ffStrbufAppendS(&state->command, "\r\n"); + +#ifdef TCP_FASTOPEN + if (state->tfo) { + // Set TCP Fast Open + flag = 1; + if (setsockopt(state->sockfd, IPPROTO_TCP, TCP_FASTOPEN, (char*) &flag, sizeof(flag)) != 0) { + FF_DEBUG("Failed to set TCP_FASTOPEN option: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + } else { + FF_DEBUG("Successfully set TCP_FASTOPEN option"); + } + } else { + FF_DEBUG("TCP Fast Open disabled"); + } +#endif + + FF_DEBUG("Using ConnectEx to send %u bytes of data", state->command.length); + DWORD sent = 0; + BOOL result = ConnectEx(state->sockfd, addr->ai_addr, (int) addr->ai_addrlen, state->command.chars, state->command.length, &sent, &state->overlapped); + + FreeAddrInfoW(addr); + addr = NULL; + + if (!result) { + if (WSAGetLastError() != WSA_IO_PENDING) { + FF_DEBUG("ConnectEx() failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + WSACloseEvent(state->overlapped.hEvent); + closesocket(state->sockfd); + state->sockfd = INVALID_SOCKET; + ffStrbufDestroy(&state->command); + return "ConnectEx() failed"; + } else { + FF_DEBUG("ConnectEx() pending"); + } + } else { + FF_DEBUG("ConnectEx() succeeded, sent %u bytes of data", (unsigned) sent); + } + + // No need to cleanup state fields here since we need them in the receive function + return NULL; +} + +const char* ffNetworkingRecvHttpResponse(FFNetworkingState* state, FFstrbuf* buffer) { + assert(buffer->allocated > 0); + FF_DEBUG("Preparing to receive HTTP response"); + + if (state->sockfd == INVALID_SOCKET) { + FF_DEBUG("Invalid socket, HTTP request might have failed"); + return "ffNetworkingSendHttpRequest() failed"; + } + + uint32_t timeout = state->timeout; + if (timeout > 0) { + FF_DEBUG("WSAWaitForMultipleEvents with timeout: %u ms", timeout); + DWORD result = WSAWaitForMultipleEvents(1, &state->overlapped.hEvent, TRUE, timeout, FALSE); + if (result != WSA_WAIT_EVENT_0) { + if (result == WSA_WAIT_TIMEOUT) { + FF_DEBUG("WSAWaitForMultipleEvents timed out"); + } else { + FF_DEBUG("WSAWaitForMultipleEvents failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + } + if (CancelIoEx((HANDLE) state->sockfd, &state->overlapped)) { + WSAWaitForMultipleEvents(1, &state->overlapped.hEvent, TRUE, 10, TRUE); + } + WSACloseEvent(state->overlapped.hEvent); + closesocket(state->sockfd); + ffStrbufDestroy(&state->command); + return "WSAWaitForMultipleEvents() failed or timeout"; + } + } + + DWORD transfer, flags; + if (!WSAGetOverlappedResult(state->sockfd, &state->overlapped, &transfer, TRUE, &flags)) { + FF_DEBUG("WSAGetOverlappedResult failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + closesocket(state->sockfd); + WSACloseEvent(state->overlapped.hEvent); + ffStrbufDestroy(&state->command); + return "WSAGetOverlappedResult() failed"; + } + FF_DEBUG("WSAGetOverlappedResult succeeded, %u bytes sent", (unsigned) transfer); + ffStrbufDestroy(&state->command); + WSACloseEvent(state->overlapped.hEvent); + state->overlapped.hEvent = NULL; + + if (setsockopt(state->sockfd, SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT, NULL, 0) != 0) { + FF_DEBUG("Failed to update connect context: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + // Not a critical error, continue anyway + } + + if (shutdown(state->sockfd, SD_SEND) == SOCKET_ERROR) { + FF_DEBUG("Failed to shutdown socket send: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + // Not a critical error, continue anyway + } + + if (timeout > 0) { + FF_DEBUG("Setting receive timeout: %u ms", timeout); + setsockopt(state->sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*) &timeout, sizeof(timeout)); + } + + // Set larger receive buffer for better performance + int rcvbuf = 65536; // 64KB + if (setsockopt(state->sockfd, SOL_SOCKET, SO_RCVBUF, (const char*) &rcvbuf, sizeof(rcvbuf))) { + FF_DEBUG("Failed to set SO_RCVBUF: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + // Not a critical error, continue anyway + } + + FF_DEBUG("Starting data reception"); + FF_A_UNUSED int recvCount = 0; + uint32_t contentLength = 0; + uint32_t headerEnd = 0; + + do { + FF_DEBUG("Data reception loop #%d, current buffer size: %u, available space: %u", + ++recvCount, + buffer->length, + ffStrbufGetFree(buffer)); + + DWORD received = 0, recvFlags = 0; + int recvResult = WSARecv(state->sockfd, &(WSABUF) { + .buf = buffer->chars + buffer->length, + .len = (ULONG) ffStrbufGetFree(buffer), + }, + 1, + &received, + &recvFlags, + NULL, + NULL); + + if (recvResult == SOCKET_ERROR || received == 0) { + if (recvResult == 0 && received == 0) { + FF_DEBUG("Connection closed (received=0)"); + } else { + FF_DEBUG("Reception failed: %s", ffDebugWin32Error((DWORD) WSAGetLastError())); + } + break; + } + + buffer->length += (uint32_t) received; + buffer->chars[buffer->length] = '\0'; + + FF_DEBUG("Successfully received %u bytes of data, total: %u bytes", (unsigned) received, buffer->length); + + // Check if HTTP header end marker is found + if (headerEnd == 0) { + char* pHeaderEnd = strstr(buffer->chars, "\r\n\r\n"); + if (pHeaderEnd) { + headerEnd = (uint32_t) (pHeaderEnd - buffer->chars); + FF_DEBUG("Found HTTP header end marker, position: %u", headerEnd); + + // Check for Content-Length header to pre-allocate enough memory + const char* clHeader = strcasestr(buffer->chars, "Content-Length:"); + if (clHeader) { + contentLength = (uint32_t) strtoul(clHeader + 15, NULL, 10); + if (contentLength > 0) { + FF_DEBUG("Detected Content-Length: %u, pre-allocating buffer", contentLength); + // Ensure buffer is large enough, adding header size and some margin + ffStrbufEnsureFree(buffer, contentLength + 16); + FF_DEBUG("Extended receive buffer to %u bytes", buffer->allocated); + } + } + } + } + } while (ffStrbufGetFree(buffer) > 0); + + FF_DEBUG("Closing socket: fd=%u", (unsigned) state->sockfd); + closesocket(state->sockfd); + state->sockfd = INVALID_SOCKET; + + if (buffer->length == 0) { + FF_DEBUG("Server response is empty"); + return "Empty server response received"; + } + + if (headerEnd == 0) { + FF_DEBUG("No HTTP header end marker found"); + return "No HTTP header end found"; + } + + if (!ffStrbufStartsWithS(buffer, "HTTP/1.0 200 OK\r\n")) { + FF_DEBUG("Invalid response: %.40s...", buffer->chars); + return "Invalid response"; + } + FF_DEBUG("Received valid HTTP 200 response, content length: %u bytes, total length: %u bytes", + contentLength, + buffer->length); + + if (contentLength > 0 && buffer->length != contentLength + headerEnd + 4) { + FF_DEBUG("Received content length mismatches: %u != %u", buffer->length, contentLength + headerEnd + 4); + return "Content length mismatch"; + } + +// If compression was used, try to decompress +#ifdef FF_HAVE_ZLIB + if (state->compression) { + FF_DEBUG("Content received, checking if compressed"); + if (!ffNetworkingDecompressGzip(buffer, buffer->chars + headerEnd)) { + FF_DEBUG("Decompression failed or invalid compression format"); + return "Failed to decompress or invalid format"; + } else { + FF_DEBUG("Decompression successful or no decompression needed, total length after decompression: %u bytes", buffer->length); + } + } +#endif + + return NULL; +} diff --git a/src/common/impl/option.c b/src/common/impl/option.c new file mode 100644 index 0000000..3d592d0 --- /dev/null +++ b/src/common/impl/option.c @@ -0,0 +1,185 @@ +#include "fastfetch.h" +#include "common/option.h" +#include "common/color.h" +#include "common/strutil.h" + +#include <limits.h> + +// Return start position of the inner key if the argument key belongs to the module specified, NULL otherwise +const char* ffOptionTestPrefix(const char* argumentKey, const char* moduleName) { + assert(argumentKey && moduleName); + + const char* subKey = argumentKey; + if (!(subKey[0] == '-' && subKey[1] == '-')) { + return NULL; + } + + subKey += 2; + uint32_t moduleNameLen = (uint32_t) strlen(moduleName); + if (strncasecmp(subKey, moduleName, moduleNameLen) != 0) { + return NULL; + } + + subKey += moduleNameLen; + + if (subKey[0] == '\0') { + return subKey; + } + + if (subKey[0] != '-') { + return NULL; + } + + subKey += 1; + + return subKey; +} + +void ffOptionParseString(const char* argumentKey, const char* value, FFstrbuf* buffer) { + if (value == NULL) { + fprintf(stderr, "Error: usage: %s <str>\n", argumentKey); + exit(477); + } + + ffStrbufSetS(buffer, value); +} + +uint32_t ffOptionParseUInt32(const char* argumentKey, const char* value) { + if (value == NULL) { + fprintf(stderr, "Error: usage: %s <num>\n", argumentKey); + exit(480); + } + + char* end; + unsigned long num = strtoul(value, &end, 10); + if (value[0] == '-' || *end != '\0' || num > UINT32_MAX) { + fprintf(stderr, "Error: usage: %s <num>\n", argumentKey); + exit(479); + } + + return (uint32_t) num; +} + +int32_t ffOptionParseInt32(const char* argumentKey, const char* value) { + if (value == NULL) { + fprintf(stderr, "Error: usage: %s <num>\n", argumentKey); + exit(480); + } + + char* end; + long num = strtol(value, &end, 10); + if (*end != '\0' || num < INT32_MIN || num > INT32_MAX) { + fprintf(stderr, "Error: usage: %s <num>\n", argumentKey); + exit(479); + } + + return (int32_t) num; +} + +int ffOptionParseEnum(const char* argumentKey, const char* requestedKey, FFKeyValuePair pairs[]) { + if (requestedKey == NULL) { + fprintf(stderr, "Error: usage: %s <value>\n", argumentKey); + exit(476); + } + + for (const FFKeyValuePair* pPair = pairs; pPair->key; ++pPair) { + if (ffStrEqualsIgnCase(requestedKey, pPair->key)) { + return pPair->value; + } + } + + fprintf(stderr, "Error: unknown %s value: %s\n", argumentKey, requestedKey); + exit(478); +} + +bool ffOptionParseBoolean(const char* str) { + return ( + !ffStrSet(str) || + ffStrEqualsIgnCase(str, "true") || + ffStrEqualsIgnCase(str, "yes") || + ffStrEqualsIgnCase(str, "on") || + ffStrEqualsIgnCase(str, "1")); +} + +void ffOptionParseColorNoClear(const char* value, FFstrbuf* buffer) { + if (!value || value[0] == '\0') { + return; + } + + // If value is already an ANSI escape code, use it + if (value[0] == '\e' && value[1] == '[') { + ffStrbufAppendS(buffer, value + 2); + ffStrbufTrimRight(buffer, 'm'); + return; + } + + ffStrbufEnsureFree(buffer, 63); + + while (*value != '\0') { +#define FF_APPEND_COLOR_CODE_COND(prefix, code) \ + if (ffStrStartsWithIgnCase(value, #prefix)) { \ + ffStrbufAppendS(buffer, code); \ + value += strlen(#prefix); \ + continue; \ + } +#define FF_APPEND_COLOR_PROP_COND(prefix, prop) \ + if (ffStrStartsWithIgnCase(value, #prefix)) { \ + if (instance.config.display.prop.length) ffStrbufAppend(buffer, &instance.config.display.prop); \ + else ffStrbufAppendS(buffer, FF_COLOR_FG_DEFAULT); \ + value += strlen(#prefix); \ + continue; \ + } + + if (ffCharIsEnglishAlphabet(value[0])) { + FF_APPEND_COLOR_CODE_COND(reset_, FF_COLOR_MODE_RESET) + else FF_APPEND_COLOR_CODE_COND(bold_, FF_COLOR_MODE_BOLD) else FF_APPEND_COLOR_CODE_COND(bright_, FF_COLOR_MODE_BOLD) else FF_APPEND_COLOR_CODE_COND(dim_, FF_COLOR_MODE_DIM) else FF_APPEND_COLOR_CODE_COND(italic_, FF_COLOR_MODE_ITALIC) else FF_APPEND_COLOR_CODE_COND(underline_, FF_COLOR_MODE_UNDERLINE) else FF_APPEND_COLOR_CODE_COND(blink_, FF_COLOR_MODE_BLINK) else FF_APPEND_COLOR_CODE_COND(inverse_, FF_COLOR_MODE_INVERSE) else FF_APPEND_COLOR_CODE_COND(hidden_, FF_COLOR_MODE_HIDDEN) else FF_APPEND_COLOR_CODE_COND(strike_, FF_COLOR_MODE_STRIKETHROUGH) else FF_APPEND_COLOR_CODE_COND(black, FF_COLOR_FG_BLACK) else FF_APPEND_COLOR_CODE_COND(red, FF_COLOR_FG_RED) else FF_APPEND_COLOR_CODE_COND(green, FF_COLOR_FG_GREEN) else FF_APPEND_COLOR_CODE_COND(yellow, FF_COLOR_FG_YELLOW) else FF_APPEND_COLOR_CODE_COND(blue, FF_COLOR_FG_BLUE) else FF_APPEND_COLOR_CODE_COND(magenta, FF_COLOR_FG_MAGENTA) else FF_APPEND_COLOR_CODE_COND(cyan, FF_COLOR_FG_CYAN) else FF_APPEND_COLOR_CODE_COND(white, FF_COLOR_FG_WHITE) else FF_APPEND_COLOR_CODE_COND(default, FF_COLOR_FG_DEFAULT) else FF_APPEND_COLOR_CODE_COND(light_black, FF_COLOR_FG_LIGHT_BLACK) else FF_APPEND_COLOR_CODE_COND(light_red, FF_COLOR_FG_LIGHT_RED) else FF_APPEND_COLOR_CODE_COND(light_green, FF_COLOR_FG_LIGHT_GREEN) else FF_APPEND_COLOR_CODE_COND(light_yellow, FF_COLOR_FG_LIGHT_YELLOW) else FF_APPEND_COLOR_CODE_COND(light_blue, FF_COLOR_FG_LIGHT_BLUE) else FF_APPEND_COLOR_CODE_COND(light_magenta, FF_COLOR_FG_LIGHT_MAGENTA) else FF_APPEND_COLOR_CODE_COND(light_cyan, FF_COLOR_FG_LIGHT_CYAN) else FF_APPEND_COLOR_CODE_COND(light_white, FF_COLOR_FG_LIGHT_WHITE) else FF_APPEND_COLOR_PROP_COND(keys, colorKeys) else FF_APPEND_COLOR_PROP_COND(title, colorTitle) else FF_APPEND_COLOR_PROP_COND(output, colorOutput) else FF_APPEND_COLOR_PROP_COND(separator, colorSeparator) else { + fprintf(stderr, "Error: invalid color code found: %s\n", value); + exit(479); + } + } else if (value[0] == '@') { + // Xterm 256 color + ++value; + char* pend = NULL; + uint32_t color = (uint32_t) strtoul(value, &pend, 10); + if (pend == value || color > 255) { + fprintf(stderr, "Error: invalid 256 color code found: %s\n", value); + exit(479); + } + + ffStrbufAppendS(buffer, FF_COLOR_FG_256); + ffStrbufAppendUInt(buffer, color); + value = pend; + continue; + } else if (value[0] == '#') { + // RGB color + ++value; + char* pend = NULL; + uint32_t rgb = (uint32_t) strtoul(value, &pend, 16); + if (pend == value) { + fprintf(stderr, "Error: invalid RGB color code found: %s\n", value); + exit(479); + } + if (pend - value > 6) { + fprintf(stderr, "Error: RGB color code too long: %s\n", value); + exit(479); + } else if (pend - value == 3) { + rgb = ((rgb & 0xF00) >> 8) * 0x110000 + + ((rgb & 0x0F0) >> 4) * 0x001100 + + ((rgb & 0x00F) >> 0) * 0x000011; + } else if (pend - value != 6) { + fprintf(stderr, "Error: invalid RGB color code length: %s\n", value); + exit(479); + } + + uint32_t r = rgb >> 16, g = (rgb >> 8) & 0xFF, b = rgb & 0xFF; + ffStrbufAppendF(buffer, FF_COLOR_FG_RGB "%u;%u;%u", r, g, b); + value = pend; + continue; + } + ffStrbufAppendC(buffer, *value); + ++value; + +#undef FF_APPEND_COLOR_CODE_COND +#undef FF_APPEND_COLOR_PROP_COND + } +} diff --git a/src/common/impl/parsing.c b/src/common/impl/parsing.c new file mode 100644 index 0000000..e88d10c --- /dev/null +++ b/src/common/impl/parsing.c @@ -0,0 +1,136 @@ +#include "fastfetch.h" +#include "common/parsing.h" + +#ifdef _WIN32 + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wformat" +#endif + +void ffParseSemver(FFstrbuf* buffer, const FFstrbuf* major, const FFstrbuf* minor, const FFstrbuf* patch) { + if (major->length > 0) { + ffStrbufAppend(buffer, major); + } else if (minor->length > 0 || patch->length > 0) { + ffStrbufAppendC(buffer, '1'); + } + + if (minor->length == 0 && patch->length == 0) { + return; + } + + ffStrbufAppendC(buffer, '.'); + + if (minor->length > 0) { + ffStrbufAppend(buffer, minor); + } else if (patch->length > 0) { + ffStrbufAppendC(buffer, '0'); + } + + if (patch->length == 0) { + return; + } + + ffStrbufAppendC(buffer, '.'); + + ffStrbufAppend(buffer, patch); +} + +int8_t ffVersionCompare(const FFVersion* version1, const FFVersion* version2) { + if (version1->major != version2->major) { + return version1->major > version2->major ? 1 : -1; + } + + if (version1->minor != version2->minor) { + return version1->minor > version2->minor ? 1 : -1; + } + + if (version1->patch != version2->patch) { + return version1->patch > version2->patch ? 1 : -1; + } + + return 0; +} + +void ffVersionToPretty(const FFVersion* version, FFstrbuf* pretty) { + if (version->major > 0 || version->minor > 0 || version->patch > 0) { + ffStrbufAppendUInt(pretty, version->major); + } + + if (version->minor > 0 || version->patch > 0) { + ffStrbufAppendC(pretty, '.'); + ffStrbufAppendUInt(pretty, version->minor); + } + + if (version->patch > 0) { + ffStrbufAppendC(pretty, '.'); + ffStrbufAppendUInt(pretty, version->patch); + } +} + +void ffParseGTK(FFstrbuf* buffer, const FFstrbuf* gtk2, const FFstrbuf* gtk3, const FFstrbuf* gtk4) { + if (gtk2->length > 0 && gtk3->length > 0 && gtk4->length > 0) { + if ((ffStrbufIgnCaseEqual(gtk2, gtk3)) && (ffStrbufIgnCaseEqual(gtk2, gtk4))) { + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK2/3/4]"); + } else if (ffStrbufIgnCaseEqual(gtk2, gtk3)) { + ffStrbufAppend(buffer, gtk3); + ffStrbufAppendS(buffer, " [GTK2/3], "); + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK4]"); + } else if (ffStrbufIgnCaseEqual(gtk3, gtk4)) { + ffStrbufAppend(buffer, gtk2); + ffStrbufAppendS(buffer, " [GTK2], "); + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK3/4]"); + } else { + ffStrbufAppend(buffer, gtk2); + ffStrbufAppendS(buffer, " [GTK2], "); + ffStrbufAppend(buffer, gtk3); + ffStrbufAppendS(buffer, " [GTK3], "); + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK4]"); + } + } else if (gtk2->length > 0 && gtk3->length > 0) { + if (ffStrbufIgnCaseEqual(gtk2, gtk3)) { + ffStrbufAppend(buffer, gtk3); + ffStrbufAppendS(buffer, " [GTK2/3]"); + } else { + ffStrbufAppend(buffer, gtk2); + ffStrbufAppendS(buffer, " [GTK2], "); + ffStrbufAppend(buffer, gtk3); + ffStrbufAppendS(buffer, " [GTK3]"); + } + } else if (gtk2->length > 0 && gtk4->length > 0) { + if (ffStrbufIgnCaseEqual(gtk2, gtk4)) { + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK2/4]"); + } else { + ffStrbufAppend(buffer, gtk2); + ffStrbufAppendS(buffer, " [GTK2], "); + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK4]"); + } + } else if (gtk3->length > 0 && gtk4->length > 0) { + if (ffStrbufIgnCaseEqual(gtk3, gtk4)) { + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK3/4]"); + } else { + ffStrbufAppend(buffer, gtk3); + ffStrbufAppendS(buffer, " [GTK3], "); + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK4]"); + } + } else if (gtk2->length > 0) { + ffStrbufAppend(buffer, gtk2); + ffStrbufAppendS(buffer, " [GTK2]"); + } else if (gtk3->length > 0) { + ffStrbufAppend(buffer, gtk3); + ffStrbufAppendS(buffer, " [GTK3]"); + } else if (gtk4->length > 0) { + ffStrbufAppend(buffer, gtk4); + ffStrbufAppendS(buffer, " [GTK4]"); + } +} + +#ifdef _WIN32 + #pragma GCC diagnostic pop +#endif diff --git a/src/common/impl/path.c b/src/common/impl/path.c new file mode 100644 index 0000000..c97210e --- /dev/null +++ b/src/common/impl/path.c @@ -0,0 +1,277 @@ +#include "common/path.h" +#include "common/io.h" +#include "common/arrutil.h" + +#if !_WIN32 +const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) { + char* path = getenv("PATH"); + if (!path) { + return "$PATH not set"; + } + + #ifdef _WIN32 + const bool appendExe = !ffStrEndsWithIgnCase(name, ".exe"); + #endif + + for (char* token = path; *token; path = token + 1) { + token = strchr(path, + #ifdef _WIN32 + ';' + #else + ':' + #endif + ); + if (!token) { + token = path + strlen(path); + } + + ffStrbufSetNS(result, (uint32_t) (token - path), path); + ffStrbufEnsureEndsWithC(result, + #ifdef _WIN32 + '\\' + #else + '/' + #endif + ); + ffStrbufAppendS(result, name); + #ifdef _WIN32 + if (appendExe) { + ffStrbufAppendS(result, ".exe"); + } + if (!ffPathExists(result->chars, FF_PATHTYPE_FILE)) { + continue; + } + #else + if (access(result->chars, X_OK) != 0) { + continue; + } + #endif + + return NULL; + } + ffStrbufClear(result); + return "Executable not found"; +} +#else + #include <windows.h> + #include <winioctl.h> + #include <errno.h> + #include <stdalign.h> + +const char* ffFindExecutableInPath(const char* name, FFstrbuf* result) { + char buffer[MAX_PATH + 1]; + DWORD length = SearchPathA(NULL, name, ".exe", sizeof(buffer), buffer, NULL); + if (length == 0) { + ffStrbufClear(result); + return "Executable not found"; + } + ffStrbufSetS(result, buffer); + return NULL; +} + +static inline int winerr2Errno(DWORD err) { + switch (err) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_NAME: + return ENOENT; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + return EACCES; + case ERROR_BUFFER_OVERFLOW: + case ERROR_INSUFFICIENT_BUFFER: + return ENAMETOOLONG; + case ERROR_INVALID_PARAMETER: + case ERROR_NOT_A_REPARSE_POINT: + return EINVAL; + default: + return EIO; + } +} + +char* frealpath(HANDLE hFile, char* resolved_name) { + if (__builtin_expect(hFile == INVALID_HANDLE_VALUE || !hFile, false)) { + errno = EINVAL; + return NULL; + } + + wchar_t resolvedNameW[MAX_PATH + 4]; /* +4 for "\\\\?\\" prefix */ + DWORD lenW = GetFinalPathNameByHandleW(hFile, resolvedNameW, (DWORD) ARRAY_SIZE(resolvedNameW), FILE_NAME_NORMALIZED); + + if (lenW == 0) { + errno = winerr2Errno(GetLastError()); + return NULL; + } + if (lenW >= ARRAY_SIZE(resolvedNameW)) { + errno = E2BIG; + return NULL; + } + lenW++; // Include null terminator + + wchar_t* srcW = resolvedNameW; + DWORD srcLenW = lenW; + + if (srcLenW >= 8 && wcsncmp(resolvedNameW, L"\\\\?\\UNC\\", 8) == 0) { + /* Convert "\\?\UNC\server\share" to "\\server\share" */ + srcW += 6; + srcLenW -= 6; + *srcW = L'\\'; + } else if (srcLenW >= 4 && wcsncmp(resolvedNameW, L"\\\\?\\", 4) == 0) { + srcW += 4; + srcLenW -= 4; + } + + if (resolved_name) { + ULONG outBytes = 0; + if (!NT_SUCCESS(RtlUnicodeToUTF8N(resolved_name, MAX_PATH, &outBytes, srcW, (ULONG) (srcLenW * sizeof(wchar_t))))) { + errno = E2BIG; + return NULL; + } + } else { + /* UTF-8 worst-case: up to 4 bytes per UTF-16 code unit */ + char tmp[(MAX_PATH + 4) * 4]; + ULONG outBytes = 0; + + if (!NT_SUCCESS(RtlUnicodeToUTF8N(tmp, (ULONG) sizeof(tmp), &outBytes, srcW, (ULONG) (srcLenW * sizeof(wchar_t))))) { + errno = E2BIG; + return NULL; + } + + resolved_name = (char*) malloc(outBytes); + if (!resolved_name) { + errno = ENOMEM; + return NULL; + } + + memcpy(resolved_name, tmp, outBytes); + } + + return resolved_name; +} + +char* realpath(const char* __restrict file_name, char* __restrict resolved_name) { + if (!file_name) { + errno = EINVAL; + return NULL; + } + + wchar_t fileNameW[MAX_PATH]; + ULONG lenBytes = 0; + + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(fileNameW, (ULONG) sizeof(fileNameW), &lenBytes, file_name, (ULONG) strlen(file_name) + 1))) { + errno = EINVAL; + return NULL; + } + + FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileW( + fileNameW, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if (hFile == INVALID_HANDLE_VALUE) { + errno = winerr2Errno(GetLastError()); + return NULL; + } + + return frealpath(hFile, resolved_name); +} + +ssize_t freadlink(HANDLE hFile, char* buf, size_t bufsiz) { + if (__builtin_expect(hFile == INVALID_HANDLE_VALUE || !buf || bufsiz == 0, false)) { + errno = EINVAL; + return -1; + } + + alignas(REPARSE_DATA_BUFFER) BYTE reparseBuf[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + DWORD bytesReturned = 0; + if (!DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT, NULL, 0, reparseBuf, (DWORD) sizeof(reparseBuf), &bytesReturned, NULL)) { + errno = winerr2Errno(GetLastError()); + return -1; + } + + REPARSE_DATA_BUFFER* rp = (REPARSE_DATA_BUFFER*) reparseBuf; + const wchar_t* targetW = NULL; + USHORT targetBytes = 0; + + if (rp->ReparseTag == IO_REPARSE_TAG_SYMLINK) { + if (rp->SymbolicLinkReparseBuffer.PrintNameLength > 0) { + targetW = rp->SymbolicLinkReparseBuffer.PathBuffer + + (rp->SymbolicLinkReparseBuffer.PrintNameOffset / sizeof(wchar_t)); + targetBytes = rp->SymbolicLinkReparseBuffer.PrintNameLength; + } else { + targetW = rp->SymbolicLinkReparseBuffer.PathBuffer + + (rp->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(wchar_t)); + targetBytes = rp->SymbolicLinkReparseBuffer.SubstituteNameLength; + + if (targetBytes >= 8 && + wcsncmp(targetW, L"\\??\\", 4) == 0) { + targetW += 4; + targetBytes -= 8; + } + } + } else if (rp->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) { + if (rp->MountPointReparseBuffer.PrintNameLength > 0) { + targetW = rp->MountPointReparseBuffer.PathBuffer + + (rp->MountPointReparseBuffer.PrintNameOffset / sizeof(wchar_t)); + targetBytes = rp->MountPointReparseBuffer.PrintNameLength; + } else { + targetW = rp->MountPointReparseBuffer.PathBuffer + + (rp->MountPointReparseBuffer.SubstituteNameOffset / sizeof(wchar_t)); + targetBytes = rp->MountPointReparseBuffer.SubstituteNameLength; + + if (targetBytes >= 8 && + wcsncmp(targetW, L"\\??\\", 4) == 0) { + targetW += 4; + targetBytes -= 8; + } + } + } else { + errno = EINVAL; + return -1; + } + + ULONG outBytes = 0; + if (!NT_SUCCESS(RtlUnicodeToUTF8N(buf, (ULONG) bufsiz, &outBytes, targetW, targetBytes))) { + errno = E2BIG; + return -1; + } + + // Not null-terminated + return (ssize_t) outBytes; +} + +ssize_t readlink(const char* path, char* buf, size_t bufsiz) { + if (!path || !buf || bufsiz == 0) { + errno = EINVAL; + return -1; + } + + wchar_t pathW[MAX_PATH]; + ULONG pathWBytes = 0; + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(pathW, (ULONG) sizeof(pathW), &pathWBytes, path, (ULONG) strlen(path) + 1))) { + errno = EINVAL; + return -1; + } + + FF_AUTO_CLOSE_FD HANDLE hFile = CreateFileW( + pathW, + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + NULL); + + if (hFile == INVALID_HANDLE_VALUE) { + errno = winerr2Errno(GetLastError()); + return -1; + } + + return freadlink(hFile, buf, bufsiz); +} +#endif diff --git a/src/common/impl/percent.c b/src/common/impl/percent.c new file mode 100644 index 0000000..266588c --- /dev/null +++ b/src/common/impl/percent.c @@ -0,0 +1,293 @@ +#include "fastfetch.h" +#include "common/percent.h" +#include "common/color.h" +#include "common/option.h" +#include "common/jsonconfig.h" +#include "common/textModifier.h" +#include "common/strutil.h" + +static void appendOutputColor(FFstrbuf* buffer, const FFModuleArgs* module) { + if (module->outputColor.length) { + ffStrbufAppendF(buffer, "\e[%sm", module->outputColor.chars); + } else if (instance.config.display.colorOutput.length) { + ffStrbufAppendF(buffer, "\e[%sm", instance.config.display.colorOutput.chars); + } +} + +const char* ffPercentParseTypeJsonConfig(yyjson_val* jsonVal, FFPercentageTypeFlags* result) { + if (yyjson_is_uint(jsonVal)) { + *result = (FFPercentageTypeFlags) yyjson_get_uint(jsonVal); + return NULL; + } + if (yyjson_is_arr(jsonVal)) { + FFPercentageTypeFlags flags = 0; + + yyjson_val* item; + size_t idx, max; + yyjson_arr_foreach (jsonVal, idx, max, item) { + const char* flag = yyjson_get_str(item); + if (!flag) { + return "Error: percent.type: invalid flag string"; + } + if (ffStrEqualsIgnCase(flag, "num")) { + flags |= FF_PERCENTAGE_TYPE_NUM_BIT; + } else if (ffStrEqualsIgnCase(flag, "bar")) { + flags |= FF_PERCENTAGE_TYPE_BAR_BIT; + } else if (ffStrEqualsIgnCase(flag, "hide-others")) { + flags |= FF_PERCENTAGE_TYPE_HIDE_OTHERS_BIT; + } else if (ffStrEqualsIgnCase(flag, "num-color")) { + flags |= FF_PERCENTAGE_TYPE_NUM_COLOR_BIT; + } else if (ffStrEqualsIgnCase(flag, "bar-monochrome")) { + flags |= FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT; + } else { + return "Error: percent.type: unknown flag string"; + } + } + + *result = flags; + return NULL; + } + + return "Error: usage: percent.type must be a number or an array of strings"; +} + +void ffPercentAppendBar(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, const FFModuleArgs* module) { + uint8_t green = config.green, yellow = config.yellow; + assert(green <= 100 && yellow <= 100); + + const FFOptionsDisplay* options = &instance.config.display; + + const bool borderAsValue = options->barBorderLeftElapsed.length && options->barBorderRightElapsed.length; + + if (!borderAsValue && options->barBorderLeft.length) { + if (!options->pipe && options->barColorBorder.length > 0) { + ffStrbufAppendF(buffer, "\e[%sm", options->barColorBorder.chars); + } + ffStrbufAppend(buffer, &options->barBorderLeft); + } + + if (percent == -DBL_MAX) { + // Use total color for simplification + if (!options->pipe && options->barColorTotal.length > 0) { + ffStrbufAppendS(buffer, "\e[" FF_COLOR_FG_LIGHT_BLACK "m"); + } + + for (uint8_t i = 0; i < options->barWidth; ++i) { + ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeft : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRight + : &options->barCharTotal); + } + } else { + const char* colorGreen = options->percentColorGreen.chars; + const char* colorYellow = options->percentColorYellow.chars; + const char* colorRed = options->percentColorRed.chars; + + FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type; + + uint8_t blocksPercent = (uint8_t) (percent / 100.0 * options->barWidth + 0.5); + assert(blocksPercent <= options->barWidth); + + bool autoColorElapsed = ffStrbufIgnCaseEqualS(&options->barColorElapsed, "auto"); + + bool monochrome = (percentType & FF_PERCENTAGE_TYPE_BAR_MONOCHROME_BIT) || !autoColorElapsed; + if (!options->pipe && options->barColorElapsed.length > 0 && monochrome) { + const char* color = NULL; + if (!autoColorElapsed) { + color = options->barColorElapsed.chars; + } else if (green <= yellow) { + if (percent < green) { + color = colorGreen; + } else if (percent < yellow) { + color = colorYellow; + } else { + color = colorRed; + } + } else { + if (percent < yellow) { + color = colorRed; + } else if (percent < green) { + color = colorYellow; + } else { + color = colorGreen; + } + } + ffStrbufAppendF(buffer, "\e[%sm", color); + } + for (uint8_t i = 0; i < blocksPercent; ++i) { + if (!options->pipe && options->barColorElapsed.length > 0 && !monochrome) { + uint32_t section1Begin = (uint32_t) ((green <= yellow ? green : yellow) / 100.0 * options->barWidth + 0.5); + uint32_t section2Begin = (uint32_t) ((green > yellow ? green : yellow) / 100.0 * options->barWidth + 0.5); + if (i == section2Begin) { + ffStrbufAppendF(buffer, "\e[%sm", (green > yellow ? colorGreen : colorRed)); + } else if (i == section1Begin) { + ffStrbufAppendF(buffer, "\e[%sm", colorYellow); + } else if (i == 0) { + ffStrbufAppendF(buffer, "\e[%sm", (green <= yellow ? colorGreen : colorRed)); + } + } + ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeftElapsed : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRightElapsed + : &options->barCharElapsed); + } + + if (blocksPercent < options->barWidth) { + if (!options->pipe && options->barColorTotal.length > 0) { + ffStrbufAppendF(buffer, "\e[%sm", options->barColorTotal.chars); + } + for (uint8_t i = blocksPercent; i < options->barWidth; ++i) { + ffStrbufAppend(buffer, borderAsValue && i == 0 ? &options->barBorderLeft : borderAsValue && i == options->barWidth - 1 ? &options->barBorderRight + : &options->barCharTotal); + } + } + } + + if (!borderAsValue && options->barBorderRight.length) { + if (!options->pipe && options->barColorBorder.length > 0) { + ffStrbufAppendF(buffer, "\e[%sm", options->barColorBorder.chars); + } + ffStrbufAppend(buffer, &options->barBorderRight); + } + + if (!options->pipe && (options->barColorElapsed.length > 0 || options->barColorTotal.length > 0 || options->barColorBorder.length > 0)) { + ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET); + appendOutputColor(buffer, module); + } +} + +void ffPercentAppendNum(FFstrbuf* buffer, double percent, FFPercentageModuleConfig config, bool parentheses, const FFModuleArgs* module) { + uint8_t green = config.green, yellow = config.yellow; + assert(green <= 100 && yellow <= 100); + + const FFOptionsDisplay* options = &instance.config.display; + FFPercentageTypeFlags percentType = config.type == 0 ? options->percentType : config.type; + + bool colored = !!(percentType & FF_PERCENTAGE_TYPE_NUM_COLOR_BIT); + + if (parentheses) { + ffStrbufAppendC(buffer, '('); + } + + if (colored && !options->pipe) { + const char* colorGreen = options->percentColorGreen.chars; + const char* colorYellow = options->percentColorYellow.chars; + const char* colorRed = options->percentColorRed.chars; + + if (percent == -DBL_MAX) { + ffStrbufAppendS(buffer, "\e[" FF_COLOR_FG_LIGHT_BLACK "m"); + } else if (green <= yellow) { + if (percent > yellow) { + ffStrbufAppendF(buffer, "\e[%sm", colorRed); + } else if (percent > green) { + ffStrbufAppendF(buffer, "\e[%sm", colorYellow); + } else { + ffStrbufAppendF(buffer, "\e[%sm", colorGreen); + } + } else { + if (percent < yellow) { + ffStrbufAppendF(buffer, "\e[%sm", colorRed); + } else if (percent < green) { + ffStrbufAppendF(buffer, "\e[%sm", colorYellow); + } else { + ffStrbufAppendF(buffer, "\e[%sm", colorGreen); + } + } + } + if (percent == -DBL_MAX) { + ffStrbufAppendS(buffer, "-"); + } else { + ffStrbufAppendF(buffer, "%*.*f%s%%", options->percentWidth, options->percentNdigits, percent, options->percentSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : ""); + } + + if (colored && !options->pipe) { + ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET); + appendOutputColor(buffer, module); + } + + if (parentheses) { + ffStrbufAppendC(buffer, ')'); + } +} + +bool ffPercentParseCommandOptions(const char* key, const char* subkey, const char* value, FFPercentageModuleConfig* config) { + if (!ffStrStartsWithIgnCase(subkey, "percent-")) { + return false; + } + + subkey += strlen("percent-"); + + if (ffStrEqualsIgnCase(subkey, "green")) { + uint32_t num = ffOptionParseUInt32(key, value); + if (num > 100) { + fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key); + exit(480); + } + config->green = (uint8_t) num; + return true; + } + + if (ffStrEqualsIgnCase(subkey, "yellow")) { + uint32_t num = ffOptionParseUInt32(key, value); + if (num > 100) { + fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key); + exit(480); + } + config->yellow = (uint8_t) num; + return true; + } + + if (ffStrEqualsIgnCase(subkey, "type")) { + config->type = (FFPercentageTypeFlags) ffOptionParseUInt32(key, value); + return true; + } + + return false; +} + +bool ffPercentParseJsonObject(yyjson_val* key, yyjson_val* value, FFPercentageModuleConfig* config) { + assert(key); + + if (!unsafe_yyjson_equals_str(key, "percent")) { + return false; + } + + if (!yyjson_is_obj(value)) { + fprintf(stderr, "Error: usage: %s must be an object\n", unsafe_yyjson_get_str(key)); + exit(480); + } + + yyjson_val* greenVal = yyjson_obj_get(value, "green"); + if (greenVal) { + int num = yyjson_get_int(greenVal); + if (num < 0 || num > 100) { + fputs("Error: usage: percent.green must be between 0 and 100\n", stderr); + exit(480); + } + config->green = (uint8_t) num; + } + + yyjson_val* yellowVal = yyjson_obj_get(value, "yellow"); + if (yellowVal) { + int num = yyjson_get_int(yellowVal); + if (num < 0 || num > 100) { + fputs("Error: usage: percent.yellow must be between 0 and 100\n", stderr); + exit(480); + } + config->yellow = (uint8_t) num; + } + + yyjson_val* typeVal = yyjson_obj_get(value, "type"); + if (typeVal) { + const char* error = ffPercentParseTypeJsonConfig(typeVal, &config->type); + if (error) { + fputs(error, stderr); + exit(480); + } + } + + return true; +} + +void ffPercentGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, FFPercentageModuleConfig config) { + yyjson_mut_val* percent = yyjson_mut_obj_add_obj(doc, module, "percent"); + yyjson_mut_obj_add_uint(doc, percent, "green", config.green); + yyjson_mut_obj_add_uint(doc, percent, "yellow", config.yellow); + yyjson_mut_obj_add_uint(doc, percent, "type", config.type); +} diff --git a/src/common/impl/printing.c b/src/common/impl/printing.c new file mode 100644 index 0000000..9f372d9 --- /dev/null +++ b/src/common/impl/printing.c @@ -0,0 +1,156 @@ +#include "fastfetch.h" +#include "common/printing.h" +#include "common/textModifier.h" +#include "logo/logo.h" + +void ffPrintLogoAndKey(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType) { + ffLogoPrintLine(); + + // This is used by --set-keyless, in this case we want neither the module name nor the separator + if (moduleName == NULL) { + return; + } + + // This is used as a magic value for hiding keys + if (!(moduleArgs && ffStrbufEqualS(&moduleArgs->key, " ")) && instance.config.display.keyType != FF_MODULE_KEY_TYPE_NONE) { + ffPrintCharTimes(' ', instance.config.display.keyPaddingLeft); + + if (!instance.config.display.pipe) { + fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout); + if (instance.config.display.brightColor) { + fputs(FASTFETCH_TEXT_MODIFIER_BOLT, stdout); + } + + if (moduleArgs && !(printType & FF_PRINT_TYPE_NO_CUSTOM_KEY_COLOR) && moduleArgs->keyColor.length > 0) { + ffPrintColor(&moduleArgs->keyColor); + } else { + ffPrintColor(&instance.config.display.colorKeys); + } + } + + if (instance.config.display.keyType & FF_MODULE_KEY_TYPE_ICON && moduleArgs && moduleArgs->keyIcon.length > 0) { + ffStrbufWriteTo(&moduleArgs->keyIcon, stdout); + } + + if (instance.config.display.keyType & FF_MODULE_KEY_TYPE_STRING) { + ffPrintCharTimes(' ', instance.config.display.keyType >> FF_MODULE_KEY_TYPE_SPACE_SHIFT); + + // NULL check is required for modules with custom keys, e.g. disk with the folder path + if ((printType & FF_PRINT_TYPE_NO_CUSTOM_KEY) || !moduleArgs || moduleArgs->key.length == 0) { + fputs(moduleName, stdout); + + if (moduleIndex > 0) { + printf(" %hhu", moduleIndex); + } + } else { + FF_STRBUF_AUTO_DESTROY key = ffStrbufCreate(); + FF_PARSE_FORMAT_STRING_CHECKED(&key, &moduleArgs->key, ((FFformatarg[]) { + FF_ARG(moduleIndex, "index"), + FF_ARG(moduleArgs->keyIcon, "icon"), + })); + ffStrbufWriteTo(&key, stdout); + } + } + + if (!instance.config.display.pipe) { + fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout); + ffPrintColor(&instance.config.display.colorSeparator); + } + + ffStrbufWriteTo(&instance.config.display.keyValueSeparator, stdout); + + if (!instance.config.display.pipe && instance.config.display.colorSeparator.length) { + fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout); + } + + if (!(printType & FF_PRINT_TYPE_NO_CUSTOM_KEY_WIDTH)) { + uint32_t keyWidth = moduleArgs && moduleArgs->keyWidth > 0 ? moduleArgs->keyWidth : instance.config.display.keyWidth; + if (keyWidth > 0) { + printf("\e[%uG", (unsigned) (keyWidth + instance.state.logoWidth)); + } + } + } + + if (!instance.config.display.pipe) { + fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout); + if (moduleArgs && moduleArgs->outputColor.length) { + ffPrintColor(&moduleArgs->outputColor); + } else if (instance.config.display.colorOutput.length) { + ffPrintColor(&instance.config.display.colorOutput); + } + } +} + +bool ffPrintFormat(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, uint32_t numArgs, const FFformatarg* arguments) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + bool success; + if (__builtin_expect(moduleArgs != NULL, 1)) { + success = ffParseFormatString(&buffer, &moduleArgs->outputFormat, numArgs, arguments); + } else { + ffStrbufSetStatic(&buffer, "undefined format"); + success = false; + } + + if (success) { + ffPrintLogoAndKey(moduleName, moduleIndex, moduleArgs, printType); + ffStrbufPutTo(&buffer, stdout); + } else { + ffPrintError(moduleName, moduleIndex, moduleArgs, printType, "%s", buffer.chars); + } + + return success; +} + +void ffPrintError(const char* moduleName, uint8_t moduleIndex, const FFModuleArgs* moduleArgs, FFPrintType printType, const char* message, ...) { + if (!instance.config.display.showErrors) { + return; + } + + ffPrintLogoAndKey(moduleName, moduleIndex, moduleArgs, printType); + + if (!instance.config.display.pipe) { + fputs(FASTFETCH_TEXT_MODIFIER_ERROR, stdout); + } + + va_list arguments; + va_start(arguments, message); + vprintf(message, arguments); + va_end(arguments); + + if (!instance.config.display.pipe) { + fputs(FASTFETCH_TEXT_MODIFIER_RESET, stdout); + } + + putchar('\n'); +} + +void ffPrintColor(const FFstrbuf* colorValue) { + // If the color is not set, this would reset in \033[m, which resets everything. + // So we only print it, if the main color is at least one char. + if (colorValue->length == 0) { + return; + } + + printf("\e[%sm", colorValue->chars); +} + +void ffPrintCharTimes(char c, uint32_t times) { + if (times == 0) { + return; + } + + if (times == 1) { + putchar(c); + return; + } + + char str[32]; + memset(str, c, sizeof(str)); // 2 instructions when compiling with AVX2 enabled + for (uint32_t i = sizeof(str); i <= times; i += (uint32_t) sizeof(str)) { + fwrite(str, 1, sizeof(str), stdout); + } + uint32_t remaining = times % sizeof(str); + if (remaining > 0) { + fwrite(str, 1, remaining, stdout); + } +} diff --git a/src/common/impl/processing_linux.c b/src/common/impl/processing_linux.c new file mode 100644 index 0000000..2b87c3b --- /dev/null +++ b/src/common/impl/processing_linux.c @@ -0,0 +1,672 @@ +#include "fastfetch.h" +#include "common/processing.h" +#include "common/io.h" +#include "common/strutil.h" +#include "common/mallocHelper.h" + +#include <stdlib.h> +#include <unistd.h> +#include <signal.h> +#include <poll.h> +#include <fcntl.h> +#include <errno.h> +#include <sys/wait.h> + +#if !(__ANDROID__ || __OpenBSD__) + #include <spawn.h> +#endif + +#if defined(__FreeBSD__) || defined(__APPLE__) + #include <sys/types.h> + #include <sys/user.h> + #include <sys/sysctl.h> +#endif +#if defined(__APPLE__) + #include <libproc.h> +#elif defined(__sun) + #include <procfs.h> +#elif defined(__OpenBSD__) + #include <sys/param.h> + #include <sys/sysctl.h> + #include <kvm.h> +#elif defined(__NetBSD__) + #include <sys/types.h> + #include <sys/sysctl.h> +#elif defined(__HAIKU__) + #include <OS.h> + #include <image.h> +#endif + +#ifndef environ +extern char** environ; +#endif + +enum { FF_PIPE_BUFSIZ = 8192 }; + +static inline int ffPipe2(int* fds, int flags) { +#ifndef FF_HAVE_PIPE2 + if (pipe(fds) == -1) { + return -1; + } + fcntl(fds[0], F_SETFL, fcntl(fds[0], F_GETFL) | flags); + fcntl(fds[1], F_SETFL, fcntl(fds[1], F_GETFL) | flags); + return 0; +#else + return pipe2(fds, flags); +#endif +} + +// Not thread-safe +const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle) { + int pipes[2]; + if (ffPipe2(pipes, O_CLOEXEC) == -1) { + return "pipe() failed"; + } + + pid_t childPid = -1; + int nullFile = ffGetNullFD(); + +#if !(__ANDROID__ || __OpenBSD__) + + // NetBSD / Darwin: native syscall + // Linux (glibc): clone3-execve + // FreeBSD: vfork-execve + // illumos: vforkx-execve + // OpenBSD / Android (bionic): fork-execve + + posix_spawn_file_actions_t file_actions; + posix_spawn_file_actions_init(&file_actions); + posix_spawn_file_actions_adddup2(&file_actions, pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO); + + static char* oldLang = NULL; + static int langIndex = -1; + + if (langIndex >= 0) { + // Found before + if (oldLang) // oldLang was set only if it needed to be changed + { + if (environ[langIndex] != oldLang) { + // environ is changed outside of this function + langIndex = -1; + } else { + environ[langIndex] = (char*) "LANG=C.UTF-8"; + } + } + } + if (langIndex < 0) { + for (int i = 0; environ[i] != NULL; i++) { + if (ffStrStartsWith(environ[i], "LANG=")) { + langIndex = i; + const char* langValue = environ[i] + 5; // Skip "LANG=" + if (ffStrEqualsIgnCase(langValue, "C") || + ffStrStartsWithIgnCase(environ[i], "C.") || + ffStrEqualsIgnCase(langValue, "en_US") || + ffStrStartsWithIgnCase(langValue, "en_US.")) { + break; // No need to change LANG + } + oldLang = environ[i]; + environ[i] = (char*) "LANG=C.UTF-8"; // Set LANG to C.UTF-8 for consistent output + break; + } + } + } + + int ret = posix_spawnp(&childPid, argv[0], &file_actions, NULL, argv, environ); + + if (oldLang) { + environ[langIndex] = oldLang; + } + + posix_spawn_file_actions_destroy(&file_actions); + + if (ret != 0) { + close(pipes[0]); + close(pipes[1]); + if (ret == ENOENT) { + return "command not found"; + } + return "posix_spawnp() failed"; + } + +#else + + // https://github.com/termux/termux-packages/issues/25369 + childPid = fork(); + if (childPid == -1) { + close(pipes[0]); + close(pipes[1]); + return "fork() failed"; + } + + if (childPid == 0) { + // Child process + dup2(pipes[1], useStdErr ? STDERR_FILENO : STDOUT_FILENO); + dup2(nullFile, useStdErr ? STDOUT_FILENO : STDERR_FILENO); + putenv("LANG=C.UTF-8"); + execvp(argv[0], argv); + _exit(127); + } + +#endif + + close(pipes[1]); + outHandle->pid = childPid; + outHandle->pipeRead = pipes[0]; + return NULL; +} + +const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) { + assert(handle->pipeRead != -1); + assert(handle->pid != -1); + + const int32_t timeout = instance.config.general.processingTimeout; + FF_AUTO_CLOSE_FD int childPipeFd = handle->pipeRead; + pid_t childPid = handle->pid; + handle->pipeRead = -1; + handle->pid = -1; + char str[FF_PIPE_BUFSIZ]; + + for (;;) { + if (timeout >= 0) { + struct pollfd pollfd = { childPipeFd, POLLIN, 0 }; + int pollret = poll(&pollfd, 1, timeout); + if (pollret == 0) { + kill(childPid, SIGTERM); + waitpid(childPid, NULL, 0); + return "poll(&pollfd, 1, timeout) timeout (try increasing --processing-timeout)"; + } else if (pollret < 0 || (pollfd.revents & POLLERR)) { + kill(childPid, SIGTERM); + waitpid(childPid, NULL, 0); + return pollret < 0 + ? "poll(&pollfd, 1, timeout) error: pollret < 0" + : "poll(&pollfd, 1, timeout) error: pollfd.revents & POLLERR"; + } + } + + ssize_t nRead = read(childPipeFd, str, FF_PIPE_BUFSIZ); + if (nRead > 0) { + ffStrbufAppendNS(buffer, (uint32_t) nRead, str); + } else if (nRead == 0) { + int stat_loc = 0; + if (childPid > 0 && waitpid(childPid, &stat_loc, 0) == childPid) { + if (!WIFEXITED(stat_loc)) { + return "child process exited abnormally"; + } + if (WEXITSTATUS(stat_loc) == 127) { + return "command not found"; + } + // We only handle 127 as an error. See `getTerminalVersionUrxvt` in `terminalshell.c` + return NULL; + } + return NULL; + } else if (nRead < 0) { + break; + } + } + + return "read(childPipeFd, str, FF_PIPE_BUFSIZ) failed"; +} + +void ffProcessGetInfoLinux(pid_t pid, FFstrbuf* processName, FFstrbuf* exe, const char** exeName, FFstrbuf* exePath) { + assert(processName->length > 0); + ffStrbufClear(exe); + if (exePath) { + ffStrbufClear(exePath); + } + +#if defined(__linux__) || defined(__GNU__) + + char filePath[64]; + snprintf(filePath, sizeof(filePath), "/proc/%d/cmdline", (int) pid); + + if (ffReadFileBuffer(filePath, exe)) { + const char* p = exe->chars; + uint32_t len = (uint32_t) strlen(p); + + if (len + 1 < exe->length) { + const char* name = memrchr(p, '/', len); + if (name) { + name++; + } else { + name = p; + } + + // For interpreters, try to find the real script path in the arguments + if (ffStrStartsWith(name, "python") + #ifndef __ANDROID__ + || ffStrEquals(name, "guile") // for shepherd + #endif + ) { + // `cmdline` always ends with a trailing '\0', and ffReadFileBuffer appends another \0 + // So `exe->chars` is always double '\0' terminated + for (p = p + len + 1; *p && *p == '-'; p += strlen(p) + 1) { // Skip arguments + assert(p - exe->chars < exe->allocated); + } + if (*p) { + len = (uint32_t) strlen(p); + memmove(exe->chars, p, len + 1); + } + } + } + + assert(len < exe->allocated); + exe->length = len; + ffStrbufTrimLeft(exe, '-'); // Login shells start with a dash + } + + if (exePath) { + snprintf(filePath, sizeof(filePath), "/proc/%d/exe", (int) pid); + char buf[PATH_MAX]; + ssize_t length = readlink(filePath, buf, PATH_MAX - 1); + if (length > 0) // doesn't contain trailing NUL + { + buf[length] = '\0'; + // When the process is a deleted executable, the resolved path is like `/usr/bin/app (deleted)` + // But we can still access the binary via `/proc/pid/exe`. See #2136 + if (ffPathExists(buf, FF_PATHTYPE_ANY)) { + ffStrbufSetNS(exePath, (uint32_t) length, buf); + } + } + + if (exePath->length == 0) { + ffStrbufSetS(exePath, filePath); + } + } + +#elif defined(__APPLE__) + + size_t len = 0; + int mibs[] = { CTL_KERN, KERN_PROCARGS2, pid }; + if (sysctl(mibs, ARRAY_SIZE(mibs), NULL, &len, NULL, 0) == 0) { // try get arg0 + // don't know why if don't let len longer, proArgs2 and len will change during the following sysctl() in old MacOS version. + len++; + FF_AUTO_FREE char* const procArgs2 = malloc(len); + if (sysctl(mibs, ARRAY_SIZE(mibs), procArgs2, &len, NULL, 0) == 0) { + // https://gist.github.com/nonowarn/770696#file-getargv-c-L46 + uint32_t argc = *(uint32_t*) procArgs2; + const char* realExePath = procArgs2 + sizeof(argc); + + const char* arg0 = memchr(realExePath, '\0', len - (size_t) (realExePath - procArgs2)); + if (exePath) { + ffStrbufSetNS(exePath, (uint32_t) (arg0 - realExePath), realExePath); + } + + do { + arg0++; + } while (*arg0 == '\0'); + assert(arg0 < procArgs2 + len); + + if (argc > 1) { + // #977 + const char* p = strrchr(arg0, '/'); + if (p) { + p++; + } else { + p = arg0; + } + if (ffStrStartsWithIgnCase(p, "python")) { // /opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/Resources/Python.app/Contents/MacOS/Python /Users/carter/.local/bin/xonsh + arg0 = p + strlen(p) + 1; + } + } + + if (*arg0 == '-') { + arg0++; // Login shells + } + + ffStrbufSetS(exe, arg0); + } + } + + if (exePath || exe->length == 0) { + char buf[PROC_PIDPATHINFO_MAXSIZE]; + int length = proc_pidpath(pid, buf, ARRAY_SIZE(buf)); + if (length > 0) { + if (exe->length == 0) { + ffStrbufSetNS(exe, (uint32_t) length, buf); + } + if (exePath) { + // We don't use exec_path above as exePath because it's a relative path and can be different + // from the actual executable being run (for example, when the original file is moved) + ffStrbufSetNS(exePath, (uint32_t) length, buf); + } + } + } + +#elif defined(__FreeBSD__) || defined(__NetBSD__) + + size_t size = ARG_MAX; + FF_AUTO_FREE char* args = malloc(size); + + static_assert(ARG_MAX > PATH_MAX, ""); + + if (exePath && sysctl((int[]) { CTL_KERN, + #if __FreeBSD__ + KERN_PROC, + KERN_PROC_PATHNAME, + pid + #else + KERN_PROC_ARGS, + pid, + KERN_PROC_PATHNAME + #endif + }, + 4, + args, + &size, + NULL, + 0) == 0) + ffStrbufSetNS(exePath, (uint32_t) (size - 1), args); + + size = ARG_MAX; + if (sysctl( + (int[]) { CTL_KERN, + #if __FreeBSD__ + KERN_PROC, + KERN_PROC_ARGS, + pid + #else + KERN_PROC_ARGS, + pid, + KERN_PROC_ARGV, + #endif + }, + 4, + args, + &size, + NULL, + 0) == 0) { + char* arg0 = args; + size_t arg0Len = strlen(args); + if (size > arg0Len + 1) { + char* p = (char*) memrchr(args, '/', arg0Len); + if (p) { + p++; + } else { + p = arg0; + } + if (ffStrStartsWith(p, "python")) // /usr/local/bin/python3.9 /home/carter/.local/bin/xonsh + { + arg0 += arg0Len + 1; + } + } + if (arg0[0] == '-') { + arg0++; + } + ffStrbufSetS(exe, arg0); + } + +#elif defined(__sun) + + char filePath[128]; + snprintf(filePath, sizeof(filePath), "/proc/%d/psinfo", (int) pid); + psinfo_t proc; + if (ffReadFileData(filePath, sizeof(proc), &proc) == sizeof(proc)) { + const char* args = proc.pr_psargs; + if (args[0] == '-') { + ++args; + } + const char* end = strchr(args, ' '); + ffStrbufSetNS(exe, end ? (uint32_t) (end - args) : (uint32_t) strlen(args), args); + } + + if (exePath) { + snprintf(filePath, sizeof(filePath), "/proc/%d/path/a.out", (int) pid); + char buf[PATH_MAX]; + ssize_t length = readlink(filePath, buf, PATH_MAX - 1); + if (length > 0) // doesn't contain trailing NUL + { + buf[length] = '\0'; + ffStrbufSetNS(exePath, (uint32_t) length, buf); + } + } + +#elif defined(__OpenBSD__) + + kvm_t* kd = kvm_open(NULL, NULL, NULL, KVM_NO_FILES, NULL); + int count = 0; + const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(struct kinfo_proc), &count); + if (proc) { + char** argv = kvm_getargv(kd, proc, 0); + if (argv) { + const char* arg0 = argv[0]; + if (arg0[0] == '-') { + arg0++; + } + ffStrbufSetS(exe, arg0); + } + } + kvm_close(kd); + +#elif defined(__HAIKU__) + + image_info info; + int32 cookie = 0; + + while (get_next_image_info(pid, &cookie, &info) == B_OK) { + if (info.type != B_APP_IMAGE) { + continue; + } + ffStrbufSetS(exe, info.name); + + if (exePath) { + ffStrbufSet(exePath, exe); + } + break; + } + +#endif + + if (exe->length == 0) { + ffStrbufSet(exe, processName); + } + + assert(exe->length > 0); + uint32_t lastSlashIndex = ffStrbufLastIndexC(exe, '/'); + if (lastSlashIndex < exe->length) { + *exeName = exe->chars + lastSlashIndex + 1; + } +} + +const char* ffProcessGetBasicInfoLinux(pid_t pid, FFstrbuf* name, pid_t* ppid, int32_t* tty) { + if (pid <= 0) { + return "Invalid pid"; + } + +#if defined(__linux__) || defined(__GNU__) + + char procFilePath[64]; + #if __linux__ + if (ppid || tty) + #endif + { + snprintf(procFilePath, sizeof(procFilePath), "/proc/%d/stat", (int) pid); + char buf[PROC_FILE_BUFFSIZ]; + ssize_t nRead = ffReadFileData(procFilePath, sizeof(buf) - 1, buf); + if (nRead <= 8) { + return "ffReadFileData(/proc/pid/stat, PROC_FILE_BUFFSIZ-1, buf) failed"; + } + buf[nRead] = '\0'; // pid (comm) state ppid pgrp session tty + + const char* pState = NULL; + + { + // comm in `/proc/pid/stat` is not encoded, and may contain ' ', ')' or even `\n` + const char* start = memchr(buf, '(', (size_t) nRead); + if (!start) { + return "memchr(stat, '(') failed"; + } + start++; + const char* end = memrchr(start, ')', (size_t) nRead - (size_t) (start - buf)); + if (!end) { + return "memrchr(stat, ')') failed"; + } + ffStrbufSetNS(name, (uint32_t) (end - start), start); + ffStrbufTrimRightSpace(name); + if (name->chars[0] == '\0') { + return "process name is empty"; + } + pState = end + 2; // skip ") " + } + + #if !__linux__ + if (ppid || tty) + #endif + { + int ppid_, tty_; + if (sscanf(pState + 2, "%d %*d %*d %d", &ppid_, &tty_) < 2) { + return "sscanf(stat) failed"; + } + + if (ppid) { + *ppid = (pid_t) ppid_; + } + if (tty) { + *tty = tty_ & 0xFF; + } + } + } + #if __linux__ + else { + snprintf(procFilePath, sizeof(procFilePath), "/proc/%d/comm", (int) pid); + ssize_t nRead = ffReadFileBuffer(procFilePath, name); + if (nRead <= 0) { + return "ffReadFileBuffer(/proc/pid/comm, name) failed"; + } + ffStrbufTrimRightSpace(name); + } + #endif + +#elif defined(__APPLE__) + + struct kinfo_proc proc; + size_t size = sizeof(proc); + if (sysctl( + (int[]) { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }, 4, &proc, &size, NULL, 0)) { + return "sysctl(KERN_PROC_PID) failed"; + } + + ffStrbufSetS(name, proc.kp_proc.p_comm); // trancated to 16 chars + if (ppid) { + *ppid = (pid_t) proc.kp_eproc.e_ppid; + } + if (tty) { + *tty = ((proc.kp_eproc.e_tdev >> 24) & 0xFF) == 0x10 + ? proc.kp_eproc.e_tdev & 0xFFFFFF + : -1; + } + +#elif defined(__FreeBSD__) + + #ifdef __DragonFly__ + #define ki_comm kp_comm + #define ki_ppid kp_ppid + #define ki_tdev kp_tdev + #define ki_flag kp_flags + #endif + + struct kinfo_proc proc; + size_t size = sizeof(proc); + if (sysctl( + (int[]) { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }, 4, &proc, &size, NULL, 0)) { + return "sysctl(KERN_PROC_PID) failed"; + } + + ffStrbufSetS(name, proc.ki_comm); + if (ppid) { + *ppid = (pid_t) proc.ki_ppid; + } + if (tty) { + if (proc.ki_tdev != NODEV && proc.ki_flag & P_CONTROLT) { + const char* ttyName = devname(proc.ki_tdev, S_IFCHR); + if (ffStrStartsWith(ttyName, "pts/")) { + *tty = (int32_t) strtol(ttyName + strlen("pts/"), NULL, 10); + } else { + *tty = -1; + } + } else { + *tty = -1; + } + } + +#elif defined(__NetBSD__) + + struct kinfo_proc2 proc; + size_t size = sizeof(proc); + if (sysctl( + (int[]) { CTL_KERN, KERN_PROC2, KERN_PROC_PID, pid, sizeof(proc), 1 }, 6, &proc, &size, NULL, 0) != 0) { + return "sysctl(KERN_PROC_PID) failed"; + } + + ffStrbufSetS(name, proc.p_comm); + if (ppid) { + *ppid = (pid_t) proc.p_ppid; + } + if (tty) { + if (proc.p_flag & P_CONTROLT) { + const char* ttyName = devname(proc.p_tdev, S_IFCHR); + if (ffStrStartsWith(ttyName, "pts/")) { + *tty = (int32_t) strtol(ttyName + strlen("pts/"), NULL, 10); + } else { + *tty = -1; + } + } else { + *tty = -1; + } + } + +#elif defined(__sun) + char path[128]; + snprintf(path, sizeof(path), "/proc/%d/psinfo", (int) pid); + psinfo_t proc; + if (ffReadFileData(path, sizeof(proc), &proc) != sizeof(proc)) { + return "ffReadFileData(psinfo) failed"; + } + + ffStrbufSetS(name, proc.pr_fname); + if (ppid) { + *ppid = proc.pr_ppid; + } + if (tty) { + *tty = (int) proc.pr_ttydev; + } + +#elif defined(__OpenBSD__) + + kvm_t* kd = kvm_open(NULL, NULL, NULL, KVM_NO_FILES, NULL); + int count = 0; + const struct kinfo_proc* proc = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(struct kinfo_proc), &count); + if (proc) { + ffStrbufSetS(name, proc->p_comm); + if (ppid) { + *ppid = proc->p_ppid; + } + if (tty) { + *tty = (int) proc->p_tdev; + } + } + kvm_close(kd); + if (!proc) { + return "kvm_getprocs() failed"; + } + +#elif defined(__HAIKU__) + + team_info info; + if (get_team_info(pid, &info) == B_OK) { + ffStrbufSetS(name, info.name); + if (ppid) { + *ppid = info.parent; + } + } + + FF_UNUSED(tty); + +#else + + return "Unsupported platform"; + +#endif + + return NULL; +} diff --git a/src/common/impl/processing_windows.c b/src/common/impl/processing_windows.c new file mode 100644 index 0000000..846276b --- /dev/null +++ b/src/common/impl/processing_windows.c @@ -0,0 +1,281 @@ +#include "fastfetch.h" +#include "common/mallocHelper.h" +#include "common/processing.h" +#include "common/io.h" +#include "common/windows/unicode.h" +#include "common/windows/nt.h" + +#include <stdalign.h> +#include <windows.h> +#include <ntstatus.h> + +enum { FF_PIPE_BUFSIZ = 8192 }; + +static void argvToCmdline(char* const argv[], FFstrbuf* result) { + // From https://gist.github.com/jin-x/cdd641d98887524b091fb1f82a68717d + + FF_STRBUF_AUTO_DESTROY temp = ffStrbufCreate(); + for (int i = 0; argv[i] != NULL; i++) { + ffStrbufSetS(&temp, argv[i]); + // Add slash (\) before double quotes (") and duplicate slashes before it + for ( + uint32_t pos = ffStrbufFirstIndexC(&temp, '"'), cnt; + pos != temp.length; + pos = ffStrbufNextIndexC(&temp, pos + cnt * 2, '"')) { + cnt = 1; + while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; } + ffStrbufInsertNC(&temp, pos, cnt, '\\'); + } + + // Add quotes around string if whitespace chars are present (with slash duplicating at the end of string) + if (ffStrbufFirstIndexS(&temp, " \t") != temp.length) { + uint32_t pos = temp.length; + uint32_t cnt = 0; + while (pos > 0 && temp.chars[pos - 1] == '\\') { ++cnt, --pos; } + if (cnt > 0) { + ffStrbufAppendNC(&temp, cnt, '\\'); + } + ffStrbufPrependC(&temp, '"'); + ffStrbufAppendC(&temp, '"'); + } + + // Add space delimiter + if (i > 0) { + ffStrbufAppendC(result, ' '); + } + ffStrbufAppend(result, &temp); + ffStrbufClear(&temp); + } +} + +const char* ffProcessSpawn(char* const argv[], bool useStdErr, FFProcessHandle* outHandle) { + const int32_t timeout = instance.config.general.processingTimeout; + + wchar_t pipeName[32]; + static unsigned pidCounter = 0; + swprintf(pipeName, ARRAY_SIZE(pipeName), L"\\\\.\\pipe\\FASTFETCH-%u-%u", instance.state.platform.pid, ++pidCounter); + + FF_AUTO_CLOSE_FD HANDLE hChildPipeRead = CreateNamedPipeW( + pipeName, + PIPE_ACCESS_INBOUND | FILE_FLAG_FIRST_PIPE_INSTANCE | (timeout < 0 ? 0 : FILE_FLAG_OVERLAPPED), + 0, + 1, + FF_PIPE_BUFSIZ, + FF_PIPE_BUFSIZ, + 0, + NULL); + if (hChildPipeRead == INVALID_HANDLE_VALUE) { + return "CreateNamedPipeW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed"; + } + + HANDLE hChildPipeWrite = CreateFileW( + pipeName, + GENERIC_WRITE, + 0, + &(SECURITY_ATTRIBUTES) { + .nLength = sizeof(SECURITY_ATTRIBUTES), + .lpSecurityDescriptor = NULL, + .bInheritHandle = TRUE, + }, + OPEN_EXISTING, + 0, + NULL); + if (hChildPipeWrite == INVALID_HANDLE_VALUE) { + return "CreateFileW(L\"\\\\.\\pipe\\FASTFETCH-$(PID)\") failed"; + } + + PROCESS_INFORMATION piProcInfo = {}; + STARTUPINFOW siStartInfo = { + .cb = sizeof(siStartInfo), + .dwFlags = STARTF_USESTDHANDLES, + }; + if (useStdErr) { + siStartInfo.hStdOutput = ffGetNullFD(); + siStartInfo.hStdError = hChildPipeWrite; + } else { + siStartInfo.hStdOutput = hChildPipeWrite; + siStartInfo.hStdError = ffGetNullFD(); + } + + FF_AUTO_FREE wchar_t* cmdline = NULL; + { + FF_STRBUF_AUTO_DESTROY buf = ffStrbufCreate(); + argvToCmdline(argv, &buf); + uint32_t cmdlineBytes = (buf.length + 1) * sizeof(wchar_t); + cmdline = malloc(cmdlineBytes); + if (!NT_SUCCESS(RtlUTF8ToUnicodeN(cmdline, cmdlineBytes, NULL, buf.chars, buf.length + 1))) { + return "RtlUTF8ToUnicodeN() failed"; + } + } + + BOOL success = CreateProcessW( + NULL, // application name + cmdline, // command line + NULL, // process security attributes + NULL, // primary thread security attributes + TRUE, // handles are inherited + 0, // creation flags + NULL, // use parent's environment + NULL, // use parent's current directory + &siStartInfo, // STARTUPINFO pointer + &piProcInfo // receives PROCESS_INFORMATION + ); + + NtClose(hChildPipeWrite); + if (!success) { + if (GetLastError() == ERROR_FILE_NOT_FOUND) { + return "command not found"; + } + return "CreateProcessW() failed"; + } + + NtClose(piProcInfo.hThread); // we don't need the thread handle + outHandle->pid = piProcInfo.hProcess; + outHandle->pipeRead = hChildPipeRead; + hChildPipeRead = INVALID_HANDLE_VALUE; // ownership transferred, don't close it + + return NULL; +} + +static void terminateChildProcess(HANDLE hProcess, HANDLE hChildPipeRead, HANDLE hReadEvent, IO_STATUS_BLOCK* piosb) { + IO_STATUS_BLOCK cancelIosb = {}; + if (NT_SUCCESS(NtCancelIoFileEx(hChildPipeRead, piosb, &cancelIosb))) { + if (hReadEvent) { + NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = -100000 }); // wait for cancellation to complete + } + } + NtTerminateProcess(hProcess, 1); +} + +const char* ffProcessReadOutput(FFProcessHandle* handle, FFstrbuf* buffer) { + assert(handle->pipeRead != INVALID_HANDLE_VALUE); + assert(handle->pid != INVALID_HANDLE_VALUE); + + int32_t timeout = instance.config.general.processingTimeout; + FF_AUTO_CLOSE_FD HANDLE hProcess = handle->pid; + FF_AUTO_CLOSE_FD HANDLE hChildPipeRead = handle->pipeRead; + FF_AUTO_CLOSE_FD HANDLE hReadEvent = NULL; + handle->pid = INVALID_HANDLE_VALUE; + handle->pipeRead = INVALID_HANDLE_VALUE; + + if (timeout >= 0 && !NT_SUCCESS(NtCreateEvent(&hReadEvent, EVENT_ALL_ACCESS, NULL, SynchronizationEvent, FALSE))) { + return "NtCreateEvent() failed"; + } + + char str[FF_PIPE_BUFSIZ]; + uint32_t nRead = 0; + IO_STATUS_BLOCK iosb = {}; + do { + NTSTATUS status = NtReadFile( + hChildPipeRead, + hReadEvent, + NULL, + NULL, + &iosb, + str, + (ULONG) sizeof(str), + NULL, + NULL); + if (status == STATUS_PENDING) { + switch (NtWaitForSingleObject(hReadEvent, FALSE, &(LARGE_INTEGER) { .QuadPart = (int64_t) timeout * -10000 })) { + case STATUS_WAIT_0: + status = iosb.Status; + break; + + case STATUS_TIMEOUT: { + terminateChildProcess(hProcess, hChildPipeRead, hReadEvent, &iosb); + return "NtReadFile(hChildPipeRead) timed out"; + } + + default: + terminateChildProcess(hProcess, hChildPipeRead, hReadEvent, &iosb); + return "NtWaitForSingleObject(hReadEvent) failed"; + } + } + + if (status == STATUS_PIPE_BROKEN || status == STATUS_END_OF_FILE) { + goto exit; + } + + if (!NT_SUCCESS(status)) { + terminateChildProcess(hProcess, hChildPipeRead, NULL, &iosb); + return "NtReadFile(hChildPipeRead) failed"; + } + + nRead = (uint32_t) iosb.Information; + ffStrbufAppendNS(buffer, nRead, str); + } while (nRead > 0); + +exit: { + PROCESS_BASIC_INFORMATION info = {}; + ULONG size; + if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size))) { + assert(size == sizeof(info)); + if (info.ExitStatus != STILL_ACTIVE && info.ExitStatus != 0) { + return "Child process exited with an error"; + } + } else { + return "NtQueryInformationProcess(ProcessBasicInformation) failed"; + } +} + + return NULL; +} + +bool ffProcessGetInfoWindows(uint32_t pid, uint32_t* ppid, FFstrbuf* pname, FFstrbuf* exe, const char** exeName, FFstrbuf* exePath, bool* gui) { + FF_AUTO_CLOSE_FD HANDLE hProcess = NtCurrentProcess(); + if (pid != 0) { + if (!NT_SUCCESS(NtOpenProcess(&hProcess, PROCESS_QUERY_LIMITED_INFORMATION, &(OBJECT_ATTRIBUTES) { + .Length = sizeof(OBJECT_ATTRIBUTES), + }, + &(CLIENT_ID) { .UniqueProcess = (HANDLE) (uintptr_t) pid }))) { + return false; + } + } + + if (ppid) { + PROCESS_BASIC_INFORMATION info = {}; + ULONG size; + if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessBasicInformation, &info, sizeof(info), &size))) { + assert(size == sizeof(info)); + *ppid = (uint32_t) info.InheritedFromUniqueProcessId; + } else { + return false; + } + } + + if (exe) { + // TODO: It's possible to query the command line with `NtQueryInformationProcess(60/*ProcessCommandLineInformation*/)` since Windows 8.1 + + alignas(UNICODE_STRING) uint8_t buffer[4096]; + ULONG size; + if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageFileNameWin32, &buffer, sizeof(buffer), &size))) { + UNICODE_STRING* imagePath = (UNICODE_STRING*) buffer; + ffStrbufSetNWS(exe, imagePath->Length / sizeof(wchar_t), imagePath->Buffer); + + if (exePath) { + ffStrbufSet(exePath, exe); + } + + if (pname && exeName) { + *exeName = exe->chars + ffStrbufLastIndexC(exe, '\\') + 1; + ffStrbufSetS(pname, *exeName); + } + } else { + return false; + } + } + + if (gui) { + SECTION_IMAGE_INFORMATION info = {}; + ULONG size; + if (NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageInformation, &info, sizeof(info), &size))) { + assert(size == sizeof(info)); + *gui = info.SubSystemType == IMAGE_SUBSYSTEM_WINDOWS_GUI; + } else { + return false; + } + } + + return true; +} diff --git a/src/common/impl/properties.c b/src/common/impl/properties.c new file mode 100644 index 0000000..c13ed1c --- /dev/null +++ b/src/common/impl/properties.c @@ -0,0 +1,172 @@ +#include "fastfetch.h" +#include "common/properties.h" +#include "common/io.h" +#include "common/mallocHelper.h" + +#include <stdlib.h> +#include <ctype.h> +#ifdef _WIN32 + #include "common/windows/getline.h" +#endif + +bool ffParsePropLinePointer(const char** line, const char* start, FFstrbuf* buffer) { + if (**line == '\0') { + return false; + } + + // Skip any amount of whitespace at the begin of line + while (**line == ' ' || **line == '\t') { + ++(*line); + } + + while (*start != '\0') { + // Any amount of whitespace in the format string matches any amount of whitespace in the line, even none + if (*start == ' ' || *start == '\t') { + while (*start == ' ' || *start == '\t') { + ++start; + } + + while (**line == ' ' || **line == '\t') { + ++(*line); + } + + continue; + } + + // Line doesn't match start, skip it + if (tolower(**line) != tolower(*start) || **line == '\0') { + return false; + } + + // Line and start match, continue testing + ++(*line); + ++start; + } + + char valueEnd = '\n'; + + // Allow faster parsing of XML + if (*(*line - 1) == '>') { + valueEnd = '<'; + } + + // Skip any amount of whitespace at the begin of the value + while (**line == ' ' || **line == '\t') { + ++(*line); + } + + // Allow faster parsing of quoted values + if (**line == '"' || **line == '\'') { + valueEnd = **line; + ++(*line); + } + + // Copy the value to the buffer + while (**line != valueEnd && **line != '\n' && **line != '\0') { + ffStrbufAppendC(buffer, **line); + ++(*line); + } + + ffStrbufTrimRight(buffer, ' '); + + return true; +} + +bool ffParsePropLines(const char* lines, const char* start, FFstrbuf* buffer) { + while (!ffParsePropLinePointer(&lines, start, buffer)) { + while (*lines != '\0' && *lines != '\n') { + ++lines; + } + + if (*lines == '\0') { + return false; + } + + // Skip '\n' + ++lines; + } + + return true; +} + +// The following functions return true if the file was found, independently if start was found +// Buffers which already contain content are not overwritten +// The last occurrence of start in the first file will be the one used + +bool ffParsePropFileValues(const char* filename, uint32_t numQueries, FFpropquery* queries) { + FF_AUTO_CLOSE_FILE FILE* file = fopen(filename, "r"); + if (file == NULL) { + return false; + } + + bool valueStorage[32]; + bool* unsetValues = valueStorage; + + if (numQueries > ARRAY_SIZE(valueStorage)) { + unsetValues = malloc(sizeof(bool) * numQueries); + } + + bool allSet = true; + for (uint32_t i = 0; i < numQueries; i++) { + unsetValues[i] = queries[i].buffer->length == 0; + if (unsetValues[i]) { + allSet = false; + } + } + + if (!allSet) { + FF_AUTO_FREE char* line = NULL; + size_t len = 0; + + while (getline(&line, &len, file) != -1) { + for (uint32_t i = 0; i < numQueries; i++) { + if (!unsetValues[i]) { + continue; + } + + uint32_t currentLength = queries[i].buffer->length; + queries[i].buffer->length = 0; + if (!ffParsePropLine(line, queries[i].start, queries[i].buffer)) { + queries[i].buffer->length = currentLength; + } + } + } + } + + if (unsetValues != valueStorage) { + free(unsetValues); + } + return true; +} + +bool ffParsePropFileHomeValues(const char* relativeFile, uint32_t numQueries, FFpropquery* queries) { + FF_STRBUF_AUTO_DESTROY absolutePath = ffStrbufCreateF("%s/%s", instance.state.platform.homeDir.chars, relativeFile); + return ffParsePropFileValues(absolutePath.chars, numQueries, queries); +} + +bool ffParsePropFileListValues(const FFlist* list, const char* relativeFile, uint32_t numQueries, FFpropquery* queries) { + bool foundAFile = false; + + FF_LIST_FOR_EACH (FFstrbuf, dirPrefix, *list) { + const uint32_t dirPrefixLength = dirPrefix->length; + ffStrbufAppendS(dirPrefix, relativeFile); + if (ffParsePropFileValues(dirPrefix->chars, numQueries, queries)) { + foundAFile = true; + } + ffStrbufSubstrBefore(dirPrefix, dirPrefixLength); + + bool allSet = true; + for (uint32_t k = 0; k < numQueries; k++) { + if (queries[k].buffer->length == 0) { + allSet = false; + break; + } + } + + if (allSet) { + break; + } + } + + return foundAFile; +} diff --git a/src/common/impl/settings.c b/src/common/impl/settings.c new file mode 100644 index 0000000..e7d246b --- /dev/null +++ b/src/common/impl/settings.c @@ -0,0 +1,609 @@ +#include "fastfetch.h" +#include "common/settings.h" +#include "common/library.h" +#include "common/thread.h" +#include "common/io.h" + +#include <string.h> + +#ifdef FF_HAVE_GIO + #include <gio/gio.h> + +typedef struct GVariantGetters { + FF_LIBRARY_SYMBOL(g_variant_dup_string) + FF_LIBRARY_SYMBOL(g_variant_get_boolean) + FF_LIBRARY_SYMBOL(g_variant_get_int32) + FF_LIBRARY_SYMBOL(g_variant_unref) +} GVariantGetters; + +static FFvariant getGVariantValue(GVariant* variant, FFvarianttype type, const GVariantGetters* variantGetters) { + FFvariant result; + + if (variant == NULL) { + result = FF_VARIANT_NULL; + } else if (type == FF_VARIANT_TYPE_STRING) { + result = (FFvariant) { .strValue = variantGetters->ffg_variant_dup_string(variant, NULL) }; // Dup string, so that variant itself can be freed + } else if (type == FF_VARIANT_TYPE_BOOL) { + result = (FFvariant) { .boolValue = (bool) variantGetters->ffg_variant_get_boolean(variant), .boolValueSet = true }; + } else if (type == FF_VARIANT_TYPE_INT) { + result = (FFvariant) { .intValue = variantGetters->ffg_variant_get_int32(variant) }; + } else { + result = FF_VARIANT_NULL; + } + + if (variant) { + variantGetters->ffg_variant_unref(variant); + } + + return result; +} + +typedef struct GSettingsData { + FF_LIBRARY_SYMBOL(g_settings_schema_source_lookup) + FF_LIBRARY_SYMBOL(g_settings_schema_has_key) + FF_LIBRARY_SYMBOL(g_settings_new_full) + FF_LIBRARY_SYMBOL(g_settings_get_value) + FF_LIBRARY_SYMBOL(g_settings_get_user_value) + FF_LIBRARY_SYMBOL(g_settings_get_default_value) + FF_LIBRARY_SYMBOL(g_settings_schema_source_get_default) + GSettingsSchemaSource* schemaSource; + GVariantGetters variantGetters; + + bool inited; +} GSettingsData; + +static const GSettingsData* getGSettingsData(void) { + static GSettingsData data; + + if (!data.inited) { + data.inited = true; + FF_LIBRARY_LOAD(libgsettings, NULL, "libgio-2.0" FF_LIBRARY_EXTENSION, 1); + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_schema_source_lookup, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_schema_has_key, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_new_full, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_get_value, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_get_user_value, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_get_default_value, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data, g_settings_schema_source_get_default, NULL) + + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data.variantGetters, g_variant_dup_string, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data.variantGetters, g_variant_get_boolean, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data.variantGetters, g_variant_get_int32, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libgsettings, data.variantGetters, g_variant_unref, NULL); + + data.schemaSource = data.ffg_settings_schema_source_get_default(); + if (data.schemaSource) { + libgsettings = NULL; + } + } + if (!data.schemaSource) { + return NULL; + } + + return &data; +} + +FFvariant ffSettingsGetGSettings(const char* schemaName, const char* path, const char* key, FFvarianttype type) { + const GSettingsData* data = getGSettingsData(); + if (data == NULL) { + return FF_VARIANT_NULL; + } + + GSettingsSchema* schema = data->ffg_settings_schema_source_lookup(data->schemaSource, schemaName, true); + if (schema == NULL) { + return FF_VARIANT_NULL; + } + + if (data->ffg_settings_schema_has_key(schema, key) == false) { + return FF_VARIANT_NULL; + } + + GSettings* settings = data->ffg_settings_new_full(schema, NULL, path); + if (settings == NULL) { + return FF_VARIANT_NULL; + } + + GVariant* variant = data->ffg_settings_get_value(settings, key); + if (variant != NULL) { + return getGVariantValue(variant, type, &data->variantGetters); + } + + variant = data->ffg_settings_get_user_value(settings, key); + if (variant != NULL) { + return getGVariantValue(variant, type, &data->variantGetters); + } + + variant = data->ffg_settings_get_default_value(settings, key); + return getGVariantValue(variant, type, &data->variantGetters); +} +#else // FF_HAVE_GIO +FFvariant ffSettingsGetGSettings(const char* schemaName, const char* path, const char* key, FFvarianttype type) { + FF_UNUSED(schemaName, path, key, type) + return FF_VARIANT_NULL; +} +#endif // FF_HAVE_GIO + +#ifdef FF_HAVE_DCONF + #include <dconf.h> + +typedef struct DConfData { + FF_LIBRARY_SYMBOL(dconf_client_read_full) + FF_LIBRARY_SYMBOL(dconf_client_new) + GVariantGetters variantGetters; + DConfClient* client; + + bool inited; +} DConfData; + +static const DConfData* getDConfData(void) { + static DConfData data; + + if (!data.inited) { + data.inited = true; + + FF_LIBRARY_LOAD(libdconf, NULL, "libdconf" FF_LIBRARY_EXTENSION, 2); + FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data, dconf_client_read_full, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data, dconf_client_new, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data.variantGetters, g_variant_dup_string, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data.variantGetters, g_variant_get_boolean, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data.variantGetters, g_variant_get_int32, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libdconf, data.variantGetters, g_variant_unref, NULL) + + data.client = data.ffdconf_client_new(); + if (data.client) { + libdconf = NULL; + } + } + if (!data.client) { + return NULL; + } + + return &data; +} + +FFvariant ffSettingsGetDConf(const char* key, FFvarianttype type) { + const DConfData* data = getDConfData(); + if (data == NULL) { + return FF_VARIANT_NULL; + } + + GVariant* variant = data->ffdconf_client_read_full(data->client, key, DCONF_READ_FLAGS_NONE, NULL); + if (variant != NULL) { + return getGVariantValue(variant, type, &data->variantGetters); + } + + variant = data->ffdconf_client_read_full(data->client, key, DCONF_READ_USER_VALUE, NULL); + if (variant != NULL) { + return getGVariantValue(variant, type, &data->variantGetters); + } + + variant = data->ffdconf_client_read_full(data->client, key, DCONF_READ_DEFAULT_VALUE, NULL); + return getGVariantValue(variant, type, &data->variantGetters); +} +#else // FF_HAVE_DCONF +FFvariant ffSettingsGetDConf(const char* key, FFvarianttype type) { + FF_UNUSED(key, type) + return FF_VARIANT_NULL; +} +#endif // FF_HAVE_DCONF + +FFvariant ffSettingsGetGnome(const char* dconfKey, const char* gsettingsSchemaName, const char* gsettingsPath, const char* gsettingsKey, FFvarianttype type) { + FFvariant gsettings = ffSettingsGetGSettings(gsettingsSchemaName, gsettingsPath, gsettingsKey, type); + + if ( + (type == FF_VARIANT_TYPE_BOOL && gsettings.boolValueSet) || + (type != FF_VARIANT_TYPE_BOOL && gsettings.strValue != NULL)) { + return gsettings; + } + + return ffSettingsGetDConf(dconfKey, type); +} + +#ifdef FF_HAVE_DBUS + #include "common/dbus.h" + +FFvariant ffSettingsGetXFConf(const char* channelName, const char* propertyName, FFvarianttype type) { + FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; + if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != NULL) { + return FF_VARIANT_NULL; + } + + DBusMessage* reply = ffDBusGetMethodReply(&dbus, "org.xfce.Xfconf", "/org/xfce/Xfconf", "org.xfce.Xfconf", "GetProperty", channelName, propertyName); + if (!reply) { + return FF_VARIANT_NULL; + } + + DBusMessageIter rootIterator; + if (!dbus.lib->ffdbus_message_iter_init(reply, &rootIterator)) { + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; + } + + if (type == FF_VARIANT_TYPE_INT) { + int64_t value; + if (ffDBusGetInt(&dbus, &rootIterator, &value)) { + dbus.lib->ffdbus_message_unref(reply); + return (FFvariant) { .intValue = (int32_t) value }; + } + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; + } + + if (type == FF_VARIANT_TYPE_STRING) { + FFstrbuf value = ffStrbufCreate(); + if (ffDBusGetString(&dbus, &rootIterator, &value)) { + dbus.lib->ffdbus_message_unref(reply); + return (FFvariant) { .strValue = value.chars }; // Leaks value.chars + } + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; + } + + if (type == FF_VARIANT_TYPE_BOOL) { + bool value; + if (ffDBusGetBool(&dbus, &rootIterator, &value)) { + dbus.lib->ffdbus_message_unref(reply); + return (FFvariant) { .boolValue = value, .boolValueSet = true }; + } + } + + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; +} + + #define FF_DBUS_ITER_CONTINUE(dbus, iterator) \ + { \ + if (!(dbus).lib->ffdbus_message_iter_next(iterator)) \ + break; \ + continue; \ + } + +FFvariant ffSettingsGetXFConfFirstMatch(const char* channelName, const char* propertyPrefix, FFvarianttype type, void* data, FFTestXfconfPropCallback* cb) { + FF_DBUS_AUTO_DESTROY_DATA FFDBusData dbus = {}; + if (ffDBusLoadData(DBUS_BUS_SESSION, &dbus) != NULL) { + return FF_VARIANT_NULL; + } + + DBusMessage* reply = ffDBusGetMethodReply(&dbus, "org.xfce.Xfconf", "/org/xfce/Xfconf", "org.xfce.Xfconf", "GetAllProperties", channelName, propertyPrefix); + if (!reply) { + return FF_VARIANT_NULL; + } + + DBusMessageIter rootIterator; + if (!dbus.lib->ffdbus_message_iter_init(reply, &rootIterator)) { + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; + } + + DBusMessageIter arrayIterator; + dbus.lib->ffdbus_message_iter_recurse(&rootIterator, &arrayIterator); + + while (true) { + if (dbus.lib->ffdbus_message_iter_get_arg_type(&arrayIterator) != DBUS_TYPE_DICT_ENTRY) { + FF_DBUS_ITER_CONTINUE(dbus, &arrayIterator) + } + + DBusMessageIter dictIterator; + dbus.lib->ffdbus_message_iter_recurse(&arrayIterator, &dictIterator); + + const char* key; + dbus.lib->ffdbus_message_iter_get_basic(&dictIterator, &key); + + if (cb(data, key)) { + FF_DBUS_ITER_CONTINUE(dbus, &arrayIterator) + } + dbus.lib->ffdbus_message_iter_next(&dictIterator); + + if (type == FF_VARIANT_TYPE_INT) { + int64_t value; + if (ffDBusGetInt(&dbus, &dictIterator, &value)) { + dbus.lib->ffdbus_message_unref(reply); + return (FFvariant) { .intValue = (int32_t) value }; + } + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; + } + + if (type == FF_VARIANT_TYPE_STRING) { + FFstrbuf value = ffStrbufCreate(); + if (ffDBusGetString(&dbus, &dictIterator, &value)) { + dbus.lib->ffdbus_message_unref(reply); + return (FFvariant) { .strValue = value.chars }; // Leaks value.chars + } + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; + } + + if (type == FF_VARIANT_TYPE_BOOL) { + bool value; + if (ffDBusGetBool(&dbus, &dictIterator, &value)) { + dbus.lib->ffdbus_message_unref(reply); + return (FFvariant) { .boolValue = value, .boolValueSet = true }; + } + } + + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; + } + + dbus.lib->ffdbus_message_unref(reply); + return FF_VARIANT_NULL; +} +#else // FF_HAVE_DBUS +FFvariant ffSettingsGetXFConf(const char* channelName, const char* propertyName, FFvarianttype type) { + FF_UNUSED(channelName, propertyName, type) + return FF_VARIANT_NULL; +} +FFvariant ffSettingsGetXFConfFirstMatch(const char* channelName, const char* propertyPrefix, FFvarianttype type, void* data, FFTestXfconfPropCallback* cb) { + FF_UNUSED(channelName, propertyPrefix, type, data, cb); + return FF_VARIANT_NULL; +} +#endif // FF_HAVE_DBUS + +#ifdef FF_HAVE_SQLITE3 + #include <sqlite3.h> + +typedef struct SQLiteData { + FF_LIBRARY_SYMBOL(sqlite3_open_v2) + FF_LIBRARY_SYMBOL(sqlite3_prepare_v2) + FF_LIBRARY_SYMBOL(sqlite3_step) + FF_LIBRARY_SYMBOL(sqlite3_data_count) + FF_LIBRARY_SYMBOL(sqlite3_column_int) + FF_LIBRARY_SYMBOL(sqlite3_column_text) + FF_LIBRARY_SYMBOL(sqlite3_finalize) + FF_LIBRARY_SYMBOL(sqlite3_close) + + bool inited; +} SQLiteData; + +static const SQLiteData* getSQLiteData(void) { + static SQLiteData data; + + if (!data.inited) { + data.inited = true; + FF_LIBRARY_LOAD(libsqlite, NULL, "libsqlite3" FF_LIBRARY_EXTENSION, 1); + FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_open_v2, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_prepare_v2, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_step, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_data_count, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_column_int, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_column_text, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_finalize, NULL) + FF_LIBRARY_LOAD_SYMBOL_VAR(libsqlite, data, sqlite3_close, NULL) + libsqlite = NULL; + } + + if (!data.ffsqlite3_close) { + return NULL; + } + + return &data; +} + +int ffSettingsGetSQLite3Int(const char* dbPath, const char* query) { + if (!ffPathExists(dbPath, FF_PATHTYPE_FILE)) { + return 0; + } + + const SQLiteData* data = getSQLiteData(); + if (data == NULL) { + return 0; + } + + sqlite3* db; + if (data->ffsqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { + return 0; + } + + sqlite3_stmt* stmt; + if (data->ffsqlite3_prepare_v2(db, query, (int) strlen(query), &stmt, NULL) != SQLITE_OK) { + data->ffsqlite3_close(db); + return 0; + } + + if (data->ffsqlite3_step(stmt) != SQLITE_ROW || data->ffsqlite3_data_count(stmt) < 1) { + data->ffsqlite3_finalize(stmt); + data->ffsqlite3_close(db); + return 0; + } + + int result = data->ffsqlite3_column_int(stmt, 0); + + data->ffsqlite3_finalize(stmt); + data->ffsqlite3_close(db); + + return result; +} + +bool ffSettingsGetSQLite3String(const char* dbPath, const char* query, FFstrbuf* result) { + if (!ffPathExists(dbPath, FF_PATHTYPE_FILE)) { + return false; + } + + const SQLiteData* data = getSQLiteData(); + if (data == NULL) { + return false; + } + + sqlite3* db; + if (data->ffsqlite3_open_v2(dbPath, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) { + return false; + } + + sqlite3_stmt* stmt; + if (data->ffsqlite3_prepare_v2(db, query, (int) strlen(query), &stmt, NULL) != SQLITE_OK) { + data->ffsqlite3_close(db); + return false; + } + + if (data->ffsqlite3_step(stmt) != SQLITE_ROW || data->ffsqlite3_data_count(stmt) < 1) { + data->ffsqlite3_finalize(stmt); + data->ffsqlite3_close(db); + return false; + } + + ffStrbufSetS(result, (const char*) data->ffsqlite3_column_text(stmt, 0)); + + data->ffsqlite3_finalize(stmt); + data->ffsqlite3_close(db); + + return true; +} +#else // FF_HAVE_SQLITE3 +int ffSettingsGetSQLite3Int(const char* dbPath, const char* query) { + FF_UNUSED(dbPath, query) + return 0; +} +bool ffSettingsGetSQLite3String(const char* dbPath, const char* query, FFstrbuf* result) { + FF_UNUSED(dbPath, query, result) + return false; +} +#endif // FF_HAVE_SQLITE3 + +#ifdef __ANDROID__ + #include <sys/system_properties.h> +bool ffSettingsGetAndroidProperty(const char* propName, FFstrbuf* result) { + ffStrbufEnsureFree(result, PROP_VALUE_MAX); + int len = __system_property_get(propName, result->chars + result->length); + if (len <= 0) { + return false; + } + result->length += (uint32_t) len; + result->chars[result->length] = '\0'; + return true; +} +#elif defined(__FreeBSD__) + #include <kenv.h> +bool ffSettingsGetFreeBSDKenv(const char* propName, FFstrbuf* result) { + // https://wiki.ghostbsd.org/index.php/Kenv + ffStrbufEnsureFree(result, KENV_MVALLEN); + int len = kenv(KENV_GET, propName, result->chars + result->length, KENV_MVALLEN); + if (len <= 1) { + return false; // number of bytes copied, including NUL terminator + } + result->length += (uint32_t) len - 1; + return true; +} +#endif + +#ifdef FF_HAVE_EET + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wconversion" + #pragma GCC diagnostic ignored "-Wsign-conversion" + #pragma GCC diagnostic ignored "-Wfloat-conversion" + #include <Eet.h> + #pragma GCC diagnostic pop + +typedef struct E_Font_Default { + char* text_class; + char* font; + int size; +} E_Font_Default; + +typedef struct E_Config { + char* theme_default_border_style; + char* icon_theme; + int use_e_cursor; + int cursor_size; + char* desktop_default_background; + Eina_List* font_defaults; +} E_Config; // Must be the same name as the top level struct in e.cfg + + #define FF_EET_EINA_FILE_DATA_DESCRIPTOR_CLASS_SET(clas, type) \ + (ffeet_eina_file_data_descriptor_class_set(clas, sizeof(*(clas)), #type, sizeof(type))) + #define FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, struct_type, member, type) \ + do { \ + struct_type ___ett; \ + ffeet_data_descriptor_element_add(edd, #member, type, EET_G_UNKNOWN, (char*) (&(___ett.member)) - (char*) (&(___ett)), 0, /* 0, */ NULL, NULL); \ + } while (0) + #define FF_EET_DATA_DESCRIPTOR_ADD_LIST(edd, struct_type, member, subtype) \ + do { \ + struct_type ___ett; \ + ffeet_data_descriptor_element_add(edd, #member, EET_T_UNKNOW, EET_G_LIST, (char*) (&(___ett.member)) - (char*) (&(___ett)), 0, /* 0, */ NULL, subtype); \ + } while (0) + +bool ffSettingsGetEnlightenmentProperty(ffEnlightenmentSettings* result) { + FF_LIBRARY_LOAD(libeet, false, "libeet" FF_LIBRARY_EXTENSION, 1); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_init, false); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_open, false); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_data_descriptor_file_new, false); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_data_read, false); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_close, false); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_shutdown, false); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_data_descriptor_free, false); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_eina_file_data_descriptor_class_set, false); + FF_LIBRARY_LOAD_SYMBOL(libeet, eet_data_descriptor_element_add, false); + + if (ffeet_init() == 0) { + return false; + } + + FF_STRBUF_AUTO_DESTROY fileName = ffStrbufCreateCopy(&instance.state.platform.homeDir); + ffStrbufAppendS(&fileName, ".e/e/config/standard/e.cfg"); + + Eet_File* ef = ffeet_open(fileName.chars, EET_FILE_MODE_READ); + if (!ef) { + ffeet_shutdown(); + return false; + } + + Eet_Data_Descriptor_Class fontDdc; + FF_EET_EINA_FILE_DATA_DESCRIPTOR_CLASS_SET(&fontDdc, E_Font_Default); + Eet_Data_Descriptor* fontDdd = ffeet_data_descriptor_file_new(&fontDdc); + if (!fontDdd) { + ffeet_close(ef); + ffeet_shutdown(); + return false; + } + FF_EET_DATA_DESCRIPTOR_ADD_BASIC(fontDdd, E_Font_Default, text_class, EET_T_STRING); + FF_EET_DATA_DESCRIPTOR_ADD_BASIC(fontDdd, E_Font_Default, font, EET_T_STRING); + FF_EET_DATA_DESCRIPTOR_ADD_BASIC(fontDdd, E_Font_Default, size, EET_T_INT); + + Eet_Data_Descriptor_Class eddc; + FF_EET_EINA_FILE_DATA_DESCRIPTOR_CLASS_SET(&eddc, E_Config); + Eet_Data_Descriptor* edd = ffeet_data_descriptor_file_new(&eddc); + if (!edd) { + ffeet_data_descriptor_free(fontDdd); + ffeet_close(ef); + ffeet_shutdown(); + return false; + } + + FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, theme_default_border_style, EET_T_STRING); + FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, icon_theme, EET_T_STRING); + FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, use_e_cursor, EET_T_INT); + FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, cursor_size, EET_T_INT); + FF_EET_DATA_DESCRIPTOR_ADD_BASIC(edd, E_Config, desktop_default_background, EET_T_STRING); + FF_EET_DATA_DESCRIPTOR_ADD_LIST(edd, E_Config, font_defaults, fontDdd); + + E_Config* parsed = ffeet_data_read(ef, edd, "config"); + + if (parsed) { + // TODO: find a better method to get the main theme name + result->theme = parsed->theme_default_border_style; + result->icon_theme = parsed->icon_theme; + result->use_e_cursor = !!parsed->use_e_cursor; + result->cursor_size = parsed->cursor_size; + result->desktop_default_background = parsed->desktop_default_background; + + E_Font_Default* firstFont = eina_list_data_get(parsed->font_defaults); + if (firstFont) { + result->font = firstFont->font; + } + } + + ffeet_close(ef); + ffeet_data_descriptor_free(edd); + ffeet_data_descriptor_free(fontDdd); + if (!parsed) { + // We don't shutdown eet so that `result->*` are not freed + ffeet_shutdown(); + } + + return !!parsed; +} +#else +bool ffSettingsGetEnlightenmentProperty(FF_A_UNUSED ffEnlightenmentSettings* result) { + return false; +} +#endif diff --git a/src/common/impl/size.c b/src/common/impl/size.c new file mode 100644 index 0000000..4e8d1e3 --- /dev/null +++ b/src/common/impl/size.c @@ -0,0 +1,42 @@ +#include "common/size.h" + +#include <inttypes.h> + +static void appendNum(FFstrbuf* result, uint64_t bytes, uint32_t base, const char** prefixes) { + const FFOptionsDisplay* options = &instance.config.display; + double size = (double) bytes; + uint8_t counter = 0; + + while (size >= base && counter < options->sizeMaxPrefix && prefixes[counter + 1]) { + size /= base; + counter++; + } + + if (counter == 0) { + ffStrbufAppendUInt(result, bytes); + } else { + ffStrbufAppendDouble(result, size, (int8_t) options->sizeNdigits, true); + } + if (options->sizeSpaceBeforeUnit != FF_SPACE_BEFORE_UNIT_NEVER) { + ffStrbufAppendC(result, ' '); + } + ffStrbufAppendS(result, prefixes[counter]); +} + +void ffSizeAppendNum(uint64_t bytes, FFstrbuf* result) { + const FFOptionsDisplay* options = &instance.config.display; + switch (options->sizeBinaryPrefix) { + case FF_SIZE_BINARY_PREFIX_TYPE_IEC: + appendNum(result, bytes, 1024, (const char*[]) { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", NULL }); + break; + case FF_SIZE_BINARY_PREFIX_TYPE_SI: + appendNum(result, bytes, 1000, (const char*[]) { "B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", NULL }); + break; + case FF_SIZE_BINARY_PREFIX_TYPE_JEDEC: + appendNum(result, bytes, 1024, (const char*[]) { "B", "KB", "MB", "GB", "TB", NULL }); + break; + default: + appendNum(result, bytes, 1024, (const char*[]) { "B", NULL }); + break; + } +} diff --git a/src/common/impl/smbios.c b/src/common/impl/smbios.c new file mode 100644 index 0000000..028bcd9 --- /dev/null +++ b/src/common/impl/smbios.c @@ -0,0 +1,805 @@ +#include "common/smbios.h" +#include "common/io.h" +#include "common/mallocHelper.h" +#include "common/debug.h" + +bool ffIsSmbiosValueSet(FFstrbuf* value) { + ffStrbufTrimRightSpace(value); + return value->length > 0 && + !ffStrbufStartsWithIgnCaseS(value, "To be filled") && + !ffStrbufStartsWithIgnCaseS(value, "To be set") && + !ffStrbufStartsWithIgnCaseS(value, "OEM") && + !ffStrbufStartsWithIgnCaseS(value, "O.E.M.") && + !ffStrbufStartsWithIgnCaseS(value, "System Product") && + !ffStrbufStartsWithIgnCaseS(value, "Unknown Product") && + !ffStrbufIgnCaseEqualS(value, "None") && + !ffStrbufIgnCaseEqualS(value, "System Name") && + !ffStrbufIgnCaseEqualS(value, "System Version") && + !ffStrbufIgnCaseEqualS(value, "System SKU#") && + !ffStrbufIgnCaseEqualS(value, "Default string") && + !ffStrbufIgnCaseEqualS(value, "Undefined") && + !ffStrbufIgnCaseEqualS(value, "Not Specified") && + !ffStrbufIgnCaseEqualS(value, "Not Applicable") && + !ffStrbufIgnCaseEqualS(value, "Not Defined") && + !ffStrbufIgnCaseEqualS(value, "Not Available") && + !ffStrbufIgnCaseEqualS(value, "INVALID") && + !ffStrbufIgnCaseEqualS(value, "Type1ProductConfigId") && + !ffStrbufIgnCaseEqualS(value, "TBD by OEM") && + !ffStrbufIgnCaseEqualS(value, "No Enclosure") && + !ffStrbufIgnCaseEqualS(value, "Chassis Version") && + !ffStrbufIgnCaseEqualS(value, "All Series") && + !ffStrbufIgnCaseEqualS(value, "N/A") && + !ffStrbufIgnCaseEqualS(value, "Unknown") && + !ffStrbufIgnCaseEqualS(value, "Standard") && ({ + // Some SMBIOS implementations use "0x0000" to indicate an unset value, even for strings. + bool zero = ffStrbufStartsWithS(value, "0x0"); + if (zero) { + for (size_t i = 2; i < value->length; i++) { + char c = value->chars[i]; + if (c != '0') { + zero = false; + break; + } + } + } + !zero; + }); +} + +static bool smbiosTableInitialized = false; +static FFSmbiosHeaderTable smbiosTable; + +const FFSmbiosHeader* ffSmbiosNextEntry(const FFSmbiosHeader* header) { + const char* p = ((const char*) header) + header->Length; + if (*p) { + do { + p += strlen(p) + 1; + } while (*p); + } else { // The terminator is always double 0 even if there is no string + p++; + } + + return (const FFSmbiosHeader*) (p + 1); +} + +static bool parseSmbiosTable(const uint8_t* data, uint32_t length) { + const FFSmbiosHeader* endOfTable = NULL; + + FF_DEBUG("Parsing SMBIOS table structures with length %u bytes", length); + FF_A_UNUSED int structureCount = 0, totalCount = 0; + for ( + const FFSmbiosHeader* header = (const FFSmbiosHeader*) data; + (const uint8_t*) header + sizeof(FFSmbiosHeader) < (const uint8_t*) data + length; + header = ffSmbiosNextEntry(header)) { + ++totalCount; + endOfTable = header; + + if (header->Length < sizeof(FFSmbiosHeader)) { + FF_DEBUG("Invalid SMBIOS structure length %u at offset 0x%lx", + header->Length, + (unsigned long) ((const uint8_t*) header - data)); + break; + } + + if (header->Handle >= 0xFF00) { + FF_DEBUG("Invalid SMBIOS structure handle 0x%04x at offset 0x%lx", + header->Handle, + (unsigned long) ((const uint8_t*) header - data)); + break; + } + + // This doesn't verify the entire structure (e.g. string section can still be truncated), + // but at least ensures the formatted section is valid and prevents infinite loops + // when the table is severely malformed. + if (__builtin_expect((const uint8_t*) header + header->Length > (const uint8_t*) data + length, false)) { + FF_DEBUG("Truncated SMBIOS structure at offset 0x%lx: length %u is too small", + (unsigned long) ((const uint8_t*) header - data), + header->Length); + break; + } + + if (header->Type < FF_SMBIOS_TYPE_END_OF_TABLE) { + if (!smbiosTable[header->Type]) { + smbiosTable[header->Type] = header; + FF_DEBUG("Found SMBIOS structure type %u, handle 0x%04X, length %u", + header->Type, + header->Handle, + header->Length); + structureCount++; + } else { + FF_DEBUG("Duplicate SMBIOS structure type %u, handle 0x%04X, length %u", + header->Type, + header->Handle, + header->Length); + } + } else if (header->Type == FF_SMBIOS_TYPE_END_OF_TABLE) { + FF_DEBUG("Reached SMBIOS end of type %u, handle 0x%04X, length %u", + header->Type, + header->Handle, + header->Length); + break; + } else { + FF_DEBUG("Found custom SMBIOS structure type %u, handle 0x%04X, length %u; ignoring", + header->Type, + header->Handle, + header->Length); + } + } + + if (!endOfTable) { + FF_DEBUG("No SMBIOS structures found in table"); + return false; + } + + FF_DEBUG("Parsed %d/%d SMBIOS structures, end-of-table (Type 127) %s", + structureCount, + totalCount, + endOfTable->Type == FF_SMBIOS_TYPE_END_OF_TABLE ? "found." : "not found! SMBIOS data may be malformed."); + smbiosTable[FF_SMBIOS_TYPE_END_OF_TABLE] = endOfTable; + + return true; +} + +#if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__sun) || defined(__HAIKU__) || defined(__OpenBSD__) || defined(__GNU__) + #include <fcntl.h> + #include <sys/stat.h> + #include <sys/types.h> + #include <sys/mman.h> + #include <stddef.h> + + #ifdef __linux__ + #include "common/properties.h" + #elif defined(__FreeBSD__) + #include "common/settings.h" + #elif defined(__NetBSD__) + #include "common/sysctl.h" + #endif + + #ifdef __linux__ +bool ffGetSmbiosValue(const char* devicesPath, const char* classPath, FFstrbuf* buffer) { + // /sys/class/dmi/id/* are all pseudo-files with very small content + // so reading the whole file at once is efficient + ffStrbufEnsureFixedLengthFree(buffer, 127); + + ssize_t len = ffReadFileData(devicesPath, buffer->allocated - 1, buffer->chars); + if (len > 0) { + assert(len < buffer->allocated); + buffer->chars[len] = '\0'; + buffer->length = (uint32_t) len; + ffStrbufTrimRightSpace(buffer); + if (ffIsSmbiosValueSet(buffer)) { + return true; + } + } + + len = ffReadFileData(classPath, buffer->allocated - 1, buffer->chars); + if (len > 0) { + assert(len < buffer->allocated); + buffer->chars[len] = '\0'; + buffer->length = (uint32_t) len; + ffStrbufTrimRightSpace(buffer); + if (ffIsSmbiosValueSet(buffer)) { + return true; + } + } + + ffStrbufClear(buffer); + return false; +} + #endif + +static bool readPhysicalMemory(int fd, off_t address, size_t length, void* buffer) { + #if !defined(__FreeBSD__) // Either causes kernel panic or returns EFAULT + // -1: unknown, 0: failed before (stop trying), 1: succeeded before + static int preadState = -1; + if (preadState != 0) { + ssize_t bytesRead = pread(fd, buffer, length, address); + if (bytesRead == (ssize_t) length) { + preadState = 1; + return true; + } + + FF_DEBUG("pread failed at address 0x%lx for %zu bytes: %s. Falling back to mmap%s", + (unsigned long) address, + length, + strerror(errno), + preadState < 0 ? " and caching failure" : ""); + preadState = 0; + } else { + FF_DEBUG("Skipping pread due to cached failure; using mmap"); + } + #endif + + off_t alignedAddress = address & ~((off_t) instance.state.platform.sysinfo.pageSize - 1); + size_t pageOffset = (size_t) (address - alignedAddress); + size_t mapLength = pageOffset + length; + + void* p = mmap(NULL, mapLength, PROT_READ, MAP_SHARED, fd, alignedAddress); + if (p == MAP_FAILED) { + FF_DEBUG("mmap failed at aligned address 0x%lx for %zu bytes: %s", + (unsigned long) alignedAddress, + mapLength, + strerror(errno)); + return false; + } + + memcpy(buffer, (const uint8_t*) p + pageOffset, length); + munmap(p, mapLength); + return true; +} + +typedef struct FFSmbios20EntryPoint { + uint8_t AnchorString[4]; + uint8_t EntryPointStructureChecksum; + uint8_t EntryPointLength; + uint8_t SmbiosMajorVersion; + uint8_t SmbiosMinorVersion; + uint16_t MaximumStructureSize; + uint8_t EntryPointRevision; + uint8_t FormattedArea[5]; + uint8_t IntermediateAnchorString[5]; + uint8_t IntermediateChecksum; + uint16_t StructureTableLength; + uint32_t StructureTableAddress; + uint16_t NumberOfSmbiosStructures; + uint8_t SmbiosBcdRevision; +} FF_A_PACKED FFSmbios20EntryPoint; +static_assert(offsetof(FFSmbios20EntryPoint, SmbiosBcdRevision) == 0x1E, + "FFSmbios20EntryPoint: Wrong struct alignment"); + +typedef struct FFSmbios30EntryPoint { + uint8_t AnchorString[5]; + uint8_t EntryPointStructureChecksum; + uint8_t EntryPointLength; + uint8_t SmbiosMajorVersion; + uint8_t SmbiosMinorVersion; + uint8_t SmbiosDocrev; + uint8_t EntryPointRevision; + uint8_t Reversed; + uint32_t StructureTableMaximumSize; + uint64_t StructureTableAddress; +} FF_A_PACKED FFSmbios30EntryPoint; + +static_assert(offsetof(FFSmbios30EntryPoint, StructureTableAddress) == 0x10, + "FFSmbios30EntryPoint: Wrong struct alignment"); + +typedef union FFSmbiosEntryPoint { + FFSmbios20EntryPoint Smbios20; + FFSmbios30EntryPoint Smbios30; +} FFSmbiosEntryPoint; + +static bool fillTableBufferFallback(FFstrbuf* buffer) { + const char* devMem = + #if __HAIKU__ + "/dev/misc/mem"; + #else + "/dev/mem"; // kern.securelevel must be -1 + #endif + FF_DEBUG("Using physical memory searching implementation: %s", devMem); + + uint32_t tableLength = 0; + off_t tableAddress = 0; + FF_AUTO_CLOSE_FD int fd = open(devMem, O_RDONLY | O_CLOEXEC); + if (fd < 0) { + FF_DEBUG("Failed to open memory device: %s", strerror(errno)); + return false; + } + FF_DEBUG("Memory device opened successfully with fd=%d", fd); + + // Works on legacy BIOS only + // See: https://wiki.osdev.org/System_Management_BIOS#UEFI_systems + // On BSD systems, we can get EFI system resource table (ESRT) via EFIIOC_GET_TABLE + // However, to acquire SMBIOS entry point, we need EFI configuration table (provided by EFI system table) + // which is not available via EFIIOC_GET_TABLE. + FF_AUTO_FREE uint8_t* smBiosBase = malloc(0x10000); + if (!readPhysicalMemory(fd, 0xF0000, 0x10000, smBiosBase)) { + FF_DEBUG("Failed to read SMBIOS memory region"); + return false; + } + FF_DEBUG("Successfully read 0x10000 bytes from physical address 0xF0000"); + + for (off_t offset = 0; offset <= 0xffe0; offset += 0x10) { + FFSmbiosEntryPoint* p = (void*) (smBiosBase + offset); + if (memcmp(p, "_SM3_", sizeof(p->Smbios30.AnchorString)) == 0) { + FF_DEBUG("Found SMBIOS 3.0 entry point at phyaddr 0x%05lX", (unsigned long) (0xF0000 + offset)); + if (p->Smbios30.EntryPointLength != sizeof(p->Smbios30)) { + FF_DEBUG("Invalid SMBIOS 3.0 entry point length: %u (expected %zu)", + p->Smbios30.EntryPointLength, + sizeof(p->Smbios30)); + return false; + } + tableLength = p->Smbios30.StructureTableMaximumSize; + tableAddress = (off_t) p->Smbios30.StructureTableAddress; + FF_DEBUG("SMBIOS 3.0: tableLength=0x%x, tableAddress=0x%lx, version=%u.%u.%u", + tableLength, + (unsigned long) tableAddress, + p->Smbios30.SmbiosMajorVersion, + p->Smbios30.SmbiosMinorVersion, + p->Smbios30.SmbiosDocrev); + break; + } else if (memcmp(p, "_SM_", sizeof(p->Smbios20.AnchorString)) == 0) { + FF_DEBUG("Found SMBIOS 2.0 entry point at phyaddr 0x%05lX", (unsigned long) (0xF0000 + offset)); + if (p->Smbios20.EntryPointLength != sizeof(p->Smbios20)) { + FF_DEBUG("Invalid SMBIOS 2.0 entry point length: %u (expected %zu)", + p->Smbios20.EntryPointLength, + sizeof(p->Smbios20)); + return false; + } + tableLength = p->Smbios20.StructureTableLength; + tableAddress = (off_t) p->Smbios20.StructureTableAddress; + FF_DEBUG("SMBIOS 2.0: tableLength=0x%x, tableAddress=0x%lx, version=%u.%u", + tableLength, + (unsigned long) tableAddress, + p->Smbios20.SmbiosMajorVersion, + p->Smbios20.SmbiosMinorVersion); + break; + } + } + if (tableLength == 0) { + FF_DEBUG("No valid SMBIOS entry point found in memory region"); + return false; + } + + ffStrbufClear(buffer); + ffStrbufEnsureFixedLengthFree(buffer, tableLength); + FF_DEBUG("Attempting to read SMBIOS table data: %u bytes at 0x%lx", tableLength, (unsigned long) tableAddress); + if (readPhysicalMemory(fd, (off_t) tableAddress, tableLength, buffer->chars)) { + buffer->length = tableLength; + buffer->chars[buffer->length] = '\0'; + FF_DEBUG("Successfully read SMBIOS table data: %u bytes", tableLength); + } else { + FF_DEBUG("Failed to read SMBIOS table data"); + return false; + } + + return true; +} + + #ifdef __OpenBSD__ +static bool detectSmbiosTableLength(const uint8_t* data, uint32_t bufferLength, uint32_t* tableLength) { + const uint8_t* p = data; + const uint8_t* end = data + bufferLength; + + while (p + sizeof(FFSmbiosHeader) <= end) { + const FFSmbiosHeader* header = (const FFSmbiosHeader*) p; + if (header->Length < sizeof(*header)) { + FF_DEBUG("Invalid SMBIOS structure length %u at offset 0x%lx", + header->Length, + (unsigned long) (p - data)); + return false; + } + + if (header->Handle >= 0xFF00) { + FF_DEBUG("Invalid SMBIOS structure handle 0x%04x at offset 0x%lx", + header->Handle, + (unsigned long) (p - data)); + return false; + } + + const uint8_t* formattedEnd = p + header->Length; + if (formattedEnd > end) { + FF_DEBUG("Truncated SMBIOS structure at offset 0x%lx: length %u is too small", + (unsigned long) (p - data), + header->Length); + return false; + } + + const char* string = (const char*) formattedEnd; + const char* stringEnd = (const char*) end; + while (true) { + size_t remaining = (size_t) (stringEnd - string); + if (remaining == 0) { + return false; + } + + const char* nul = memchr(string, '\0', remaining); + if (!nul) { + return false; + } + + string = nul + 1; + if (string >= stringEnd) { + return false; + } + + if (*string == '\0') { + ++string; + break; + } + } + + if (header->Type == FF_SMBIOS_TYPE_END_OF_TABLE) { + *tableLength = (uint32_t) (string - (const char*) data); + return true; + } + + p = (const uint8_t*) string; + } + + return false; +} + +static bool fillTableBufferPlatform(FFstrbuf* buffer) { + FF_DEBUG("Using OpenBSD /var/run/dmesg.boot implementation"); + + char dmesg[2048]; + ssize_t size = ffReadFileData("/var/run/dmesg.boot", sizeof(dmesg) - 1, dmesg); + if (size <= 0) { + FF_DEBUG("Failed to read /var/run/dmesg.boot"); + return false; + } else { + FF_DEBUG("Successfully read %zd bytes from /var/run/dmesg.boot", size); + } + dmesg[size] = '\0'; + + const char* const needle = "\nbios0 at mainbus0: SMBIOS rev. "; + size_t needleLen = strlen(needle); + char* line = memmem(dmesg, (size_t) size, needle, needleLen); + if (!line) { + FF_DEBUG("Failed to find SMBIOS line in /var/run/dmesg.boot"); + return false; + } + line += needleLen; + + char* lineEnd = memchr(line, '\n', (size_t) ((dmesg + size) - line)); + if (!lineEnd) { + lineEnd = dmesg + size; + } + + char* address = memchr(line, '@', (size_t) (lineEnd - line)); + if (!address) { + FF_DEBUG("Failed to find SMBIOS table address in dmesg line"); + return false; + } + + do { + ++address; + } while (address < lineEnd && (*address == ' ' || *address == '\t')); + + errno = 0; + char* addressEnd = NULL; + unsigned long long parsedAddress = strtoull(address, &addressEnd, 16); + if (errno != 0 || addressEnd == address || parsedAddress == 0) { + FF_DEBUG("Failed to parse OpenBSD SMBIOS table address from line: %.*s", + (int) (lineEnd - line), + line); + return false; + } + + off_t tableAddress = (off_t) parsedAddress; + FF_DEBUG("Parsed OpenBSD SMBIOS table address: 0x%llx", parsedAddress); + + FF_AUTO_CLOSE_FD int fd = open("/dev/mem", O_RDONLY | O_CLOEXEC); + if (fd < 0) { + FF_DEBUG("Failed to open /dev/mem: %s", strerror(errno)); + return false; + } + + uint32_t readLength = 0x10000; + ffStrbufClear(buffer); + ffStrbufEnsureFixedLengthFree(buffer, readLength); + + FF_DEBUG("Attempting to read OpenBSD SMBIOS table data: %u bytes at 0x%lx", + readLength, + (unsigned long) tableAddress); + + if (!readPhysicalMemory(fd, tableAddress, readLength, buffer->chars)) { + FF_DEBUG("Failed to read OpenBSD SMBIOS table data"); + return false; + } + + uint32_t detectedLength = 0; + if (detectSmbiosTableLength((const uint8_t*) buffer->chars, readLength, &detectedLength)) { + buffer->length = detectedLength; + buffer->chars[buffer->length] = '\0'; + FF_DEBUG("Determined OpenBSD SMBIOS table length: %u bytes", detectedLength); + return true; + } + + FF_DEBUG("SMBIOS end-of-table marker not found within first %u bytes; give up", readLength); + ffStrbufClear(buffer); + return false; +} + #else +static bool fillTableBufferPlatform(FFstrbuf* buffer) { + #if __HAIKU__ && __GNU__ + return false; + #elif defined(__linux__) + FF_DEBUG("Using Linux implementation - trying /sys/firmware/dmi/tables/DMI"); + if (ffAppendFileBuffer("/sys/firmware/dmi/tables/DMI", buffer)) { + return true; + } + + FF_DEBUG("Failed to read /sys/firmware/dmi/tables/DMI, falling back to memory-mapped implementation"); + #endif + + { + #if !defined(__sun) && !defined(__NetBSD__) + FF_DEBUG("Using memory-mapped implementation"); + FF_STRBUF_AUTO_DESTROY strEntryAddress = ffStrbufCreate(); + #ifdef __FreeBSD__ + FF_DEBUG("Using FreeBSD kenv implementation"); + if (!ffSettingsGetFreeBSDKenv("hint.smbios.0.mem", &strEntryAddress)) { + FF_DEBUG("Failed to get SMBIOS address from FreeBSD kenv"); + return false; // non-UEFI systems + } + FF_DEBUG("Got SMBIOS address from kenv: %s", strEntryAddress.chars); + #elif defined(__linux__) + { + FF_DEBUG("Using Linux EFI systab implementation"); + FF_STRBUF_AUTO_DESTROY systab = ffStrbufCreate(); + if (!ffAppendFileBuffer("/sys/firmware/efi/systab", &systab)) { + FF_DEBUG("Failed to read /sys/firmware/efi/systab"); + return false; + } + if (!ffParsePropLines(systab.chars, "SMBIOS3=", &strEntryAddress) && + !ffParsePropLines(systab.chars, "SMBIOS=", &strEntryAddress)) { + FF_DEBUG("Failed to find SMBIOS entry in systab"); + return false; + } + FF_DEBUG("Found SMBIOS entry in systab: %s", strEntryAddress.chars); + } + #endif + + off_t entryAddress = (off_t) strtol(strEntryAddress.chars, NULL, 16); + if (entryAddress == 0) { + FF_DEBUG("Invalid SMBIOS entry address: 0"); + return false; + } + FF_DEBUG("Parsed SMBIOS entry address: 0x%lx", (unsigned long) entryAddress); + + FF_AUTO_CLOSE_FD int fd = open("/dev/mem", O_RDONLY | O_CLOEXEC); + if (fd < 0) { + FF_DEBUG("Failed to open /dev/mem: %s", strerror(errno)); + return false; + } + FF_DEBUG("/dev/mem opened successfully with fd=%d", fd); + + FFSmbiosEntryPoint entryPoint; + FF_DEBUG("Attempting to read %zu bytes from physical address 0x%lx", + sizeof(entryPoint), + (unsigned long) entryAddress); + if (!readPhysicalMemory(fd, entryAddress, sizeof(entryPoint), &entryPoint)) { + return false; + } + FF_DEBUG("Successfully read SMBIOS entry point data"); + #else + // Sun or NetBSD + FF_DEBUG("Using %s specific implementation", + #ifdef __NetBSD__ + "NetBSD" + #else + "SunOS" + #endif + ); + + FF_AUTO_CLOSE_FD int fd = open("/dev/smbios", O_RDONLY | O_CLOEXEC); + if (fd < 0) { + FF_DEBUG("Failed to open /dev/smbios: %s", strerror(errno)); + return false; + } + FF_DEBUG("/dev/smbios opened successfully with fd=%d", fd); + + FFSmbiosEntryPoint entryPoint; + #ifdef __NetBSD__ + off_t addr = (off_t) ffSysctlGetInt64("machdep.smbios", 0); + if (addr == 0) { + FF_DEBUG("Failed to get SMBIOS address from sysctl"); + return false; + } + FF_DEBUG("Got SMBIOS address from sysctl: 0x%lx", (unsigned long) addr); + + if (pread(fd, &entryPoint, sizeof(entryPoint), addr) < 1) { + FF_DEBUG("Failed to read SMBIOS entry point: %s", strerror(errno)); + return false; + } + FF_DEBUG("Successfully read SMBIOS entry point"); + #else + FF_DEBUG("Reading SMBIOS entry point from /dev/smbios"); + if (ffReadFDData(fd, sizeof(entryPoint), &entryPoint) < 1) { + FF_DEBUG("Failed to read SMBIOS entry point: %s", strerror(errno)); + return false; + } + FF_DEBUG("Successfully read SMBIOS entry point"); + #endif + #endif + + uint32_t tableLength = 0; + off_t tableAddress = 0; + if (memcmp(entryPoint.Smbios20.AnchorString, "_SM_", sizeof(entryPoint.Smbios20.AnchorString)) == 0) { + FF_DEBUG("Found SMBIOS 2.0 entry point"); + if (entryPoint.Smbios20.EntryPointLength != sizeof(entryPoint.Smbios20)) { + FF_DEBUG("Invalid SMBIOS 2.0 entry point length: %u (expected %zu)", + entryPoint.Smbios20.EntryPointLength, + sizeof(entryPoint.Smbios20)); + return false; + } + tableLength = entryPoint.Smbios20.StructureTableLength; + tableAddress = (off_t) entryPoint.Smbios20.StructureTableAddress; + FF_DEBUG("SMBIOS 2.0: tableLength=0x%x, tableAddress=0x%lx, version=%u.%u", + tableLength, + (unsigned long) tableAddress, + entryPoint.Smbios20.SmbiosMajorVersion, + entryPoint.Smbios20.SmbiosMinorVersion); + } else if (memcmp(entryPoint.Smbios30.AnchorString, "_SM3_", sizeof(entryPoint.Smbios30.AnchorString)) == 0) { + FF_DEBUG("Found SMBIOS 3.0 entry point"); + if (entryPoint.Smbios30.EntryPointLength != sizeof(entryPoint.Smbios30)) { + FF_DEBUG("Invalid SMBIOS 3.0 entry point length: %u (expected %zu)", + entryPoint.Smbios30.EntryPointLength, + sizeof(entryPoint.Smbios30)); + return false; + } + tableLength = entryPoint.Smbios30.StructureTableMaximumSize; + tableAddress = (off_t) entryPoint.Smbios30.StructureTableAddress; + FF_DEBUG("SMBIOS 3.0: tableLength=0x%x, tableAddress=0x%lx, version=%u.%u.%u", + tableLength, + (unsigned long) tableAddress, + entryPoint.Smbios30.SmbiosMajorVersion, + entryPoint.Smbios30.SmbiosMinorVersion, + entryPoint.Smbios30.SmbiosDocrev); + } else { + FF_DEBUG("Unknown SMBIOS entry point format"); + return false; // Dragonfly goes here + } + + ffStrbufClear(buffer); + ffStrbufEnsureFixedLengthFree(buffer, tableLength); + FF_DEBUG("Attempting to read SMBIOS table data: %u bytes at 0x%lx", tableLength, (unsigned long) tableAddress); + if (readPhysicalMemory(fd, tableAddress, tableLength, buffer->chars)) { + buffer->length = tableLength; + buffer->chars[buffer->length] = '\0'; + FF_DEBUG("Successfully read SMBIOS table data: %u bytes", tableLength); + } else { + ffStrbufClear(buffer); + return false; + } + } + + return true; +} + #endif + +const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable() { + static FFstrbuf buffer; + if (!smbiosTableInitialized) { + smbiosTableInitialized = true; + FF_DEBUG("Initializing SMBIOS buffer"); + ffStrbufInit(&buffer); + + if (!fillTableBufferPlatform(&buffer)) { + FF_DEBUG("Platform specific SMBIOS retrieval failed, trying fallback method"); + if (!fillTableBufferFallback(&buffer)) { + FF_DEBUG("Fallback SMBIOS retrieval also failed"); + ffStrbufDestroy(&buffer); + return NULL; + } + } + + if (!parseSmbiosTable((const uint8_t*) buffer.chars, buffer.length)) { + ffStrbufClear(&buffer); + } + } + + if (buffer.length == 0) { + FF_DEBUG("No valid SMBIOS data available"); + return NULL; + } + + return &smbiosTable; +} +#elif defined(_WIN32) + #include "common/windows/nt.h" + + #pragma GCC diagnostic ignored "-Wmultichar" + +typedef struct FFRawSmbiosData { + uint8_t Used20CallingMethod; + uint8_t SMBIOSMajorVersion; + uint8_t SMBIOSMinorVersion; + uint8_t DmiRevision; + uint32_t Length; + uint8_t SMBIOSTableData[]; +} FFRawSmbiosData; + +const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable() { + static SYSTEM_FIRMWARE_TABLE_INFORMATION* buffer; + + if (!smbiosTableInitialized) { + smbiosTableInitialized = true; + FF_DEBUG("Initializing Windows SMBIOS buffer"); + + FF_DEBUG("Querying system firmware table size with signature 'RSMB'"); + SYSTEM_FIRMWARE_TABLE_INFORMATION sfti = { + .ProviderSignature = 'RSMB', + .Action = SystemFirmwareTableGet, + }; + ULONG bufSize = 0; + NtQuerySystemInformation(SystemFirmwareTableInformation, &sfti, sizeof(sfti), &bufSize); + if (bufSize <= sizeof(FFRawSmbiosData) + sizeof(sfti)) { + FF_DEBUG("Invalid firmware table size: %lu (must be > %zu)", bufSize, sizeof(FFRawSmbiosData) + sizeof(sfti)); + return NULL; + } + if (bufSize != sfti.TableBufferLength + (ULONG) sizeof(sfti)) { + FF_DEBUG("Firmware table size mismatch: NtQuerySystemInformation returned %lu but expected %lu", + bufSize, + sfti.TableBufferLength + (ULONG) sizeof(sfti)); + return NULL; + } + FF_DEBUG("Firmware table size: %lu bytes", bufSize); + + buffer = malloc(bufSize); + *buffer = sfti; + FF_DEBUG("Allocated buffer for SMBIOS data"); + + if (!NT_SUCCESS(NtQuerySystemInformation(SystemFirmwareTableInformation, buffer, bufSize, &bufSize))) { + FF_DEBUG("NtQuerySystemInformation(SystemFirmwareTableInformation) failed"); + free(buffer); + buffer = NULL; + return NULL; + } + FFRawSmbiosData* rawData = (FFRawSmbiosData*) buffer->TableBuffer; + + FF_DEBUG("Successfully retrieved SMBIOS data: version %u.%u, length %u bytes", + rawData->SMBIOSMajorVersion, + rawData->SMBIOSMinorVersion, + rawData->Length); + + if (!parseSmbiosTable(rawData->SMBIOSTableData, rawData->Length)) { + free(buffer); + buffer = NULL; + return NULL; + } + } + + if (!buffer) { + FF_DEBUG("No valid SMBIOS data available"); + return NULL; + } + return &smbiosTable; +} +#elif defined(__APPLE__) + #include "common/apple/cf_helpers.h" + +const FFSmbiosHeaderTable* ffGetSmbiosHeaderTable() { + static CFDataRef smbiosDataBuffer; + + if (!smbiosTableInitialized) { + smbiosTableInitialized = true; + FF_DEBUG("Initializing SMBIOS buffer on Apple platform"); + + FF_IOOBJECT_AUTO_RELEASE io_registry_entry_t registryEntry = IOServiceGetMatchingService(MACH_PORT_NULL, IOServiceMatching("AppleSMBIOS")); + + if (!registryEntry) { + FF_DEBUG("IOServiceGetMatchingService() failed to find AppleSMBIOS"); + return NULL; + } + + FF_DEBUG("AppleSMBIOS service found, retrieving SMBIOS data"); + smbiosDataBuffer = IORegistryEntryCreateCFProperty(registryEntry, CFSTR("SMBIOS"), kCFAllocatorDefault, kNilOptions); + if (!smbiosDataBuffer) { + FF_DEBUG("IORegistryEntryCreateCFProperty() failed to get SMBIOS data"); + return NULL; + } + if (CFGetTypeID(smbiosDataBuffer) != CFDataGetTypeID()) { + FF_DEBUG("Unexpected SMBIOS data type: expected CFData"); + CFRelease(smbiosDataBuffer); + smbiosDataBuffer = NULL; + return NULL; + } + + FF_DEBUG("Successfully retrieved SMBIOS data: %lu bytes", CFDataGetLength(smbiosDataBuffer)); + if (!parseSmbiosTable((const uint8_t*) CFDataGetBytePtr(smbiosDataBuffer), (uint32_t) CFDataGetLength(smbiosDataBuffer))) { + CFRelease(smbiosDataBuffer); + smbiosDataBuffer = NULL; + return NULL; + } + } + + if (!smbiosDataBuffer) { + FF_DEBUG("No valid SMBIOS data available"); + return NULL; + } + + return &smbiosTable; +} +#endif diff --git a/src/common/impl/strutil.c b/src/common/impl/strutil.c new file mode 100644 index 0000000..3a801f7 --- /dev/null +++ b/src/common/impl/strutil.c @@ -0,0 +1,80 @@ +#include "common/strutil.h" + +uint8_t ffUtf8CharLenWidth(const char* str, uint32_t length, uint8_t* width) { + if (__builtin_expect(length == 0 || *str == '\0', false)) { + if (width) { + *width = 0; + } + return 0; + } + + unsigned char first = (unsigned char) *str; + if (__builtin_expect(first < 0x80, true)) { + if (width) { + *width = 1; + } + return 1; + } + + uint8_t bytes; + if ((first & 0xE0) == 0xC0) { + bytes = 2; + } else if ((first & 0xF0) == 0xE0) { + bytes = 3; + } else if ((first & 0xF8) == 0xF0) { + bytes = 4; + } else { + if (width) { + *width = 1; + } + return 1; + } + + if (length < bytes) { + if (width) { + *width = 1; + } + return 1; + } + + for (uint8_t i = 1; i < bytes; ++i) { + unsigned char continuation = (unsigned char) str[i]; + if (continuation == '\0' || (continuation & 0xC0) != 0x80) { + if (width) { + *width = 1; + } + return 1; + } + } + + uint32_t ucs = (uint32_t) (first & ((1U << (8 - bytes)) - 1)); + for (uint8_t i = 1; i < bytes; ++i) { + ucs <<= 6; + ucs |= (uint32_t) ((unsigned char) str[i] & 0x3F); + } + + int wcWidth = mk_wcwidth(ucs); + if (width) { + *width = (uint8_t) (wcWidth < 0 ? 0 : wcWidth); + } + return bytes; +} + +uint32_t ffUtf8StrWidth(const char* str, uint32_t length) { + uint32_t result = 0; + const char* ptr = str; + + while (length > 0 && *ptr != '\0') { + uint8_t width = 0; + uint8_t bytes = ffUtf8CharLenWidth(ptr, length, &width); + if (__builtin_expect(bytes == 0, false)) { + break; + } + + result += width; + ptr += bytes; + length -= bytes; + } + + return result > 0 ? result : (uint32_t) (ptr - str); +} diff --git a/src/common/impl/sysctl.c b/src/common/impl/sysctl.c new file mode 100644 index 0000000..5974698 --- /dev/null +++ b/src/common/impl/sysctl.c @@ -0,0 +1,93 @@ +#include "common/sysctl.h" + +#include <stdlib.h> + +#ifdef __OpenBSD__ +const char* ffSysctlGetString(int mib1, int mib2, FFstrbuf* result) { + size_t neededLength; + if (sysctl((int[]) { mib1, mib2 }, 2, NULL, &neededLength, NULL, 0) != 0 || neededLength == 1) { // neededLength is 1 for empty strings, because of the null terminator + return "sysctl() length query failed"; + } + + ffStrbufEnsureFree(result, (uint32_t) neededLength - 1); + + if (sysctl((int[]) { mib1, mib2 }, 2, result->chars + result->length, &neededLength, NULL, 0) != 0) { + return "sysctl() failed to retrieve string data"; + } + + result->length += (uint32_t) neededLength - 1; + result->chars[result->length] = '\0'; + + return NULL; +} + +int ffSysctlGetInt(int mib1, int mib2, int defaultValue) { + int result; + size_t neededLength = sizeof(result); + if (sysctl((int[]) { mib1, mib2 }, 2, &result, &neededLength, NULL, 0) != 0) { + return defaultValue; + } + return result; +} + +int64_t ffSysctlGetInt64(int mib1, int mib2, int64_t defaultValue) { + int64_t result; + size_t neededLength = sizeof(result); + if (sysctl((int[]) { mib1, mib2 }, 2, &result, &neededLength, NULL, 0) != 0) { + return defaultValue; + } + return result; +} +#else +const char* ffSysctlGetString(const char* propName, FFstrbuf* result) { + size_t neededLength; + if (sysctlbyname(propName, NULL, &neededLength, NULL, 0) != 0 || neededLength == 1) { // neededLength is 1 for empty strings, because of the null terminator + return "sysctlbyname() failed"; + } + + ffStrbufEnsureFree(result, (uint32_t) neededLength - 1); + + if (sysctlbyname(propName, result->chars + result->length, &neededLength, NULL, 0) != 0) { + return "sysctlbyname() failed to retrieve string data"; + } + + result->length += (uint32_t) neededLength - 1; + + result->chars[result->length] = '\0'; + + return NULL; +} + +int ffSysctlGetInt(const char* propName, int defaultValue) { + int result; + size_t neededLength = sizeof(result); + if (sysctlbyname(propName, &result, &neededLength, NULL, 0) != 0) { + return defaultValue; + } + return result; +} + +int64_t ffSysctlGetInt64(const char* propName, int64_t defaultValue) { + int64_t result; + size_t neededLength = sizeof(result); + if (sysctlbyname(propName, &result, &neededLength, NULL, 0) != 0) { + return defaultValue; + } + return result; +} +#endif // OpenBSD + +void* ffSysctlGetData(int* request, u_int requestLength, size_t* resultLength) { + if (sysctl(request, requestLength, NULL, resultLength, NULL, 0) != 0) { + return NULL; + } + + void* data = malloc(*resultLength); + + if (sysctl(request, requestLength, data, resultLength, NULL, 0) != 0) { + free(data); + return NULL; + } + + return data; +} diff --git a/src/common/impl/temps.c b/src/common/impl/temps.c new file mode 100644 index 0000000..652f3fb --- /dev/null +++ b/src/common/impl/temps.c @@ -0,0 +1,165 @@ +#include "fastfetch.h" +#include "common/temps.h" +#include "common/textModifier.h" +#include "common/strutil.h" + +void ffTempsAppendNum(double celsius, FFstrbuf* buffer, FFColorRangeConfig config, const FFModuleArgs* module) { + if (celsius == -DBL_MAX) { // ignores invalid value + return; + } + + const FFOptionsDisplay* options = &instance.config.display; + const char* colorGreen = options->tempColorGreen.chars; + const char* colorYellow = options->tempColorYellow.chars; + const char* colorRed = options->tempColorRed.chars; + + uint8_t green = config.green, yellow = config.yellow; + + if (!options->pipe) { + if (green <= yellow) { + if (celsius > yellow) { + ffStrbufAppendF(buffer, "\e[%sm", colorRed); + } else if (celsius > green) { + ffStrbufAppendF(buffer, "\e[%sm", colorYellow); + } else { + ffStrbufAppendF(buffer, "\e[%sm", colorGreen); + } + } else { + if (celsius < yellow) { + ffStrbufAppendF(buffer, "\e[%sm", colorRed); + } else if (celsius < green) { + ffStrbufAppendF(buffer, "\e[%sm", colorYellow); + } else { + ffStrbufAppendF(buffer, "\e[%sm", colorGreen); + } + } + } + + switch (options->tempUnit) { + case FF_TEMPERATURE_UNIT_DEFAULT: + case FF_TEMPERATURE_UNIT_CELSIUS: + ffStrbufAppendF(buffer, "%.*f%s°C", options->tempNdigits, celsius, options->tempSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : ""); + break; + case FF_TEMPERATURE_UNIT_FAHRENHEIT: + ffStrbufAppendF(buffer, "%.*f%s°F", options->tempNdigits, celsius * 1.8 + 32, options->tempSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_ALWAYS ? " " : ""); + break; + case FF_TEMPERATURE_UNIT_KELVIN: + ffStrbufAppendF(buffer, "%.*f%sK", options->tempNdigits, celsius + 273.15, options->tempSpaceBeforeUnit == FF_SPACE_BEFORE_UNIT_NEVER ? "" : " "); + break; + } + + if (!options->pipe) { + ffStrbufAppendS(buffer, FASTFETCH_TEXT_MODIFIER_RESET); + if (module->outputColor.length) { + ffStrbufAppendF(buffer, "\e[%sm", module->outputColor.chars); + } else if (instance.config.display.colorOutput.length) { + ffStrbufAppendF(buffer, "\e[%sm", instance.config.display.colorOutput.chars); + } + } +} + +bool ffTempsParseCommandOptions(const char* key, const char* subkey, const char* value, bool* useTemp, FFColorRangeConfig* config) { + if (!ffStrStartsWithIgnCase(subkey, "temp")) { + return false; + } + + if (subkey[strlen("temp")] == '\0') { + *useTemp = ffOptionParseBoolean(value); + return true; + } + + if (subkey[strlen("temp")] != '-') { + return false; + } + + subkey += strlen("temp-"); + + if (ffStrEqualsIgnCase(subkey, "green")) { + uint32_t num = ffOptionParseUInt32(key, value); + if (num > 100) { + fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key); + exit(480); + } + config->green = (uint8_t) num; + return true; + } + + if (ffStrEqualsIgnCase(subkey, "yellow")) { + uint32_t num = ffOptionParseUInt32(key, value); + if (num > 100) { + fprintf(stderr, "Error: usage: %s must be between 0 and 100\n", key); + exit(480); + } + config->yellow = (uint8_t) num; + return true; + } + + return false; +} + +bool ffTempsParseJsonObject(yyjson_val* key, yyjson_val* value, bool* useTemp, FFColorRangeConfig* config) { + assert(key); + + if (!unsafe_yyjson_equals_str(key, "temp")) { + return false; + } + + if (yyjson_is_bool(value)) { + *useTemp = yyjson_get_bool(value); + return true; + } + + if (yyjson_is_null(value)) { + *useTemp = false; + return true; + } + + if (!yyjson_is_obj(value)) { + fprintf(stderr, "Error: usage: %s must be an object or a boolean\n", unsafe_yyjson_get_str(key)); + exit(480); + } + + *useTemp = true; + + yyjson_val* greenVal = yyjson_obj_get(value, "green"); + if (greenVal) { + if (!yyjson_is_int(greenVal)) { + fputs("Error: usage: temp.green must be an integer between 0 and 100\n", stderr); + exit(480); + } + + int num = unsafe_yyjson_get_int(greenVal); + if (num < 0 || num > 100) { + fputs("Error: usage: temp.green must be between 0 and 100\n", stderr); + exit(480); + } + config->green = (uint8_t) num; + } + + yyjson_val* yellowVal = yyjson_obj_get(value, "yellow"); + if (yellowVal) { + if (!yyjson_is_int(yellowVal)) { + fputs("Error: usage: temp.yellow must be an integer between 0 and 100\n", stderr); + exit(480); + } + + int num = unsafe_yyjson_get_int(yellowVal); + if (num < 0 || num > 100) { + fputs("Error: usage: temp.yellow must be between 0 and 100\n", stderr); + exit(480); + } + config->yellow = (uint8_t) num; + } + + return true; +} + +void ffTempsGenerateJsonConfig(yyjson_mut_doc* doc, yyjson_mut_val* module, bool temp, FFColorRangeConfig config) { + if (!temp) { + yyjson_mut_obj_add_bool(doc, module, "temp", false); + } else { + yyjson_mut_val* temp = yyjson_mut_obj_add_obj(doc, module, "temp"); + yyjson_mut_obj_add_uint(doc, temp, "green", config.green); + yyjson_mut_obj_add_uint(doc, temp, "yellow", config.yellow); + } +} diff --git a/src/common/impl/time.c b/src/common/impl/time.c new file mode 100644 index 0000000..c60d1fe --- /dev/null +++ b/src/common/impl/time.c @@ -0,0 +1,97 @@ +#include "common/time.h" + +#include <stdio.h> + +char ffTimeInternalBuffer[64]; // Reduce memory usage and prevent redundant allocations + +#ifdef _WIN32 + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wformat" +#endif + +const char* ffTimeToFullStr(uint64_t msec) { + if (msec == 0) { + return ""; + } + time_t tsec = (time_t) (msec / 1000); + const struct tm* tm = localtime(&tsec); + + uint32_t len = 0; + len += (uint32_t) strftime(ffTimeInternalBuffer, ARRAY_SIZE(ffTimeInternalBuffer) - len, "%FT%T", tm); + len += (uint32_t) snprintf(ffTimeInternalBuffer + len, ARRAY_SIZE(ffTimeInternalBuffer) - len, ".%03u", (unsigned) (msec % 1000)); + len += (uint32_t) strftime(ffTimeInternalBuffer + len, ARRAY_SIZE(ffTimeInternalBuffer) - len, "%z", tm); + return ffTimeInternalBuffer; +} + +const char* ffTimeToShortStr(uint64_t msec) { + if (msec == 0) { + return ""; + } + time_t tsec = (time_t) (msec / 1000); + + strftime(ffTimeInternalBuffer, ARRAY_SIZE(ffTimeInternalBuffer), "%F %T", localtime(&tsec)); + return ffTimeInternalBuffer; +} + +const char* ffTimeToTimeStr(uint64_t msec) { + if (msec == 0) { + return ""; + } + time_t tsec = (time_t) (msec / 1000); + + uint32_t len = (uint32_t) strftime(ffTimeInternalBuffer, ARRAY_SIZE(ffTimeInternalBuffer), "%T", localtime(&tsec)); + sprintf(ffTimeInternalBuffer + len, ".%03u", (unsigned) (msec % 1000)); + return ffTimeInternalBuffer; +} + +#ifdef _WIN32 + #pragma GCC diagnostic pop +#endif + +FFTimeGetAgeResult ffTimeGetAge(uint64_t birthMs, uint64_t nowMs) { + FFTimeGetAgeResult result = {}; + if (__builtin_expect(birthMs == 0 || nowMs < birthMs, 0)) { + return result; + } + + time_t birth_s = (time_t) (birthMs / 1000); + struct tm birth_tm; +#ifdef _WIN32 + localtime_s(&birth_tm, &birth_s); +#else + localtime_r(&birth_s, &birth_tm); +#endif + + time_t now_s = (time_t) (nowMs / 1000); + struct tm now_tm; +#ifdef _WIN32 + localtime_s(&now_tm, &now_s); +#else + localtime_r(&now_s, &now_tm); +#endif + + result.years = (uint32_t) (now_tm.tm_year - birth_tm.tm_year); + if (now_tm.tm_yday < birth_tm.tm_yday) { + result.years--; + } + + birth_tm.tm_year += (int) result.years; + birth_s = mktime(&birth_tm); + uint32_t diff_s = (uint32_t) (now_s - birth_s); + result.daysOfYear = diff_s / (24 * 60 * 60); + + birth_tm.tm_year += 1; + result.yearsFraction = (double) diff_s / (double) (mktime(&birth_tm) - birth_s) + result.years; + + return result; +} + +#ifdef _WIN32 +double ffQpcMultiplier; + +__attribute__((constructor)) static void ffTimeInitQpcMultiplier(void) { + LARGE_INTEGER frequency; + RtlQueryPerformanceFrequency(&frequency); + ffQpcMultiplier = 1000. / (double) frequency.QuadPart; +} +#endif diff --git a/src/common/impl/wcwidth.c b/src/common/impl/wcwidth.c new file mode 100644 index 0000000..4453510 --- /dev/null +++ b/src/common/impl/wcwidth.c @@ -0,0 +1,31 @@ +#include "common/wcwidth.h" +#include "3rdparty/widecharwidth/widechar_width_c.h" + +int mk_wcwidth(uint32_t wc) { + // // We render U+1F6E1 (🛡) with a width of 2, + // // but widechar_width says it has a width of 1 because Unicode classifies it as "neutral". + // // + // // So we simply decide the width ourselves + // if (wc == 0x1F6E1) return 2; + // + // Well terminals do show it as width 1 after all + + int width = widechar_wcwidth(wc); + + switch (width) { + case widechar_ambiguous: + case widechar_private_use: + return 1; + case widechar_widened_in_9: + // Our renderer supports Unicode 9 + return 2; + case widechar_nonprint: + case widechar_combining: + case widechar_unassigned: + case widechar_non_character: + return 0; + default: + // Use the width widechar_width gave us. + return width; + } +} |