#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"); } }