master ohmu/fastfetch-openbsd / src / detection / packages / packages_linux.c
ohmu  ·  2026-10-15
  1#include "packages.h"
  2#include "common/io.h"
  3#include "common/parsing.h"
  4#include "common/properties.h"
  5#include "common/settings.h"
  6#include "common/strutil.h"
  7#include "detection/os/os.h"
  8
  9static uint32_t getNumElements(FFstrbuf* baseDir, const char* dirname, bool isdir) {
 10    uint32_t baseDirLength = baseDir->length;
 11    ffStrbufAppendS(baseDir, dirname);
 12    uint32_t num_elements = ffPackagesGetNumElements(baseDir->chars, isdir);
 13    ffStrbufSubstrBefore(baseDir, baseDirLength);
 14    return num_elements;
 15}
 16
 17static uint32_t getNumStringsImpl(const char* filename, const char* needle) {
 18    FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
 19    if (!ffReadFileBuffer(filename, &content)) {
 20        return 0;
 21    }
 22
 23    uint32_t count = 0;
 24    char* iter = content.chars;
 25    size_t needleLength = strlen(needle);
 26    while ((iter = memmem(iter, content.length - (size_t) (iter - content.chars), needle, needleLength)) != nullptr) {
 27        ++count;
 28        iter += needleLength;
 29    }
 30
 31    return count;
 32}
 33
 34static uint32_t getNumStrings(FFstrbuf* baseDir, const char* filename, const char* needle, const char* packageId) {
 35    uint32_t baseDirLength = baseDir->length;
 36    ffStrbufAppendS(baseDir, filename);
 37
 38    FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate();
 39    FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate();
 40
 41    uint32_t num_elements;
 42    if (ffPackagesReadCache(&cacheDir, &cacheContent, baseDir->chars, packageId, &num_elements)) {
 43        ffStrbufSubstrBefore(baseDir, baseDirLength);
 44        return num_elements;
 45    }
 46
 47    num_elements = getNumStringsImpl(baseDir->chars, needle);
 48    ffStrbufSubstrBefore(baseDir, baseDirLength);
 49
 50    ffPackagesWriteCache(&cacheDir, &cacheContent, num_elements);
 51
 52    return num_elements;
 53}
 54
 55static uint32_t getSQLite3Int(FFstrbuf* baseDir, const char* dbPath, const char* query, const char* packageId) {
 56    uint32_t baseDirLength = baseDir->length;
 57    ffStrbufAppendS(baseDir, dbPath);
 58
 59    FF_STRBUF_AUTO_DESTROY cacheDir = ffStrbufCreate();
 60    FF_STRBUF_AUTO_DESTROY cacheContent = ffStrbufCreate();
 61
 62    uint32_t num_elements;
 63    if (ffPackagesReadCache(&cacheDir, &cacheContent, baseDir->chars, packageId, &num_elements)) {
 64        ffStrbufSubstrBefore(baseDir, baseDirLength);
 65        return num_elements;
 66    }
 67
 68    num_elements = (uint32_t) ffSettingsGetSQLite3Int(baseDir->chars, query);
 69    ffStrbufSubstrBefore(baseDir, baseDirLength);
 70
 71    ffPackagesWriteCache(&cacheDir, &cacheContent, num_elements);
 72
 73    return num_elements;
 74}
 75
 76static uint32_t countFilesRecursiveImpl(FFstrbuf* baseDirPath, const char* filename) {
 77    uint32_t baseDirPathLength = baseDirPath->length;
 78
 79    ffStrbufAppendC(baseDirPath, '/');
 80    ffStrbufAppendS(baseDirPath, filename);
 81    bool exists = ffPathExists(baseDirPath->chars, FF_PATHTYPE_FILE);
 82    ffStrbufSubstrBefore(baseDirPath, baseDirPathLength);
 83    if (exists) {
 84        return 1;
 85    }
 86
 87    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDirPath->chars);
 88    if (dirp == nullptr) {
 89        return 0;
 90    }
 91
 92    ffStrbufAppendC(baseDirPath, '/');
 93    baseDirPathLength = baseDirPath->length;
 94
 95    uint32_t sum = 0;
 96
 97    struct dirent* entry;
 98    while ((entry = readdir(dirp)) != nullptr) {
 99        // According to the PMS, neither category nor package name can begin with '.', so no need to check for . or .. specifically
100        if (entry->d_type != DT_DIR || entry->d_name[0] == '.') {
101            continue;
102        }
103
104        ffStrbufAppendS(baseDirPath, entry->d_name);
105        sum += countFilesRecursiveImpl(baseDirPath, filename);
106        ffStrbufSubstrBefore(baseDirPath, baseDirPathLength);
107    }
108
109    return sum;
110}
111
112static uint32_t countFilesRecursive(FFstrbuf* baseDir, const char* dirname, const char* filename) {
113    uint32_t baseDirLength = baseDir->length;
114    ffStrbufAppendS(baseDir, dirname);
115    uint32_t sum = countFilesRecursiveImpl(baseDir, filename);
116    ffStrbufSubstrBefore(baseDir, baseDirLength);
117    return sum;
118}
119
120static uint32_t getNumElementsBySuffix(FFstrbuf* baseDir, const char* dirname, const char* suffix) {
121    uint32_t baseDirLength = baseDir->length;
122    ffStrbufAppendS(baseDir, dirname);
123    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars);
124    ffStrbufSubstrBefore(baseDir, baseDirLength);
125    if (dirp == nullptr) {
126        return 0;
127    }
128
129    uint32_t count = 0;
130    struct dirent* entry;
131    while ((entry = readdir(dirp)) != nullptr) {
132        if (entry->d_name[0] != '.' && ffStrEndsWithIgnCase(entry->d_name, suffix)) {
133            ++count;
134        }
135    }
136    return count;
137}
138
139static uint32_t getXBPSImpl(FFstrbuf* baseDir) {
140    FF_AUTO_CLOSE_DIR DIR* dir = opendir(baseDir->chars);
141    if (dir == nullptr) {
142        return 0;
143    }
144
145    uint32_t result = 0;
146
147    struct dirent* entry;
148    while ((entry = readdir(dir)) != nullptr) {
149        if (entry->d_type != DT_REG || !ffStrStartsWithIgnCase(entry->d_name, "pkgdb-")) {
150            continue;
151        }
152
153        ffStrbufAppendC(baseDir, '/');
154        ffStrbufAppendS(baseDir, entry->d_name);
155        result = getNumStringsImpl(baseDir->chars, "<string>installed</string>");
156        break;
157    }
158
159    return result;
160}
161
162static uint32_t getXBPS(FFstrbuf* baseDir, const char* dirname) {
163    uint32_t baseDirLength = baseDir->length;
164    ffStrbufAppendS(baseDir, dirname);
165    uint32_t result = getXBPSImpl(baseDir);
166    ffStrbufSubstrBefore(baseDir, baseDirLength);
167    return result;
168}
169
170static uint32_t getSnap(FFstrbuf* baseDir) {
171    uint32_t result = getNumElements(baseDir, "/snap", true);
172
173    if (result == 0) {
174        result = getNumElements(baseDir, "/var/lib/snapd/snap", true);
175    }
176
177    // Accounting for the /snap/bin folder
178    return result > 0 ? result - 1 : 0;
179}
180
181#ifdef FF_HAVE_RPM
182    #include "common/library.h"
183    #include <rpm/rpmlib.h>
184    #include <rpm/rpmts.h>
185    #include <rpm/rpmdb.h>
186    #include <rpm/rpmlog.h>
187
188static uint32_t getRpmFromLibrpm(void) {
189    FF_LIBRARY_LOAD(rpm, 0, "librpm" FF_LIBRARY_EXTENSION, 12)
190    FF_LIBRARY_LOAD_SYMBOL(rpm, rpmReadConfigFiles, 0)
191    FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsCreate, 0)
192    FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsInitIterator, 0)
193    FF_LIBRARY_LOAD_SYMBOL(rpm, rpmdbGetIteratorCount, 0)
194    FF_LIBRARY_LOAD_SYMBOL(rpm, rpmdbFreeIterator, 0)
195    FF_LIBRARY_LOAD_SYMBOL(rpm, rpmtsFree, 0)
196    FF_LIBRARY_LOAD_SYMBOL(rpm, rpmlogSetMask, 0)
197
198    // Don't print any error messages
199    ffrpmlogSetMask(RPMLOG_MASK(RPMLOG_EMERG));
200
201    if (ffrpmReadConfigFiles(nullptr, nullptr) != 0) {
202        return 0;
203    }
204
205    rpmts ts = ffrpmtsCreate();
206    if (ts == nullptr) {
207        return 0;
208    }
209
210    rpmdbMatchIterator mi = ffrpmtsInitIterator(ts, RPMDBI_LABEL, nullptr, 0);
211    if (mi == nullptr) {
212        ffrpmtsFree(ts);
213        return 0;
214    }
215
216    int count = ffrpmdbGetIteratorCount(mi);
217
218    ffrpmdbFreeIterator(mi);
219    ffrpmtsFree(ts);
220
221    return count > 0 ? (uint32_t) count : 0;
222}
223
224#endif // FF_HAVE_RPM
225
226static uint32_t getAMPackages(FFstrbuf* baseDir) {
227    uint32_t baseLength = baseDir->length;
228    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars);
229    if (!dirp) {
230        return 0;
231    }
232
233    uint32_t result = 0;
234    struct dirent* entry;
235    while ((entry = readdir(dirp)) != nullptr) {
236        if (entry->d_name[0] == '.') {
237            continue;
238        }
239        if (entry->d_type == DT_DIR) {
240            ffStrbufAppendF(baseDir, "/%s/remove", entry->d_name);
241            if (ffPathExists(baseDir->chars, FF_PATHTYPE_FILE)) {
242                ++result;
243            }
244            ffStrbufSubstrBefore(baseDir, baseLength);
245        }
246    }
247    return result;
248}
249
250static uint32_t getAMSystem(FFstrbuf* baseDir) {
251    // #771
252    uint32_t baseDirLength = baseDir->length;
253
254    ffStrbufAppendS(baseDir, "/opt");
255    uint32_t optDirLength = baseDir->length;
256
257    uint32_t result = 0;
258
259    ffStrbufAppendS(baseDir, "/am/APP-MANAGER");
260    if (ffPathExists(baseDir->chars, FF_PATHTYPE_FILE)) {
261        ++result; // `am` itself is counted as a package too
262        ffStrbufSubstrBefore(baseDir, optDirLength);
263        result = getAMPackages(baseDir);
264    }
265
266    ffStrbufSubstrBefore(baseDir, baseDirLength);
267    return result;
268}
269
270static uint32_t getAMUser(void) {
271    if (instance.state.platform.configDirs.length == 0) {
272        return 0;
273    }
274
275    // check if $XDG_CONFIG_HOME/appman/appman-config exists
276    FFstrbuf* baseDir = FF_LIST_FIRST(FFstrbuf, instance.state.platform.configDirs);
277    uint32_t baseLen = baseDir->length;
278    ffStrbufAppendS(baseDir, "appman/appman-config");
279    FF_STRBUF_AUTO_DESTROY packagesPath = ffStrbufCreate();
280    if (ffReadFileBuffer(baseDir->chars, &packagesPath)) {
281        ffStrbufTrimRightSpace(&packagesPath);
282    }
283    ffStrbufSubstrBefore(baseDir, baseLen);
284
285    return packagesPath.length > 0 ? getAMPackages(&packagesPath) : 0;
286}
287
288static int compareHash(const void* a, const void* b) {
289    return memcmp(a, b, 32);
290}
291
292static uint32_t getGuixPackagesImpl(char* filename) {
293    FF_STRBUF_AUTO_DESTROY content = ffStrbufCreate();
294    if (!ffAppendFileBuffer(filename, &content)) {
295        return 0;
296    }
297
298    // Count number of unique /gnu/store/ paths in PROFILE/manifest based on their hash value.
299    // Contains packages explicitly installed and their propagated inputs.
300    char* pend = content.chars;
301
302    for (const char* pattern = content.chars; (pattern = strstr(pattern, "/gnu/store/")); pattern += 32) {
303        pattern += strlen("/gnu/store/");
304        memmove(pend, pattern, 32);
305        pend += 32;
306    }
307
308    if (pend == content.chars) {
309        return 0;
310    }
311
312    qsort(content.chars, (size_t) (pend - content.chars) / 32, 32, compareHash);
313
314    uint32_t count = 1;
315    for (const char* p = content.chars + 32; p < pend; p += 32) {
316        count += compareHash(p - 32, p) != 0;
317    }
318
319    return count;
320}
321
322static uint32_t getGuixPackages(FFstrbuf* baseDir, const char* dirname) {
323    uint32_t baseDirLength = baseDir->length;
324    ffStrbufAppendS(baseDir, dirname);
325    ffStrbufAppendS(baseDir, "/manifest");
326    uint32_t num_elements = getGuixPackagesImpl(baseDir->chars);
327    ffStrbufSubstrBefore(baseDir, baseDirLength);
328    return num_elements;
329}
330
331static inline uint32_t getFlatpakRuntimePackagesArch(FFstrbuf* baseDir) {
332    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars);
333    if (dirp == nullptr) {
334        return 0;
335    }
336
337    uint32_t num_elements = 0;
338
339    struct dirent* entry;
340    while ((entry = readdir(dirp)) != nullptr) {
341        if (entry->d_type == DT_DIR && entry->d_name[0] != '.') {
342            num_elements += getNumElements(baseDir, entry->d_name, true);
343        }
344    }
345
346    return num_elements;
347}
348
349static inline uint32_t getFlatpakRuntimePackages(FFstrbuf* baseDir) {
350    ffStrbufAppendS(baseDir, "runtime/");
351    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars);
352    if (dirp == nullptr) {
353        return 0;
354    }
355
356    uint32_t runtimeDirLength = baseDir->length;
357    uint32_t num_elements = 0;
358
359    struct dirent* entry;
360    while ((entry = readdir(dirp)) != nullptr) {
361        if (entry->d_type == DT_DIR && entry->d_name[0] != '.') {
362            // `flatpak list` ignores `.Locale` and `.Debug` packages, and maybe others
363            const char* dot = strrchr(entry->d_name, '.');
364            if (__builtin_expect(!dot, false)) {
365                continue;
366            }
367            dot++;
368
369            if (ffStrEquals(dot, "Locale") || ffStrEquals(dot, "Debug")) {
370                continue;
371            }
372
373            ffStrbufAppendS(baseDir, entry->d_name);
374            ffStrbufAppendC(baseDir, '/');
375            num_elements += getFlatpakRuntimePackagesArch(baseDir);
376            ffStrbufSubstrBefore(baseDir, runtimeDirLength);
377        }
378    }
379
380    return num_elements;
381}
382
383static inline uint32_t getFlatpakAppPackages(FFstrbuf* baseDir) {
384    ffStrbufAppendS(baseDir, "app/");
385    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars);
386    if (dirp == nullptr) {
387        return 0;
388    }
389
390    uint32_t appDirLength = baseDir->length;
391    uint32_t num_elements = 0;
392
393    struct dirent* entry;
394    while ((entry = readdir(dirp)) != nullptr) {
395        if (entry->d_type == DT_DIR && entry->d_name[0] != '.') {
396            ffStrbufAppendS(baseDir, entry->d_name);
397            ffStrbufAppendS(baseDir, "/current");
398            if (ffPathExists(baseDir->chars, FF_PATHTYPE_ANY)) { // Exclude deleted apps, #1856
399                ++num_elements;
400            }
401            ffStrbufSubstrBefore(baseDir, appDirLength);
402        }
403    }
404    return num_elements;
405}
406
407static uint32_t getFlatpakPackages(FFstrbuf* baseDir, const char* dirname) {
408    uint32_t num_elements = 0;
409    uint32_t baseDirLength = baseDir->length;
410    ffStrbufAppendS(baseDir, dirname);
411    ffStrbufAppendS(baseDir, "/flatpak/");
412    uint32_t flatpakDirLength = baseDir->length;
413
414    num_elements += getFlatpakAppPackages(baseDir);
415    ffStrbufSubstrBefore(baseDir, flatpakDirLength);
416
417    num_elements += getFlatpakRuntimePackages(baseDir);
418
419    ffStrbufSubstrBefore(baseDir, baseDirLength);
420
421    return num_elements;
422}
423
424static uint32_t getPacmanPackages(FFstrbuf* baseDir) {
425    FF_STRBUF_AUTO_DESTROY dbPath = ffStrbufCreate();
426    FF_STRBUF_AUTO_DESTROY rootDir = ffStrbufCreate();
427
428    // Get path to pacman.conf
429    uint32_t baseDirLen = baseDir->length;
430    ffStrbufAppendS(baseDir, "/etc/pacman.conf");
431
432    bool confFound = ffParsePropFileValues(baseDir->chars, 2, (FFpropquery[]) {
433                                                                  { "DBPath =", &dbPath },
434                                                                  { "RootDir =", &rootDir },
435                                                              });
436    ffStrbufSubstrBefore(baseDir, baseDirLen);
437
438    if (confFound) {
439        if (dbPath.length > 0) {
440            // If DBPath is specified, use it
441            ffStrbufEnsureEndsWithC(&dbPath, '/');
442            ffStrbufAppendS(&dbPath, "local");
443        } else if (rootDir.length > 0) {
444            // ... otherwise, use RootDir
445            ffStrbufDestroy(&dbPath);
446            ffStrbufInitMove(&dbPath, &rootDir);
447            ffStrbufEnsureEndsWithC(&dbPath, '/');
448            ffStrbufAppendS(&dbPath, "var/lib/pacman/local");
449        }
450    }
451
452    if (dbPath.length == 0) {
453        ffStrbufSetStatic(&dbPath, "/var/lib/pacman/local");
454    }
455
456    return getNumElements(baseDir, dbPath.chars, true);
457}
458
459static uint32_t getEmergePackagesImpl(FFstrbuf* baseDir) {
460    FF_AUTO_CLOSE_DIR DIR* dirp = opendir(baseDir->chars);
461    if (dirp == nullptr)
462        return 0;
463
464    uint32_t result = 0;
465
466    struct dirent *entry;
467    while ((entry = readdir(dirp)) != nullptr)
468    {
469        if (entry->d_type != DT_DIR || entry->d_name[0] == '.')
470            continue;
471
472        result += getNumElements(baseDir, entry->d_name, true);
473    }
474    return result;
475}
476
477static uint32_t getEmergePackages(FFstrbuf* baseDir, const char* dirname) {
478    uint32_t baseDirLength = baseDir->length;
479    ffStrbufAppendS(baseDir, dirname);
480    ffStrbufAppendC(baseDir, '/');
481    uint32_t result = getEmergePackagesImpl(baseDir);
482    ffStrbufSubstrBefore(baseDir, baseDirLength);
483    return result;
484}
485
486static void getPackageCounts(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) {
487    if (FF_PACKAGES_IS_ENABLED(options, APK)) {
488        packageCounts->apk += getNumStrings(baseDir, "/lib/apk/db/installed", "C:Q", "apk");
489    }
490    if (FF_PACKAGES_IS_ENABLED(options, CRUX)) {
491        packageCounts->crux += getNumStrings(baseDir, "/var/lib/pkg/db", "\n\n", "crux");
492    }
493    if (FF_PACKAGES_IS_ENABLED(options, DPKG)) {
494        packageCounts->dpkg += getNumStrings(baseDir, "/var/lib/dpkg/status", "Status: install ok installed", "dpkg");
495    }
496    if (FF_PACKAGES_IS_ENABLED(options, LPKG)) {
497        packageCounts->lpkg += getNumStrings(baseDir, "/opt/Loc-OS-LPKG/installed-lpkg/Listinstalled-lpkg.list", "\n", "lpkg");
498    }
499    if (FF_PACKAGES_IS_ENABLED(options, EMERGE)) {
500        packageCounts->emerge += getEmergePackages(baseDir, "/var/db/pkg");
501    }
502    if (FF_PACKAGES_IS_ENABLED(options, EOPKG)) {
503        packageCounts->eopkg += getNumElements(baseDir, "/var/lib/eopkg/package", true);
504    }
505    if (FF_PACKAGES_IS_ENABLED(options, FLATPAK)) {
506        packageCounts->flatpakSystem += getFlatpakPackages(baseDir, "/var/lib");
507    }
508    if (FF_PACKAGES_IS_ENABLED(options, KISS)) {
509        packageCounts->kiss += getNumElements(baseDir, "/var/db/kiss/installed", true);
510    }
511    if (FF_PACKAGES_IS_ENABLED(options, NIX)) {
512        packageCounts->nixDefault += ffPackagesGetNix(baseDir, "/nix/var/nix/profiles/default");
513        packageCounts->nixSystem += ffPackagesGetNix(baseDir, "/run/current-system");
514    }
515    if (FF_PACKAGES_IS_ENABLED(options, PACMAN)) {
516        packageCounts->pacman += getPacmanPackages(baseDir);
517    }
518    if (FF_PACKAGES_IS_ENABLED(options, LPKGBUILD)) {
519        packageCounts->lpkgbuild += getNumElements(baseDir, "/opt/Loc-OS-LPKG/lpkgbuild/remove", false);
520    }
521    if (FF_PACKAGES_IS_ENABLED(options, PKGTOOL)) {
522        packageCounts->pkgtool += getNumElements(baseDir, "/var/log/packages", false);
523    }
524    if (FF_PACKAGES_IS_ENABLED(options, PORG)) {
525        packageCounts->porg += getNumElements(baseDir, "/var/log/porg", false);
526    }
527    if (FF_PACKAGES_IS_ENABLED(options, RPM)) {
528        // `Sigmd5` is the only table that doesn't contain the virtual `gpg-pubkey` package
529        packageCounts->rpm += getSQLite3Int(baseDir, "/var/lib/rpm/rpmdb.sqlite", "SELECT count(*) FROM Sigmd5", "rpm");
530    }
531    if (FF_PACKAGES_IS_ENABLED(options, RUM)) {
532        packageCounts->rum += getSQLite3Int(baseDir, "/var/lib/rakuos/rum-rpmdb/rpmdb.sqlite", "SELECT count(*) FROM Sigmd5", "rum");
533    }
534    if (FF_PACKAGES_IS_ENABLED(options, SNAP)) {
535        packageCounts->snap += getSnap(baseDir);
536    }
537    if (FF_PACKAGES_IS_ENABLED(options, XBPS)) {
538        packageCounts->xbps += getXBPS(baseDir, "/var/db/xbps");
539    }
540    if (FF_PACKAGES_IS_ENABLED(options, BREW)) {
541        packageCounts->brewCask += getNumElements(baseDir, "/home/linuxbrew/.linuxbrew/Caskroom", true);
542        packageCounts->brew += getNumElements(baseDir, "/home/linuxbrew/.linuxbrew/Cellar", true);
543    }
544    if (FF_PACKAGES_IS_ENABLED(options, PALUDIS)) {
545        packageCounts->paludis += countFilesRecursive(baseDir, "/var/db/paludis/repositories", "environment.bz2");
546    }
547    if (FF_PACKAGES_IS_ENABLED(options, OPKG)) {
548        packageCounts->opkg += getNumStrings(baseDir, "/usr/lib/opkg/status", "Package:", "opkg"); // openwrt
549    }
550    if (FF_PACKAGES_IS_ENABLED(options, AM)) {
551        packageCounts->amSystem = getAMSystem(baseDir);
552    }
553    if (FF_PACKAGES_IS_ENABLED(options, SORCERY)) {
554        packageCounts->sorcery += getNumStrings(baseDir, "/var/state/sorcery/packages", ":installed:", "sorcery");
555    }
556    if (FF_PACKAGES_IS_ENABLED(options, GUIX)) {
557        packageCounts->guixSystem += getGuixPackages(baseDir, "/run/current-system/profile");
558    }
559    if (FF_PACKAGES_IS_ENABLED(options, LINGLONG)) {
560        packageCounts->linglong += getNumElements(baseDir, "/var/lib/linglong/layers", true);
561    }
562    if (FF_PACKAGES_IS_ENABLED(options, PACSTALL)) {
563        packageCounts->pacstall += getNumElements(baseDir, "/var/lib/pacstall/metadata", false);
564    }
565    if (FF_PACKAGES_IS_ENABLED(options, PISI)) {
566        packageCounts->pisi += getNumElements(baseDir, "/var/lib/pisi/package", true);
567    }
568    if (FF_PACKAGES_IS_ENABLED(options, PKGSRC)) {
569        packageCounts->pkgsrc += getNumElements(baseDir, "/usr/pkg/pkgdb", DT_DIR);
570    }
571    if (FF_PACKAGES_IS_ENABLED(options, MOSS)) {
572        packageCounts->moss += getSQLite3Int(baseDir, "/.moss/db/state", "SELECT COUNT(*) FROM state_selections WHERE state_id = (SELECT MAX(id) FROM state)", "moss");
573    }
574    if (FF_PACKAGES_IS_ENABLED(options, CARDS)) {
575        packageCounts->cards += getNumElements(baseDir, "/var/lib/pkg/DB", true);
576    }
577}
578
579static void getPackageCountsRegular(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) {
580    getPackageCounts(baseDir, packageCounts, options);
581
582    if (FF_PACKAGES_IS_ENABLED(options, PACMAN)) {
583        uint32_t baseDirLength = baseDir->length;
584        ffStrbufAppendS(baseDir, FASTFETCH_TARGET_DIR_ETC "/pacman-mirrors.conf");
585        if (ffParsePropFile(baseDir->chars, "Branch =", &packageCounts->pacmanBranch) && packageCounts->pacmanBranch.length == 0) {
586            ffStrbufAppendS(&packageCounts->pacmanBranch, "stable");
587        }
588        ffStrbufSubstrBefore(baseDir, baseDirLength);
589    }
590}
591
592static void getPackageCountsBedrock(FFstrbuf* baseDir, FFPackagesResult* packageCounts, FFPackagesOptions* options) {
593    uint32_t baseDirLength = baseDir->length;
594
595    ffStrbufAppendS(baseDir, "/bedrock/strata");
596
597    FF_AUTO_CLOSE_DIR DIR* dir = opendir(baseDir->chars);
598    if (dir == nullptr) {
599        ffStrbufSubstrBefore(baseDir, baseDirLength);
600        return;
601    }
602
603    ffStrbufAppendC(baseDir, '/');
604    uint32_t baseDirLength2 = baseDir->length;
605
606    struct dirent* entry;
607    while ((entry = readdir(dir)) != nullptr) {
608        if (entry->d_type != DT_DIR) {
609            continue;
610        }
611        if (entry->d_name[0] == '.') {
612            continue;
613        }
614
615        ffStrbufAppendS(baseDir, entry->d_name);
616        getPackageCounts(baseDir, packageCounts, options);
617        ffStrbufSubstrBefore(baseDir, baseDirLength2);
618    }
619
620    ffStrbufSubstrBefore(baseDir, baseDirLength);
621}
622
623static uint32_t getInstallReleasePackages(FFstrbuf* baseDir) {
624    uint32_t result = 0;
625
626    uint32_t baseDirLength = baseDir->length;
627    ffStrbufAppendS(baseDir, ".config/install_release/state.json");
628    if (ffPathExists(baseDir->chars, FF_PATHTYPE_ANY)) {
629        yyjson_doc* doc = yyjson_read_file(baseDir->chars, YYJSON_READ_NOFLAG, nullptr, nullptr);
630        if (doc != nullptr) {
631            yyjson_val* root = yyjson_doc_get_root(doc);
632            if (yyjson_is_obj(root)) {
633                result = (uint32_t) yyjson_obj_size(root);
634            }
635            yyjson_doc_free(doc);
636        }
637    }
638    ffStrbufSubstrBefore(baseDir, baseDirLength);
639    return result;
640}
641
642void ffDetectPackagesImpl(FFPackagesResult* result, FFPackagesOptions* options) {
643    FF_STRBUF_AUTO_DESTROY baseDir = ffStrbufCreateA(512);
644    ffStrbufAppendS(&baseDir, FASTFETCH_TARGET_DIR_ROOT);
645
646    if (ffStrbufIgnCaseEqualS(&ffDetectOS()->id, "bedrock")) {
647        getPackageCountsBedrock(&baseDir, result, options);
648    } else {
649        getPackageCountsRegular(&baseDir, result, options);
650    }
651
652// If SQL failed, we can still try with librpm.
653// This is needed on openSUSE, which seems to use a proprietary database file
654// This method doesn't work on bedrock, so we do it here.
655#ifdef FF_HAVE_RPM
656    if (FF_PACKAGES_IS_ENABLED(options, RPM) && result->rpm == 0) {
657        result->rpm = getRpmFromLibrpm();
658    }
659#endif
660
661    ffStrbufSet(&baseDir, &instance.state.platform.homeDir);
662    if (FF_PACKAGES_IS_ENABLED(options, NIX)) {
663        // Count packages from $HOME/.nix-profile
664        result->nixUser += ffPackagesGetNix(&baseDir, ".nix-profile");
665
666        // Check in $XDG_STATE_HOME/nix/profile
667        FF_STRBUF_AUTO_DESTROY stateHome = ffStrbufCreate();
668        const char* stateHomeEnv = getenv("XDG_STATE_HOME");
669        if (ffStrSet(stateHomeEnv)) {
670            ffStrbufSetS(&stateHome, stateHomeEnv);
671            ffStrbufEnsureEndsWithC(&stateHome, '/');
672        } else {
673            ffStrbufSet(&stateHome, &instance.state.platform.homeDir);
674            ffStrbufAppendS(&stateHome, ".local/state/");
675        }
676        result->nixUser += ffPackagesGetNix(&stateHome, "nix/profile");
677
678        // Check in /etc/profiles/per-user/$USER
679        FF_STRBUF_AUTO_DESTROY userPkgsDir = ffStrbufCreateStatic("/etc/profiles/per-user/");
680        result->nixUser += ffPackagesGetNix(&userPkgsDir, instance.state.platform.userName.chars);
681    }
682
683    if (FF_PACKAGES_IS_ENABLED(options, GUIX)) {
684        result->guixUser += getGuixPackages(&baseDir, ".guix-profile");
685        result->guixHome += getGuixPackages(&baseDir, ".guix-home/profile");
686    }
687
688    if (FF_PACKAGES_IS_ENABLED(options, FLATPAK)) {
689        result->flatpakUser = getFlatpakPackages(&baseDir, "/.local/share");
690    }
691
692    if (FF_PACKAGES_IS_ENABLED(options, AM)) {
693        result->amUser = getAMUser();
694    }
695
696    if (FF_PACKAGES_IS_ENABLED(options, SOAR)) {
697        result->soar += getSQLite3Int(&baseDir, ".local/share/soar/db/soar.db", "SELECT COUNT(DISTINCT pkg_id || pkg_name) FROM packages WHERE is_installed = true", "soar");
698    }
699
700    if (FF_PACKAGES_IS_ENABLED(options, APPIMAGE)) {
701        result->appimage += getNumElementsBySuffix(&baseDir, "/AppImages", ".appimage");
702        result->appimage += getNumElementsBySuffix(&baseDir, "/Applications", ".appimage");
703    }
704
705    if (FF_PACKAGES_IS_ENABLED(options, INSTALLRELEASE)) {
706        result->installrelease = getInstallReleasePackages(&baseDir);
707    }
708}