summaryrefslogtreecommitdiffstats
path: root/src/detection/packages
diff options
context:
space:
mode:
authorsumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
committersumuel <samuel@yakubos.org>2026-08-17 20:44:55 +0000
commit76424950e373d3b04ac3dd13019151bfba3e8423 (patch)
tree4c3cfdbda039e592b9186be3e28f8d7cfd439e8a /src/detection/packages
Add the files
Diffstat (limited to 'src/detection/packages')
-rw-r--r--src/detection/packages/packages.c119
-rw-r--r--src/detection/packages/packages.h72
-rw-r--r--src/detection/packages/packages_apple.c56
-rw-r--r--src/detection/packages/packages_bsd.c28
-rw-r--r--src/detection/packages/packages_haiku.c12
-rw-r--r--src/detection/packages/packages_linux.c651
-rw-r--r--src/detection/packages/packages_nbsd.c9
-rw-r--r--src/detection/packages/packages_nix.c149
-rw-r--r--src/detection/packages/packages_nosupport.c4
-rw-r--r--src/detection/packages/packages_obsd.c9
-rw-r--r--src/detection/packages/packages_sunos.c17
-rw-r--r--src/detection/packages/packages_windows.c203
12 files changed, 1329 insertions, 0 deletions
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 <inttypes.h>
+#include <stddef.h>
+
+#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, "<string>installed</string>");
+ 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 <rpm/rpmlib.h>
+ #include <rpm/rpmts.h>
+ #include <rpm/rpmdb.h>
+ #include <rpm/rpmlog.h>
+
+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: <hash>\n<count>
+ 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 <dirent.h>
+
+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 <stdalign.h>
+#include <windows.h>
+#include "common/windows/nt.h"
+#include <ntstatus.h>
+#include <shlobj.h>
+
+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);
+ }
+}