From 76424950e373d3b04ac3dd13019151bfba3e8423 Mon Sep 17 00:00:00 2001 From: sumuel Date: Mon, 17 Aug 2026 20:44:55 +0000 Subject: Add the files --- src/detection/packages/packages.c | 119 +++++ src/detection/packages/packages.h | 72 +++ src/detection/packages/packages_apple.c | 56 +++ src/detection/packages/packages_bsd.c | 28 ++ src/detection/packages/packages_haiku.c | 12 + src/detection/packages/packages_linux.c | 651 ++++++++++++++++++++++++++++ src/detection/packages/packages_nbsd.c | 9 + src/detection/packages/packages_nix.c | 149 +++++++ src/detection/packages/packages_nosupport.c | 4 + src/detection/packages/packages_obsd.c | 9 + src/detection/packages/packages_sunos.c | 17 + src/detection/packages/packages_windows.c | 203 +++++++++ 12 files changed, 1329 insertions(+) create mode 100644 src/detection/packages/packages.c create mode 100644 src/detection/packages/packages.h create mode 100644 src/detection/packages/packages_apple.c create mode 100644 src/detection/packages/packages_bsd.c create mode 100644 src/detection/packages/packages_haiku.c create mode 100644 src/detection/packages/packages_linux.c create mode 100644 src/detection/packages/packages_nbsd.c create mode 100644 src/detection/packages/packages_nix.c create mode 100644 src/detection/packages/packages_nosupport.c create mode 100644 src/detection/packages/packages_obsd.c create mode 100644 src/detection/packages/packages_sunos.c create mode 100644 src/detection/packages/packages_windows.c (limited to 'src/detection/packages') diff --git a/src/detection/packages/packages.c b/src/detection/packages/packages.c new file mode 100644 index 0000000..25f59b3 --- /dev/null +++ b/src/detection/packages/packages.c @@ -0,0 +1,119 @@ +#include "packages.h" +#include "common/io.h" +#include "common/time.h" + +#include +#include + +#ifdef __APPLE__ + #define st_mtim st_mtimespec +#endif + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options); + +const char* ffDetectPackages(FFPackagesResult* result, FFPackagesOptions* options) { + ffDetectPackagesImpl(result, options); + + for (uint32_t i = 0; i < offsetof(FFPackagesResult, all) / sizeof(uint32_t); ++i) { + result->all += ((uint32_t*) result)[i]; + } + + return NULL; +} + +bool ffPackagesReadCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, const char* filePath, const char* packageId, uint32_t* result) { +#ifndef _WIN32 + struct stat st; + if (stat(filePath, &st) < 0) // file doesn't exist or isn't accessible + { + *result = 0; + return true; + } + + if (__builtin_expect(st.st_mtim.tv_sec <= 0, false)) { + return false; + } + + uint64_t mtime_current = (uint64_t) st.st_mtim.tv_sec * 1000ull + (uint64_t) st.st_mtim.tv_nsec / 1000000ull; +#else + FF_AUTO_CLOSE_FD HANDLE handle = CreateFileA(filePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + + if (handle == INVALID_HANDLE_VALUE) // file doesn't exist or isn't accessible + { + *result = 0; + return true; + } + + uint64_t mtime_current; + FILE_BASIC_INFORMATION fileInfo; + IO_STATUS_BLOCK iosb; + if (!NT_SUCCESS(NtQueryInformationFile(handle, &iosb, &fileInfo, sizeof(fileInfo), FileBasicInformation))) { + return false; + } + + mtime_current = ffFileTimeToUnixMs((uint64_t) fileInfo.LastWriteTime.QuadPart); +#endif + + ffStrbufSet(cacheDir, &instance.state.platform.cacheDir); + ffStrbufEnsureEndsWithC(cacheDir, '/'); + ffStrbufAppendF(cacheDir, "fastfetch/packages/%s.txt", packageId); + + if (ffReadFileBuffer(cacheDir->chars, cacheContent)) { + uint64_t mtime_cached; + uint32_t num_cached; + if (sscanf(cacheContent->chars, "%" SCNu64 " %" SCNu32, &mtime_cached, &num_cached) == 2 && + mtime_cached == mtime_current && num_cached > 0) { + *result = num_cached; + return true; + } + } + + ffStrbufSetF(cacheContent, "%" PRIu64 " ", mtime_current); + + return false; +} + +bool ffPackagesWriteCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint32_t num_elements) { + if (__builtin_expect(cacheContent->length == 0, false)) { + return false; + } + + ffStrbufAppendF(cacheContent, "%" PRIu32, num_elements); + return ffWriteFileBuffer(cacheDir->chars, cacheContent); +} + +#ifndef _WIN32 +uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(dirname); + if (dirp == NULL) { + return 0; + } + + uint32_t num_elements = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + bool ok = false; + + if (entry->d_name[0] != '.') { + #if !defined(__sun) && !defined(__HAIKU__) + if (entry->d_type != DT_UNKNOWN && entry->d_type != DT_LNK) { + ok = entry->d_type == (isdir ? DT_DIR : DT_REG); + } else + #endif + { + struct stat stbuf; + if (fstatat(dirfd(dirp), entry->d_name, &stbuf, 0) == 0) { + ok = isdir ? S_ISDIR(stbuf.st_mode) : S_ISREG(stbuf.st_mode); + } + } + } + + if (ok) { + ++num_elements; + } + } + + return num_elements; +} +#endif diff --git a/src/detection/packages/packages.h b/src/detection/packages/packages.h new file mode 100644 index 0000000..f0d4200 --- /dev/null +++ b/src/detection/packages/packages.h @@ -0,0 +1,72 @@ +#pragma once + +#include "fastfetch.h" +#include "modules/packages/option.h" + +typedef struct FFPackagesResult { + uint32_t amSystem; + uint32_t amUser; + uint32_t apk; + uint32_t appimage; + uint32_t brew; + uint32_t brewCask; + uint32_t cards; + uint32_t choco; + uint32_t dpkg; + uint32_t emerge; + uint32_t eopkg; + uint32_t flatpakSystem; + uint32_t flatpakUser; + uint32_t guixHome; + uint32_t guixSystem; + uint32_t guixUser; + uint32_t hpkgSystem; + uint32_t hpkgUser; + uint32_t kiss; + uint32_t linglong; + uint32_t lpkg; + uint32_t lpkgbuild; + uint32_t macports; + uint32_t moss; + uint32_t mport; + uint32_t nixDefault; + uint32_t nixSystem; + uint32_t nixUser; + uint32_t opkg; + uint32_t pacman; + uint32_t pacstall; + uint32_t paludis; + uint32_t pisi; + uint32_t pkg; + uint32_t pkgsrc; + uint32_t pkgtool; + uint32_t rpm; + uint32_t scoopGlobal; + uint32_t scoopUser; + uint32_t snap; + uint32_t soar; + uint32_t sorcery; + uint32_t winget; + uint32_t xbps; + + uint32_t all; // Make sure this goes last + + FFstrbuf pacmanBranch; +} FFPackagesResult; + +#if FF_PACKAGES_REMOVE_DISABLED + #define FF_PACKAGES_IS_ENABLED(options, pkgName) ({ (void) options; !((FF_PACKAGES_DISABLE_LIST) & (FF_PACKAGES_FLAG_ ## pkgName ## _BIT)); }) +#else + #define FF_PACKAGES_IS_ENABLED(options, pkgName) (!((options)->disabled & (FF_PACKAGES_FLAG_ ## pkgName ## _BIT))) +#endif + +const char* ffDetectPackages(FFPackagesResult* result, FFPackagesOptions* options); +bool ffPackagesReadCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, const char* filePath, const char* packageId, uint32_t* result); +bool ffPackagesWriteCache(FFstrbuf* cacheDir, FFstrbuf* cacheContent, uint32_t num_elements); + +#if defined(__linux__) || defined(__APPLE__) || defined(__GNU__) +uint32_t ffPackagesGetNix(FFstrbuf* baseDir, const char* dirname); +#endif +#ifndef _WIN32 +uint32_t ffPackagesGetNumElements(const char* dirname, bool isdir); +#endif diff --git a/src/detection/packages/packages_apple.c b/src/detection/packages/packages_apple.c new file mode 100644 index 0000000..43de97d --- /dev/null +++ b/src/detection/packages/packages_apple.c @@ -0,0 +1,56 @@ +#include "packages.h" +#include "common/io.h" +#include "common/parsing.h" +#include "common/processing.h" +#include "common/strutil.h" + +static void countBrewPackages(FFstrbuf* baseDir, FFPackagesResult* result) { + uint32_t baseDirLength = baseDir->length; + + ffStrbufAppendS(baseDir, "/Caskroom"); + result->brewCask += ffPackagesGetNumElements(baseDir->chars, true); + ffStrbufSubstrBefore(baseDir, baseDirLength); + + ffStrbufAppendS(baseDir, "/Cellar"); + result->brew += ffPackagesGetNumElements(baseDir->chars, true); + ffStrbufSubstrBefore(baseDir, baseDirLength); +} + +static uint32_t getMacPortsPackages(FFstrbuf* baseDir) { + ffStrbufAppendS(baseDir, "/var/macports/software"); + return ffPackagesGetNumElements(baseDir->chars, true); +} + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreate(); + if (FF_PACKAGES_IS_ENABLED(options, BREW)) { + const char* prefix = getenv("HOMEBREW_PREFIX"); + if (ffStrSet(prefix)) { + ffStrbufSetS(&baseDir, prefix); + } else { +#ifdef __aarch64__ + ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT "/opt/homebrew"); +#else + ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_USR "/local"); +#endif + } + countBrewPackages(&baseDir, result); + } + if (FF_PACKAGES_IS_ENABLED(options, MACPORTS)) { + const char* prefix = getenv("MACPORTS_PREFIX"); + if (ffStrSet(prefix)) { + ffStrbufSetS(&baseDir, prefix); + } else { + ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT "/opt/local"); + } + + result->macports = getMacPortsPackages(&baseDir); + } + if (FF_PACKAGES_IS_ENABLED(options, NIX)) { + ffStrbufSetS(&baseDir, FASTFETCH_TARGET_DIR_ROOT); + result->nixDefault += ffPackagesGetNix(&baseDir, "/nix/var/nix/profiles/default"); + result->nixSystem += ffPackagesGetNix(&baseDir, "/run/current-system"); + ffStrbufSet(&baseDir, &instance.state.platform.homeDir); + result->nixUser = ffPackagesGetNix(&baseDir, "/.nix-profile"); + } +} diff --git a/src/detection/packages/packages_bsd.c b/src/detection/packages/packages_bsd.c new file mode 100644 index 0000000..73772ac --- /dev/null +++ b/src/detection/packages/packages_bsd.c @@ -0,0 +1,28 @@ +#include "packages.h" + +#include "common/settings.h" + +static uint32_t getSQLite3Int(const char* dbPath, const char* query, const char* packageId) { + FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + + uint32_t num_elements; + if (ffPackagesReadCache(&cacheDir, &cacheContent, dbPath, packageId, &num_elements)) { + return num_elements; + } + + num_elements = (uint32_t) ffSettingsGetSQLite3Int(dbPath, query); + + ffPackagesWriteCache(&cacheDir, &cacheContent, num_elements); + + return num_elements; +} + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, PKG)) { + result->pkg = getSQLite3Int(FASTFETCH_TARGET_DIR_ROOT "/var/db/pkg/local.sqlite", "SELECT count(*) FROM packages", "pkg"); + } + if (FF_PACKAGES_IS_ENABLED(options, MPORT)) { + result->mport = getSQLite3Int(FASTFETCH_TARGET_DIR_ROOT "/var/db/mport/master.db", "SELECT count(*) FROM packages", "mport"); + } +} diff --git a/src/detection/packages/packages_haiku.c b/src/detection/packages/packages_haiku.c new file mode 100644 index 0000000..eee14cf --- /dev/null +++ b/src/detection/packages/packages_haiku.c @@ -0,0 +1,12 @@ +#include "packages.h" + +#include "common/io.h" + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + // TODO: Use the Package Kit C++ API instead (would account for disabled packages) + + if (FF_PACKAGES_IS_ENABLED(options, HPKG)) { + result->hpkgSystem = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/system/packages", false); + result->hpkgUser = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/boot/home/config/packages", false); + } +} diff --git a/src/detection/packages/packages_linux.c b/src/detection/packages/packages_linux.c new file mode 100644 index 0000000..79de8cb --- /dev/null +++ b/src/detection/packages/packages_linux.c @@ -0,0 +1,651 @@ +#include "packages.h" +#include "common/io.h" +#include "common/parsing.h" +#include "common/properties.h" +#include "common/settings.h" +#include "common/strutil.h" +#include "detection/os/os.h" + +static uint32_t getNumElements(FFstrbuf* baseDir, const char* dirname, bool isdir) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + uint32_t num_elements = ffPackagesGetNumElements(baseDir->chars, isdir); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; +} + +static uint32_t getNumStringsImpl(const char* filename, const char* needle) { + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + if (!ffReadFileBuffer(filename, &content)) { + return 0; + } + + uint32_t count = 0; + char* iter = content.chars; + size_t needleLength = strlen(needle); + while ((iter = memmem(iter, content.length - (size_t) (iter - content.chars), needle, needleLength)) != NULL) { + ++count; + iter += needleLength; + } + + return count; +} + +static uint32_t getNumStrings(FFstrbuf* baseDir, const char* filename, const char* needle, const char* packageId) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, filename); + + FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + + uint32_t num_elements; + if (ffPackagesReadCache(&cacheDir, &cacheContent, baseDir->chars, packageId, &num_elements)) { + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; + } + + num_elements = getNumStringsImpl(baseDir->chars, needle); + ffStrbufSubstrBefore(baseDir, baseDirLength); + + ffPackagesWriteCache(&cacheDir, &cacheContent, num_elements); + + return num_elements; +} + +static uint32_t getSQLite3Int(FFstrbuf* baseDir, const char* dbPath, const char* query, const char* packageId) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dbPath); + + FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + + uint32_t num_elements; + if (ffPackagesReadCache(&cacheDir, &cacheContent, baseDir->chars, packageId, &num_elements)) { + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; + } + + num_elements = (uint32_t) ffSettingsGetSQLite3Int(baseDir->chars, query); + ffStrbufSubstrBefore(baseDir, baseDirLength); + + ffPackagesWriteCache(&cacheDir, &cacheContent, num_elements); + + return num_elements; +} + +static uint32_t countFilesRecursiveImpl(FFstrbuf* baseDirPath, const char* filename) { + uint32_t baseDirPathLength = baseDirPath->length; + + ffStrbufAppendC(baseDirPath, '/'); + ffStrbufAppendS(baseDirPath, filename); + bool exists = ffPathExists(baseDirPath->chars, FF_PATHTYPE_FILE); + ffStrbufSubstrBefore(baseDirPath, baseDirPathLength); + if (exists) { + return 1; + } + + DIR* dirp = opendir(baseDirPath->chars); + if (dirp == NULL) { + return 0; + } + + ffStrbufAppendC(baseDirPath, '/'); + baseDirPathLength = baseDirPath->length; + + uint32_t sum = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + // According to the PMS, neither category nor package name can begin with '.', so no need to check for . or .. specifically + if (entry->d_type != DT_DIR || entry->d_name[0] == '.') { + continue; + } + + ffStrbufAppendS(baseDirPath, entry->d_name); + sum += countFilesRecursiveImpl(baseDirPath, filename); + ffStrbufSubstrBefore(baseDirPath, baseDirPathLength); + } + + closedir(dirp); + return sum; +} + +static uint32_t countFilesRecursive(FFstrbuf* baseDir, const char* dirname, const char* filename) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + uint32_t sum = countFilesRecursiveImpl(baseDir, filename); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return sum; +} + +static uint32_t getNumElementsBySuffix(FFstrbuf* baseDir, const char* dirname, const char* suffix) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + ffStrbufSubstrBefore(baseDir, baseDirLength); + if (dirp == NULL) { + return 0; + } + + uint32_t count = 0; + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] != '.' && ffStrEndsWithIgnCase(entry->d_name, suffix)) { + ++count; + } + } + return count; +} + +static uint32_t getXBPSImpl(FFstrbuf* baseDir) { + DIR* dir = opendir(baseDir->chars); + if (dir == NULL) { + return 0; + } + + uint32_t result = 0; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_type != DT_REG || !ffStrStartsWithIgnCase(entry->d_name, "pkgdb-")) { + continue; + } + + ffStrbufAppendC(baseDir, '/'); + ffStrbufAppendS(baseDir, entry->d_name); + result = getNumStringsImpl(baseDir->chars, "installed"); + break; + } + + closedir(dir); + return result; +} + +static uint32_t getXBPS(FFstrbuf* baseDir, const char* dirname) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + uint32_t result = getXBPSImpl(baseDir); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return result; +} + +static uint32_t getSnap(FFstrbuf* baseDir) { + uint32_t result = getNumElements(baseDir, "/snap", true); + + if (result == 0) { + result = getNumElements(baseDir, "/var/lib/snapd/snap", true); + } + + // Accounting for the /snap/bin folder + return result > 0 ? result - 1 : 0; +} + +#ifdef FF_HAVE_RPM + #include "common/library.h" + #include + #include + #include + #include + +static uint32_t getRpmFromLibrpm(void) { + FF_LIBRARY_LOAD(rpm, 0, "librpm" FF_LIBRARY_EXTENSION, 12) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmReadConfigFiles, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsCreate, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsInitIterator, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmdbGetIteratorCount, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmdbFreeIterator, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsFree, 0) + FF_LIBRARY_LOAD_SYMBOL(rpm, rpmlogSetMask, 0) + + // Don't print any error messages + ffrpmlogSetMask(RPMLOG_MASK(RPMLOG_EMERG)); + + if (ffrpmReadConfigFiles(NULL, NULL) != 0) { + return 0; + } + + rpmts ts = ffrpmtsCreate(); + if (ts == NULL) { + return 0; + } + + rpmdbMatchIterator mi = ffrpmtsInitIterator(ts, RPMDBI_LABEL, NULL, 0); + if (mi == NULL) { + ffrpmtsFree(ts); + return 0; + } + + int count = ffrpmdbGetIteratorCount(mi); + + ffrpmdbFreeIterator(mi); + ffrpmtsFree(ts); + + return count > 0 ? (uint32_t) count : 0; +} + +#endif // FF_HAVE_RPM + +static uint32_t getAMPackages(FFstrbuf* baseDir) { + uint32_t baseLength = baseDir->length; + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + if (!dirp) { + return 0; + } + + uint32_t result = 0; + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_name[0] == '.') { + continue; + } + if (entry->d_type == DT_DIR) { + ffStrbufAppendF(baseDir, "/%s/remove", entry->d_name); + if (ffPathExists(baseDir->chars, FF_PATHTYPE_FILE)) { + ++result; + } + ffStrbufSubstrBefore(baseDir, baseLength); + } + } + return result; +} + +static uint32_t getAMSystem(FFstrbuf* baseDir) { + // #771 + uint32_t baseDirLength = baseDir->length; + + ffStrbufAppendS(baseDir, "/opt"); + uint32_t optDirLength = baseDir->length; + + uint32_t result = 0; + + ffStrbufAppendS(baseDir, "/am/APP-MANAGER"); + if (ffPathExists(baseDir->chars, FF_PATHTYPE_FILE)) { + ++result; // `am` itself is counted as a package too + ffStrbufSubstrBefore(baseDir, optDirLength); + result = getAMPackages(baseDir); + } + + ffStrbufSubstrBefore(baseDir, baseDirLength); + return result; +} + +static uint32_t getAMUser(void) { + if (instance.state.platform.configDirs.length == 0) { + return 0; + } + + // check if $XDG_CONFIG_HOME/appman/appman-config exists + FFstrbuf* baseDir = FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs); + uint32_t baseLen = baseDir->length; + ffStrbufAppendS(baseDir, "appman/appman-config"); + FF_STRBUF_AUTO_DESTROY packagesPath = ffStrbufCreate(); + if (ffReadFileBuffer(baseDir->chars, &packagesPath)) { + ffStrbufTrimRightSpace(&packagesPath); + } + ffStrbufSubstrBefore(baseDir, baseLen); + + return packagesPath.length > 0 ? getAMPackages(&packagesPath) : 0; +} + +static int compareHash(const void* a, const void* b) { + return memcmp(a, b, 32); +} + +static uint32_t getGuixPackagesImpl(char* filename) { + FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate(); + if (!ffAppendFileBuffer(filename, &content)) { + return 0; + } + + // Count number of unique /gnu/store/ paths in PROFILE/manifest based on their hash value. + // Contains packages explicitly installed and their propagated inputs. + char* pend = content.chars; + + for (const char* pattern = content.chars; (pattern = strstr(pattern, "/gnu/store/")); pattern += 32) { + pattern += strlen("/gnu/store/"); + memmove(pend, pattern, 32); + pend += 32; + } + + if (pend == content.chars) { + return 0; + } + + qsort(content.chars, (size_t) (pend - content.chars) / 32, 32, compareHash); + + uint32_t count = 1; + for (const char* p = content.chars + 32; p < pend; p += 32) { + count += compareHash(p - 32, p) != 0; + } + + return count; +} + +static uint32_t getGuixPackages(FFstrbuf* baseDir, const char* dirname) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + ffStrbufAppendS(baseDir, "/manifest"); + uint32_t num_elements = getGuixPackagesImpl(baseDir->chars); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; +} + +static inline uint32_t getFlatpakRuntimePackagesArch(FFstrbuf* baseDir) { + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + if (dirp == NULL) { + return 0; + } + + uint32_t num_elements = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_type == DT_DIR && entry->d_name[0] != '.') { + num_elements += getNumElements(baseDir, entry->d_name, true); + } + } + + return num_elements; +} + +static inline uint32_t getFlatpakRuntimePackages(FFstrbuf* baseDir) { + ffStrbufAppendS(baseDir, "runtime/"); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + if (dirp == NULL) { + return 0; + } + + uint32_t runtimeDirLength = baseDir->length; + uint32_t num_elements = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_type == DT_DIR && entry->d_name[0] != '.') { + // `flatpak list` ignores `.Locale` and `.Debug` packages, and maybe others + const char* dot = strrchr(entry->d_name, '.'); + if (__builtin_expect(!dot, false)) { + continue; + } + dot++; + + if (ffStrEquals(dot, "Locale") || ffStrEquals(dot, "Debug")) { + continue; + } + + ffStrbufAppendS(baseDir, entry->d_name); + ffStrbufAppendC(baseDir, '/'); + num_elements += getFlatpakRuntimePackagesArch(baseDir); + ffStrbufSubstrBefore(baseDir, runtimeDirLength); + } + } + + return num_elements; +} + +static inline uint32_t getFlatpakAppPackages(FFstrbuf* baseDir) { + ffStrbufAppendS(baseDir, "app/"); + FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars); + if (dirp == NULL) { + return 0; + } + + uint32_t appDirLength = baseDir->length; + uint32_t num_elements = 0; + + struct dirent* entry; + while ((entry = readdir(dirp)) != NULL) { + if (entry->d_type == DT_DIR && entry->d_name[0] != '.') { + ffStrbufAppendS(baseDir, entry->d_name); + ffStrbufAppendS(baseDir, "/current"); + if (ffPathExists(baseDir->chars, FF_PATHTYPE_ANY)) { // Exclude deleted apps, #1856 + ++num_elements; + } + ffStrbufSubstrBefore(baseDir, appDirLength); + } + } + return num_elements; +} + +static uint32_t getFlatpakPackages(FFstrbuf* baseDir, const char* dirname) { + uint32_t num_elements = 0; + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + ffStrbufAppendS(baseDir, "/flatpak/"); + uint32_t flatpakDirLength = baseDir->length; + + num_elements += getFlatpakAppPackages(baseDir); + ffStrbufSubstrBefore(baseDir, flatpakDirLength); + + num_elements += getFlatpakRuntimePackages(baseDir); + + ffStrbufSubstrBefore(baseDir, baseDirLength); + + return num_elements; +} + +static uint32_t getPacmanPackages(FFstrbuf* baseDir) { + FF_STRBUF_AUTO_DESTROY dbPath = ffStrbufCreate(); + FF_STRBUF_AUTO_DESTROY rootDir = ffStrbufCreate(); + + // Get path to pacman.conf + uint32_t baseDirLen = baseDir->length; + ffStrbufAppendS(baseDir, "/etc/pacman.conf"); + + bool confFound = ffParsePropFileValues(baseDir->chars, 2, (FFpropquery[]) { + { "DBPath =", &dbPath }, + { "RootDir =", &rootDir }, + }); + ffStrbufSubstrBefore(baseDir, baseDirLen); + + if (confFound) { + if (dbPath.length > 0) { + // If DBPath is specified, use it + ffStrbufEnsureEndsWithC(&dbPath, '/'); + ffStrbufAppendS(&dbPath, "local"); + } else if (rootDir.length > 0) { + // ... otherwise, use RootDir + ffStrbufDestroy(&dbPath); + ffStrbufInitMove(&dbPath, &rootDir); + ffStrbufEnsureEndsWithC(&dbPath, '/'); + ffStrbufAppendS(&dbPath, "var/lib/pacman/local"); + } + } + + if (dbPath.length == 0) { + ffStrbufSetStatic(&dbPath, "/var/lib/pacman/local"); + } + + return getNumElements(baseDir, dbPath.chars, true); +} + +static void getPackageCounts(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, APK)) { + packageCounts->apk += getNumStrings(baseDir, "/lib/apk/db/installed", "C:Q", "apk"); + } + if (FF_PACKAGES_IS_ENABLED(options, DPKG)) { + packageCounts->dpkg += getNumStrings(baseDir, "/var/lib/dpkg/status", "Status: install ok installed", "dpkg"); + } + if (FF_PACKAGES_IS_ENABLED(options, LPKG)) { + packageCounts->lpkg += getNumStrings(baseDir, "/opt/Loc-OS-LPKG/installed-lpkg/Listinstalled-lpkg.list", "\n", "lpkg"); + } + if (FF_PACKAGES_IS_ENABLED(options, EMERGE)) { + packageCounts->emerge += countFilesRecursive(baseDir, "/var/db/pkg", "SIZE"); + } + if (FF_PACKAGES_IS_ENABLED(options, EOPKG)) { + packageCounts->eopkg += getNumElements(baseDir, "/var/lib/eopkg/package", true); + } + if (FF_PACKAGES_IS_ENABLED(options, FLATPAK)) { + packageCounts->flatpakSystem += getFlatpakPackages(baseDir, "/var/lib"); + } + if (FF_PACKAGES_IS_ENABLED(options, KISS)) { + packageCounts->kiss += getNumElements(baseDir, "/var/db/kiss/installed", true); + } + if (FF_PACKAGES_IS_ENABLED(options, NIX)) { + packageCounts->nixDefault += ffPackagesGetNix(baseDir, "/nix/var/nix/profiles/default"); + packageCounts->nixSystem += ffPackagesGetNix(baseDir, "/run/current-system"); + } + if (FF_PACKAGES_IS_ENABLED(options, PACMAN)) { + packageCounts->pacman += getPacmanPackages(baseDir); + } + if (FF_PACKAGES_IS_ENABLED(options, LPKGBUILD)) { + packageCounts->lpkgbuild += getNumElements(baseDir, "/opt/Loc-OS-LPKG/lpkgbuild/remove", false); + } + if (FF_PACKAGES_IS_ENABLED(options, PKGTOOL)) { + packageCounts->pkgtool += getNumElements(baseDir, "/var/log/packages", false); + } + if (FF_PACKAGES_IS_ENABLED(options, RPM)) { + // `Sigmd5` is the only table that doesn't contain the virtual `gpg-pubkey` package + packageCounts->rpm += getSQLite3Int(baseDir, "/var/lib/rpm/rpmdb.sqlite", "SELECT count(*) FROM Sigmd5", "rpm"); + } + if (FF_PACKAGES_IS_ENABLED(options, SNAP)) { + packageCounts->snap += getSnap(baseDir); + } + if (FF_PACKAGES_IS_ENABLED(options, XBPS)) { + packageCounts->xbps += getXBPS(baseDir, "/var/db/xbps"); + } + if (FF_PACKAGES_IS_ENABLED(options, BREW)) { + packageCounts->brewCask += getNumElements(baseDir, "/home/linuxbrew/.linuxbrew/Caskroom", true); + packageCounts->brew += getNumElements(baseDir, "/home/linuxbrew/.linuxbrew/Cellar", true); + } + if (FF_PACKAGES_IS_ENABLED(options, PALUDIS)) { + packageCounts->paludis += countFilesRecursive(baseDir, "/var/db/paludis/repositories", "environment.bz2"); + } + if (FF_PACKAGES_IS_ENABLED(options, OPKG)) { + packageCounts->opkg += getNumStrings(baseDir, "/usr/lib/opkg/status", "Package:", "opkg"); // openwrt + } + if (FF_PACKAGES_IS_ENABLED(options, AM)) { + packageCounts->amSystem = getAMSystem(baseDir); + } + if (FF_PACKAGES_IS_ENABLED(options, SORCERY)) { + packageCounts->sorcery += getNumStrings(baseDir, "/var/state/sorcery/packages", ":installed:", "sorcery"); + } + if (FF_PACKAGES_IS_ENABLED(options, GUIX)) { + packageCounts->guixSystem += getGuixPackages(baseDir, "/run/current-system/profile"); + } + if (FF_PACKAGES_IS_ENABLED(options, LINGLONG)) { + packageCounts->linglong += getNumElements(baseDir, "/var/lib/linglong/layers", true); + } + if (FF_PACKAGES_IS_ENABLED(options, PACSTALL)) { + packageCounts->pacstall += getNumElements(baseDir, "/var/lib/pacstall/metadata", false); + } + if (FF_PACKAGES_IS_ENABLED(options, PISI)) { + packageCounts->pisi += getNumElements(baseDir, "/var/lib/pisi/package", true); + } + if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) { + packageCounts->pkgsrc += getNumElements(baseDir, "/usr/pkg/pkgdb", DT_DIR); + } + if (FF_PACKAGES_IS_ENABLED(options, MOSS)) { + packageCounts->moss += getSQLite3Int(baseDir, "/.moss/db/state", "SELECT COUNT(*) FROM state_selections WHERE state_id = (SELECT MAX(id) FROM state)", "moss"); + } + if (FF_PACKAGES_IS_ENABLED(options, CARDS)) { + packageCounts->cards += getNumElements(baseDir, "/var/lib/pkg/DB", true); + } +} + +static void getPackageCountsRegular(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) { + getPackageCounts(baseDir, packageCounts, options); + + if (FF_PACKAGES_IS_ENABLED(options, PACMAN)) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, FASTFETCH_TARGET_DIR_ETC "/pacman-mirrors.conf"); + if (ffParsePropFile(baseDir->chars, "Branch =", &packageCounts->pacmanBranch) && packageCounts->pacmanBranch.length == 0) { + ffStrbufAppendS(&packageCounts->pacmanBranch, "stable"); + } + ffStrbufSubstrBefore(baseDir, baseDirLength); + } +} + +static void getPackageCountsBedrock(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) { + uint32_t baseDirLength = baseDir->length; + + ffStrbufAppendS(baseDir, "/bedrock/strata"); + + FF_AUTO_CLOSE_DIR DIR* dir = opendir(baseDir->chars); + if (dir == NULL) { + ffStrbufSubstrBefore(baseDir, baseDirLength); + return; + } + + ffStrbufAppendC(baseDir, '/'); + uint32_t baseDirLength2 = baseDir->length; + + struct dirent* entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_type != DT_DIR) { + continue; + } + if (entry->d_name[0] == '.') { + continue; + } + + ffStrbufAppendS(baseDir, entry->d_name); + getPackageCounts(baseDir, packageCounts, options); + ffStrbufSubstrBefore(baseDir, baseDirLength2); + } + + ffStrbufSubstrBefore(baseDir, baseDirLength); +} + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreateA(512); + ffStrbufAppendS(&baseDir, FASTFETCH_TARGET_DIR_ROOT); + + if (ffStrbufIgnCaseEqualS(&ffDetectOS()->id, "bedrock")) { + getPackageCountsBedrock(&baseDir, result, options); + } else { + getPackageCountsRegular(&baseDir, result, options); + } + +// If SQL failed, we can still try with librpm. +// This is needed on openSUSE, which seems to use a proprietary database file +// This method doesn't work on bedrock, so we do it here. +#ifdef FF_HAVE_RPM + if (FF_PACKAGES_IS_ENABLED(options, RPM) && result->rpm == 0) { + result->rpm = getRpmFromLibrpm(); + } +#endif + + ffStrbufSet(&baseDir, &instance.state.platform.homeDir); + if (FF_PACKAGES_IS_ENABLED(options, NIX)) { + // Count packages from $HOME/.nix-profile + result->nixUser += ffPackagesGetNix(&baseDir, ".nix-profile"); + + // Check in $XDG_STATE_HOME/nix/profile + FF_STRBUF_AUTO_DESTROY stateHome = ffStrbufCreate(); + const char* stateHomeEnv = getenv("XDG_STATE_HOME"); + if (ffStrSet(stateHomeEnv)) { + ffStrbufSetS(&stateHome, stateHomeEnv); + ffStrbufEnsureEndsWithC(&stateHome, '/'); + } else { + ffStrbufSet(&stateHome, &instance.state.platform.homeDir); + ffStrbufAppendS(&stateHome, ".local/state/"); + } + result->nixUser += ffPackagesGetNix(&stateHome, "nix/profile"); + + // Check in /etc/profiles/per-user/$USER + FF_STRBUF_AUTO_DESTROY userPkgsDir = ffStrbufCreateStatic("/etc/profiles/per-user/"); + result->nixUser += ffPackagesGetNix(&userPkgsDir, instance.state.platform.userName.chars); + } + + if (FF_PACKAGES_IS_ENABLED(options, GUIX)) { + result->guixUser += getGuixPackages(&baseDir, ".guix-profile"); + result->guixHome += getGuixPackages(&baseDir, ".guix-home/profile"); + } + + if (FF_PACKAGES_IS_ENABLED(options, FLATPAK)) { + result->flatpakUser = getFlatpakPackages(&baseDir, "/.local/share"); + } + + if (FF_PACKAGES_IS_ENABLED(options, AM)) { + result->amUser = getAMUser(); + } + + if (FF_PACKAGES_IS_ENABLED(options, SOAR)) { + result->soar += getSQLite3Int(&baseDir, ".local/share/soar/db/soar.db", "SELECT COUNT(DISTINCT pkg_id || pkg_name) FROM packages WHERE is_installed = true", "soar"); + } + + if (FF_PACKAGES_IS_ENABLED(options, APPIMAGE)) { + result->appimage += getNumElementsBySuffix(&baseDir, "/AppImages", ".appimage"); + result->appimage += getNumElementsBySuffix(&baseDir, "/Applications", ".appimage"); + } +} diff --git a/src/detection/packages/packages_nbsd.c b/src/detection/packages/packages_nbsd.c new file mode 100644 index 0000000..2f79126 --- /dev/null +++ b/src/detection/packages/packages_nbsd.c @@ -0,0 +1,9 @@ +#include "packages.h" + +#include "common/io.h" + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) { + result->pkgsrc = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/usr/pkg/pkgdb", true); + } +} diff --git a/src/detection/packages/packages_nix.c b/src/detection/packages/packages_nix.c new file mode 100644 index 0000000..d1cc264 --- /dev/null +++ b/src/detection/packages/packages_nix.c @@ -0,0 +1,149 @@ +#include "packages.h" +#include "common/io.h" +#include "common/processing.h" +#include "common/strutil.h" + +static bool isValidNixPkg(FFstrbuf* pkg) { + if (!ffPathExists(pkg->chars, FF_PATHTYPE_DIRECTORY)) { + return false; + } + + ffStrbufSubstrAfterLastC(pkg, '/'); + if ( + ffStrbufStartsWithS(pkg, "nixos-system-nixos-") || + ffStrbufEndsWithS(pkg, "-doc") || + ffStrbufEndsWithS(pkg, "-man") || + ffStrbufEndsWithS(pkg, "-info") || + ffStrbufEndsWithS(pkg, "-dev") || + ffStrbufEndsWithS(pkg, "-bin")) { + return false; + } + + enum { START, + DIGIT, + DOT, + MATCH } state = START; + + for (uint32_t i = 0; i < pkg->length; i++) { + char c = pkg->chars[i]; + switch (state) { + case START: + if (ffCharIsDigit(c)) { + state = DIGIT; + } + break; + case DIGIT: + if (ffCharIsDigit(c)) { + continue; + } + if (c == '.') { + state = DOT; + } else { + state = START; + } + break; + case DOT: + if (ffCharIsDigit(c)) { + state = MATCH; + } else { + state = START; + } + break; + case MATCH: + break; + } + } + + return state == MATCH; +} + +static bool checkNixCache(FFstrbuf* cacheDir, FFstrbuf* hash, uint32_t* count) { + if (!ffPathExists(cacheDir->chars, FF_PATHTYPE_FILE)) { + return false; + } + + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate(); + if (!ffReadFileBuffer(cacheDir->chars, &cacheContent)) { + return false; + } + + // Format: \n + uint32_t split = ffStrbufFirstIndexC(&cacheContent, '\n'); + if (split == cacheContent.length) { + return false; + } + + ffStrbufSetNS(hash, split, cacheContent.chars); + *count = (uint32_t) atoi(cacheContent.chars + split + 1); + + return true; +} + +static bool writeNixCache(FFstrbuf* cacheDir, FFstrbuf* hash, uint32_t count) { + FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreateCopy(hash); + ffStrbufAppendF(&cacheContent, "\n%u", count); + return ffWriteFileBuffer(cacheDir->chars, &cacheContent); +} + +static uint32_t getNixPackagesImpl(char* path) { + // Nix detection is kinda slow, so we only do it if the dir exists + if (!ffPathExists(path, FF_PATHTYPE_DIRECTORY)) { + return 0; + } + + FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreateCopy(&instance.state.platform.cacheDir); + ffStrbufEnsureEndsWithC(&cacheDir, '/'); + ffStrbufAppendS(&cacheDir, "fastfetch/packages/nix"); + ffStrbufAppendS(&cacheDir, path); + + // Check the hash first to determine if we need to recompute the count + FF_STRBUF_AUTO_DESTROY hash = ffStrbufCreateA(64); + FF_STRBUF_AUTO_DESTROY cacheHash = ffStrbufCreateA(64); + uint32_t count = 0; + + ffProcessAppendStdOut(&hash, (char* const[]) { "nix-store", "--query", "--hash", path, NULL }); + + if (checkNixCache(&cacheDir, &cacheHash, &count) && ffStrbufEqual(&hash, &cacheHash)) { + return count; + } + + // Cache is invalid, recompute the count + count = 0; + + // Implementation based on bash script from here: + // https://github.com/fastfetch-cli/fastfetch/issues/195#issuecomment-1191748222 + + FF_STRBUF_AUTO_DESTROY output = ffStrbufCreateA(1024); + + ffProcessAppendStdOut(&output, (char* const[]) { "nix-store", "--query", "--requisites", path, NULL }); + + uint32_t lineLength = 0; + for (uint32_t i = 0; i < output.length; i++) { + if (output.chars[i] != '\n') { + lineLength++; + continue; + } + + output.chars[i] = '\0'; + FFstrbuf line = { + .allocated = 0, + .length = lineLength, + .chars = output.chars + i - lineLength + }; + if (isValidNixPkg(&line)) { + count++; + } + lineLength = 0; + } + + writeNixCache(&cacheDir, &hash, count); + return count; +} + +uint32_t ffPackagesGetNix(FFstrbuf* baseDir, const char* dirname) { + uint32_t baseDirLength = baseDir->length; + ffStrbufAppendS(baseDir, dirname); + uint32_t num_elements = getNixPackagesImpl(baseDir->chars); + ffStrbufSubstrBefore(baseDir, baseDirLength); + return num_elements; +} diff --git a/src/detection/packages/packages_nosupport.c b/src/detection/packages/packages_nosupport.c new file mode 100644 index 0000000..40300fa --- /dev/null +++ b/src/detection/packages/packages_nosupport.c @@ -0,0 +1,4 @@ +#include "packages.h" + +void ffDetectPackagesImpl(FF_A_UNUSED FFPackagesResult* result, FF_A_UNUSED FFPackagesOptions* options) { +} diff --git a/src/detection/packages/packages_obsd.c b/src/detection/packages/packages_obsd.c new file mode 100644 index 0000000..c7b098e --- /dev/null +++ b/src/detection/packages/packages_obsd.c @@ -0,0 +1,9 @@ +#include "packages.h" + +#include "common/io.h" + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, PKG)) { + result->pkg = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/var/db/pkg", true); + } +} diff --git a/src/detection/packages/packages_sunos.c b/src/detection/packages/packages_sunos.c new file mode 100644 index 0000000..16b8cd7 --- /dev/null +++ b/src/detection/packages/packages_sunos.c @@ -0,0 +1,17 @@ +#include "packages.h" +#include + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, PKG)) { + yyjson_doc* doc = yyjson_read_file(FASTFETCH_TARGET_DIR_ROOT "/var/pkg/state/installed/catalog.attrs", YYJSON_READ_NOFLAG, NULL, NULL); + if (doc) { + yyjson_val* packageCount = yyjson_obj_get(yyjson_doc_get_root(doc), "package-count"); + if (packageCount) { + result->pkg = (uint32_t) yyjson_get_uint(packageCount); + } + } + } + if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) { + result->pkgsrc = ffPackagesGetNumElements(FASTFETCH_TARGET_DIR_ROOT "/usr/pkg/pkgdb", true); + } +} diff --git a/src/detection/packages/packages_windows.c b/src/detection/packages/packages_windows.c new file mode 100644 index 0000000..a3faee5 --- /dev/null +++ b/src/detection/packages/packages_windows.c @@ -0,0 +1,203 @@ +#include "packages.h" +#include "common/processing.h" +#include "common/strutil.h" +#include "common/path.h" +#include "common/windows/unicode.h" +#include "common/mallocHelper.h" +#include "common/io.h" + +#include +#include +#include "common/windows/nt.h" +#include +#include + +static uint32_t getNumElements(const char* searchPath, DWORD type, const wchar_t* ignore) { + FF_AUTO_CLOSE_FD HANDLE dfd = CreateFileA(searchPath, FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + if (dfd == INVALID_HANDLE_VALUE) { + return 0; + } + + bool flag = ignore == NULL; + uint32_t counter = 0; + alignas(8) uint8_t buffer[64 * 1024]; + BOOLEAN firstScan = TRUE; + + size_t ignoreLen = ignore ? wcslen(ignore) : 0; + + while (true) { + IO_STATUS_BLOCK ioStatus = {}; + NTSTATUS status = NtQueryDirectoryFile( + dfd, + NULL, + NULL, + NULL, + &ioStatus, + buffer, + ARRAY_SIZE(buffer), + FileDirectoryInformation, + FALSE, + NULL, + firstScan); + firstScan = FALSE; + + if (!NT_SUCCESS(status) && status != STATUS_BUFFER_OVERFLOW) { + break; + } + + for (FILE_DIRECTORY_INFORMATION* entry = (FILE_DIRECTORY_INFORMATION*) buffer; + ; + entry = (FILE_DIRECTORY_INFORMATION*) ((uint8_t*) entry + entry->NextEntryOffset)) { + if (!(entry->FileAttributes & type)) { + continue; + } + + if (!flag && + ignoreLen == entry->FileNameLength / sizeof(*entry->FileName) && + _wcsnicmp(entry->FileName, ignore, ignoreLen) == 0) { + flag = true; + continue; + } + + counter++; + + if (entry->NextEntryOffset == 0) { + break; + } + } + + if (status == STATUS_SUCCESS) { + break; // No next page + } + } + + if (type == FILE_ATTRIBUTE_DIRECTORY && counter >= 2) { + counter -= 2; // accounting for . and .. + } + + return counter; +} + +static inline void wrapYyjsonFree(yyjson_doc** doc) { + assert(doc); + if (*doc) { + yyjson_doc_free(*doc); + } +} + +static void detectScoop(FFPackagesResult* result) { + FF_STRBUF_AUTO_DESTROY scoopPath = ffStrbufCreateA(MAX_PATH + 3); + ffStrbufAppend(&scoopPath, &instance.state.platform.homeDir); + ffStrbufAppendS(&scoopPath, ".config/scoop/config.json"); + + yyjson_val* root = NULL; + + yyjson_doc* FF_A_CLEANUP(wrapYyjsonFree) doc = yyjson_read_file(scoopPath.chars, 0, NULL, NULL); + if (doc) { + root = yyjson_doc_get_root(doc); + if (!yyjson_is_obj(root)) { + root = NULL; + } + } + + { + ffStrbufClear(&scoopPath); + if (root) { + ffStrbufSetJsonVal(&scoopPath, yyjson_obj_get(root, "root_path")); + } + if (scoopPath.length == 0) { + ffStrbufSet(&scoopPath, &instance.state.platform.homeDir); + ffStrbufAppendS(&scoopPath, "/scoop"); + } + ffStrbufAppendS(&scoopPath, "/apps/"); + result->scoopUser = getNumElements(scoopPath.chars, FILE_ATTRIBUTE_DIRECTORY, L"scoop"); + } + + { + ffStrbufClear(&scoopPath); + if (root) { + ffStrbufSetJsonVal(&scoopPath, yyjson_obj_get(root, "global_path")); + } + if (scoopPath.length == 0) { + PWSTR pPath = NULL; + if (SUCCEEDED(SHGetKnownFolderPath(&FOLDERID_ProgramData, KF_FLAG_DEFAULT, NULL, &pPath))) { + ffStrbufSetWS(&scoopPath, pPath); + CoTaskMemFree(pPath); + } + ffStrbufAppendS(&scoopPath, "/scoop"); + } + ffStrbufAppendS(&scoopPath, "/apps/"); + result->scoopGlobal = getNumElements(scoopPath.chars, FILE_ATTRIBUTE_DIRECTORY, L"scoop"); + } +} + +static void detectChoco(FF_A_UNUSED FFPackagesResult* result) { + const char* chocoInstall = getenv("ChocolateyInstall"); + if (!chocoInstall || chocoInstall[0] == '\0') { + return; + } + + char chocoPath[MAX_PATH + 3]; + char* pend = ffStrCopy(chocoPath, chocoInstall, ARRAY_SIZE(chocoPath)); + ffStrCopy(pend, "/lib/", ARRAY_SIZE(chocoPath) - (size_t) (pend - chocoPath)); + result->choco = getNumElements(chocoPath, FILE_ATTRIBUTE_DIRECTORY, L"choco"); +} + +static void detectPacman(FFPackagesResult* result) { + const char* msystemPrefix = getenv("MSYSTEM_PREFIX"); + if (!msystemPrefix) { + return; + } + + // MSYS2 + char pacmanPath[MAX_PATH + 3]; + char* pend = ffStrCopy(pacmanPath, msystemPrefix, ARRAY_SIZE(pacmanPath)); + ffStrCopy(pend, "/../var/lib/pacman/local/", ARRAY_SIZE(pacmanPath) - (size_t) (pend - pacmanPath)); + result->pacman = getNumElements(pacmanPath, FILE_ATTRIBUTE_DIRECTORY, NULL); +} + +static void detectWinget(FFPackagesResult* result) { + FF_STRBUF_AUTO_DESTROY buffer = ffStrbufCreate(); + if (ffProcessAppendStdOut(&buffer, (char*[]) { + "winget.exe", + "list", + "--disable-interactivity", + NULL, + })) { + return; + } + + uint32_t index = ffStrbufFirstIndexS(&buffer, "--\r\n"); // Ignore garbage and table headers + if (index == buffer.length) { + return; + } + + uint32_t count = 0; + for ( + index += strlen("--\r\n"); + (index = ffStrbufNextIndexC(&buffer, index, '\n')) < buffer.length; + ++index) { + ++count; + } + + if (buffer.chars[buffer.length - 1] != '\n') { // count last line + ++count; + } + + result->winget = count; +} + +void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) { + if (FF_PACKAGES_IS_ENABLED(options, SCOOP)) { + detectScoop(result); + } + if (FF_PACKAGES_IS_ENABLED(options, CHOCO)) { + detectChoco(result); + } + if (FF_PACKAGES_IS_ENABLED(options, PACMAN)) { + detectPacman(result); + } + if (FF_PACKAGES_IS_ENABLED(options, WINGET)) { + detectWinget(result); + } +} -- cgit v1.2.3