#include "./manager.h" #include "./alpmdatabase.h" #include "./utilities.h" #include "./list.h" #include "./config.h" #include "../network/userrepository.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace IoUtilities; using namespace ConversionUtilities; namespace RepoIndex { /*! * \cond */ constexpr int defaultSigLevel = ALPM_SIG_PACKAGE | ALPM_SIG_PACKAGE_OPTIONAL | ALPM_SIG_DATABASE | ALPM_SIG_DATABASE_OPTIONAL; inline ostream &operator <<(ostream &stream, const QString &str) { stream << str.toLocal8Bit().data(); return stream; } /*! * \endcond */ /*! * \brief The Manager class helps accessing ALPM. * * - It queries the ALPM for database and package information. * - It serializes the information as JSON objects used by the network classes and the web interface. */ /*! * \brief Creates a new manager class; initializes a new ALPM handle. * \param rootdir Specifies the root directory. * \param dbpath Specifies the database directory. */ Manager::Manager(const Config &config) : m_config(config), m_writeCacheBeforeGone(true), m_sigLevel(defaultSigLevel), m_localFileSigLevel(ALPM_SIG_USE_DEFAULT) { alpm_errno_t err; if(!(m_handle = alpm_initialize(config.alpmRootDir().toLocal8Bit().data(), config.alpmDbPath().toLocal8Bit().data(), &err))) { throw runtime_error(string("Cannot initialize ALPM: ") + alpm_strerror(err)); } m_localDb = make_unique(alpm_get_localdb(m_handle), config.alpmDbPath()); if(config.isAurEnabled()) { m_userRepo = make_unique(m_networkAccessManager); } } /*! * \brief Releases the associated ALPM handle. */ Manager::~Manager() { if(m_writeCacheBeforeGone) { writeCache(); } alpm_release(m_handle); } /*! * \brief Returns the first package with the specified name from the specified database. */ AlpmPackage *Manager::packageFromDatabase(const QString &dbName, const QString &pkgName) { if(auto *db = databaseByName(dbName)) { return static_cast(db->packageByName(pkgName)); } else { return nullptr; } } /*! * \brief Returns the first package with the specified name from the specified database. */ const AlpmPackage *Manager::packageFromDatabase(const QString &dbName, const QString &pkgName) const { if(const auto *db = databaseByName(dbName)) { return static_cast(db->packageByName(pkgName)); } else { return nullptr; } } /*! * \brief Returns the first package with the specified \a name from one of the sync databases. */ AlpmPackage *Manager::packageFromSyncDatabases(const QString &pkgName) { for(const auto &dbEntry : syncDatabases()) { if(auto *pkg = dbEntry.second->packageByName(pkgName)) { return static_cast(pkg); } } return nullptr; } /*! * \brief Returns the first package with the specified \a name from one of the sync databases. */ const AlpmPackage *Manager::packageFromSyncDatabases(const QString &pkgName) const { for(const auto &dbEntry : syncDatabases()) { if(const auto *pkg = dbEntry.second->packageByName(pkgName)) { return static_cast(pkg); } } return nullptr; } /*! * \brief Creates a new package instance for the specified package file. * * Verifies the integrity of the file if the option is set. */ unique_ptr Manager::packageFromFile(const char *fileName, bool verifyIntegrity) { alpm_pkg_t *pkg; if(alpm_pkg_load(m_handle, fileName, verifyIntegrity, static_cast(m_localFileSigLevel), &pkg) != 0) { throw runtime_error(string("Unable to load package file: ") + alpm_strerror(alpm_errno(m_handle))); } else { return make_unique(pkg); } } /*! * \brief Returns the first package satisfiing the specified dependency from one of the available package sources (excluding the local database). */ Package *Manager::packageProviding(const Dependency &dependency) { for(auto &dbEntry : syncDatabases()) { if(auto *pkg = dbEntry.second->packageProviding(dependency)) { return pkg; } } if(config().isAurEnabled()) { // TODO: check AUR } return nullptr; } /*! * \brief Returns the first package satisfiing the specified dependency from one of the available package sources (excluding the local database * and sources requirering requests such as the AUR). */ const Package *Manager::packageProviding(const Dependency &dependency) const { for(const auto &dbEntry : syncDatabases()) { if(const auto *pkg = dbEntry.second->packageProviding(dependency)) { return pkg; } } return nullptr; } /*! * \brief Sets the install reason for the specified \a package. */ void Manager::setInstallReason(AlpmPackage *package, alpm_pkgreason_t reason) { if(alpm_pkg_set_reason(package->ptr(), reason)) { throw runtime_error(string("Unable to set install reason of the package ") + package->name().toLocal8Bit().data() + ": " + alpm_strerror(alpm_errno(m_handle))); } } /*! * \brief Returns the last value with the specified \a key in the specified multimap. */ const string &lastValue(const multimap &mm, const string &key) { static const string defaultValue; const auto i = find_if(mm.crbegin(), mm.crend(), [&key] (const pair &i) { return i.first == key; }); return i != mm.crend() ? i->second : defaultValue; } /*! * \brief Parses a "SigLevel" denotation from pacman config file. */ int Manager::parseSigLevel(const string &sigLevelStr) { int sigLevel = defaultSigLevel; // split sig level denotation into parts const auto parts = splitString >(sigLevelStr, " "); for(const auto &part : parts) { // determine whether part affect packages, databases or both bool package = true, db = true; const char *partStart = part.data(); if(!strncmp(partStart, "Package", 7)) { db = false; // package only part partStart += 7; } else if(!strncmp(partStart, "Database", 8)) { package = false; // db only part partStart += 8; } // set sig level according part if(!strcmp(partStart, "Never")) { if(package) { sigLevel &= ~ALPM_SIG_PACKAGE; } if(db) { sigLevel &= ~ALPM_SIG_DATABASE; } } else if(!strcmp(partStart, "Optional")) { if(package) { sigLevel |= ALPM_SIG_PACKAGE | ALPM_SIG_PACKAGE_OPTIONAL; } if(db) { sigLevel |= ALPM_SIG_DATABASE | ALPM_SIG_DATABASE_OPTIONAL; } } else if(!strcmp(partStart, "Required")) { if(package) { sigLevel |= ALPM_SIG_PACKAGE; sigLevel &= ~ALPM_SIG_PACKAGE_OPTIONAL; } if(db) { sigLevel |= ALPM_SIG_DATABASE; sigLevel &= ~ALPM_SIG_DATABASE_OPTIONAL; } } else if(!strcmp(partStart, "TrustedOnly")) { if(package) { sigLevel &= ~(ALPM_SIG_PACKAGE_MARGINAL_OK | ALPM_SIG_PACKAGE_UNKNOWN_OK); } if(db) { sigLevel &= ~(ALPM_SIG_DATABASE_MARGINAL_OK | ALPM_SIG_DATABASE_UNKNOWN_OK); } } else if(!strcmp(partStart, "TrustAll")) { if(package) { sigLevel |= ALPM_SIG_PACKAGE_MARGINAL_OK | ALPM_SIG_PACKAGE_UNKNOWN_OK; } if(db) { sigLevel |= ALPM_SIG_DATABASE_MARGINAL_OK | ALPM_SIG_DATABASE_UNKNOWN_OK; } } else { cerr << shchar << "Warning: Invalid value \"" << part << "\" for \"SigLevel\" in pacman config file will be ignored." << endl; } } return sigLevel; } /*! * \brief Parses a "Usage" denotation from pacman config file. */ int Manager::parseUsage(const string &usageStr) { int usage = 0; const auto parts = splitString >(usageStr, " "); for(const auto &part : parts) { if(part == "Sync") { usage |= ALPM_DB_USAGE_SYNC; } else if(part == "Search") { usage |= ALPM_DB_USAGE_SEARCH; } else if(part == "Install") { usage |= ALPM_DB_USAGE_INSTALL; } else if(part == "Upgrade") { usage |= ALPM_DB_USAGE_UPGRADE; } else { cerr << shchar << "Warning: Invalid value \"" << part << "\" for \"Usage\" in pacman config file will be ignored." << endl; } } return usage ? usage : ALPM_DB_USAGE_ALL; } /*! * \brief Parses and applies the Pacman configuration. Registers the listed sync databases. */ void Manager::applyPacmanConfig() { // open config file and parse as ini try { IniFile configIni; { fstream configFile; configFile.exceptions(ios_base::failbit | ios_base::badbit); configFile.open(m_config.pacmanConfFile().toLocal8Bit().data(), ios_base::in); configIni.parse(configFile); } // determine current cpu archtitecture (required for server URLs) static const string sysArch(QSysInfo::currentCpuArchitecture().toStdString()); string arch = sysArch; const auto &config = configIni.data(); // read relevant options static const string sigLevelKey("SigLevel"); static const string usageKey("Usage"); int globalSigLevel; try { const auto &options = config.at("options"); const auto &specifiedArch = lastValue(options, "Architecture"); if(!specifiedArch.empty() && specifiedArch != "auto") { arch = specifiedArch; } const auto &specifiedDir = lastValue(options, "CacheDir"); if(!specifiedDir.empty()) { m_pacmanCacheDir = QString::fromStdString(specifiedDir); } globalSigLevel = parseSigLevel(lastValue(options, sigLevelKey)); } catch(const out_of_range &) { // no options specified globalSigLevel = defaultSigLevel; } // register sync databases unordered_map includedInis; for(const auto &scope : config) { if(scope.first != "options") { // read and validate database name QString dbName = QString::fromLocal8Bit(scope.first.c_str()); if(dbName.compare(QLatin1String("local"), Qt::CaseInsensitive) == 0) { cerr << shchar << "Error: Unable to add database from pacman config: The database name mustn't be \"local\" because this name is reserved for the local database." << endl; } else if(dbName.startsWith(QLatin1String("aur"), Qt::CaseInsensitive)) { cerr << shchar << "Error: Unable to add database from pacman config: The database name mustn't start with \"aur\" because this name is reserved for the Arch Linux User Repository." << endl; } else if(m_syncDbs.count(dbName)) { cerr << shchar << "Error: Unable to add database from pacman config: Database names must be unique. Ignoring second occurance of database \"" << scope.first << "\"." << endl; } else { // read sig level and usage const auto &sigLevelStr = lastValue(scope.second, sigLevelKey); int sigLevel = sigLevelStr.empty() ? globalSigLevel : parseSigLevel(sigLevelStr); int usage = parseUsage(lastValue(scope.second, usageKey)); // try to register database in the ALPM system if(alpm_db_t *db = alpm_register_syncdb(m_handle, scope.first.c_str(), static_cast(sigLevel))) { // set usage if(alpm_db_set_usage(db, static_cast(usage)) == 0) { if(m_config.isVerbose() || m_config.runServer()) { cerr << shchar << "Added database [" << scope.first << "]" << endl; } } else { if(m_config.isVerbose() || m_config.runServer()) { cerr << shchar << "Warning: Added database [" << scope.first << "] but failed to set usage" << endl; } } // add servers for(auto range = scope.second.equal_range("Server"); range.first != range.second; ++range.first) { string url = range.first->second; findAndReplace(url, "$repo", scope.first); findAndReplace(url, "$arch", arch); alpm_db_add_server(db, url.c_str()); if(m_config.isVerbose() || m_config.runServer()) { cerr << shchar << "Added server: " << url << endl; } } // add included servers for(auto range = scope.second.equal_range("Include"); range.first != range.second; ++range.first) { const auto &path = range.first->second; auto &includedIni = includedInis[path]; if(includedIni.data().empty()) { try { fstream includedFile; includedFile.exceptions(ios_base::failbit | ios_base::badbit); includedFile.open(path, ios_base::in); includedIni.parse(includedFile); } catch (const ios_base::failure &) { cerr << shchar << "Error: An IO exception occured when parsing the included file \"" << path << "\"." << endl; } } try { const auto &includedScope = includedIni.data().at(string()); for(auto range = includedScope.equal_range("Server"); range.first != range.second; ++range.first) { string url = range.first->second; findAndReplace(url, "$repo", scope.first); findAndReplace(url, "$arch", arch); alpm_db_add_server(db, url.c_str()); if(m_config.isVerbose() || m_config.runServer()) { cerr << shchar << "Added server: " << url << endl; } } } catch (const out_of_range &) { cerr << shchar << "Warning: Included file \"" << path << "\" has no values." << endl; } } auto emplaced = m_syncDbs.emplace(dbName, make_unique(db, m_config.alpmDbPath())); // add sync db to internal map if(usage & ALPM_DB_USAGE_UPGRADE) { // -> db is used to upgrade local database localDataBase()->upgradeSources() << emplaced.first->second.get(); } } else { cerr << shchar << "Error: Unable to add sync database [" << scope.first << "]" << endl; } } } } } catch (const ios_base::failure &) { throw ios_base::failure("Error: An IO exception occured when parsing the config file."); } } /*! * \brief Applies the repository index configuration. */ void Manager::applyRepoIndexConfig() { // check whether an entry already exists, if not create a new one for(const RepoEntry &repoEntry : m_config.repoEntries()) { AlpmDatabase *syncDb = nullptr; try { syncDb = m_syncDbs.at(repoEntry.name()).get(); cerr << shchar << "Applying config for database [" << syncDb->name() << "]" << endl; if(!repoEntry.dataBasePath().isEmpty()) { cerr << shchar << "Warning: Can't use data base path specified in repo index config because the repo \"" << repoEntry.name() << "\" has already been added from the Pacman config." << endl; } if(repoEntry.sigLevel()) { cerr << shchar << "Warning: Can't use sig level specified in repo index config because the repo \"" << repoEntry.name() << "\" has already been added from the Pacman config." << endl; } syncDb->setPackagesDirectory(repoEntry.packageDir()); syncDb->setSourcesDirectory(repoEntry.sourceDir()); } catch(const out_of_range &) { if(repoEntry.name().compare(QLatin1String("local"), Qt::CaseInsensitive) == 0) { cerr << shchar << "Error: Unable to add database from repo index config: The database name mustn't be \"local\" because this name is reserved for the local database." << endl; } else if(repoEntry.name().startsWith(QLatin1String("aur"), Qt::CaseInsensitive)) { cerr << shchar << "Error: Unable to add database from repo index config: The database name mustn't start with \"aur\" because this name is reserved for the Arch Linux User Repository." << endl; } else { // TODO: database path auto *db = alpm_register_syncdb(m_handle, repoEntry.name().toLocal8Bit().data(), static_cast(repoEntry.sigLevel())); auto emplaced = m_syncDbs.emplace(repoEntry.name(), make_unique(db, m_config.alpmDbPath())); if(emplaced.second) { syncDb = emplaced.first->second.get(); syncDb->setSourcesDirectory(repoEntry.sourceDir()); syncDb->setPackagesDirectory(repoEntry.packageDir()); if(m_config.isVerbose() || m_config.runServer()) { cerr << shchar << "Added database [" << repoEntry.name() << "]" << endl; } } } } if(syncDb) { syncDb->addServerUrls(repoEntry.servers()); } } // add upgrade sources for(const RepoEntry &repoEntry : m_config.repoEntries()) { try { auto &upgradeSources = m_syncDbs.at(repoEntry.name())->upgradeSources(); for(const auto &upgradeSourceName : repoEntry.upgradeSources()) { if(auto *source = repositoryByName(upgradeSourceName)) { upgradeSources << source; } else { cerr << shchar << "Warning: The specified upgrade source \"" << upgradeSourceName << "\" can not be found and will be ignored." << endl; } } } catch(const out_of_range &) { // entry should have been added before } } } /*! * \brief Initiates all ALPM data bases. * \remarks Must be called, after all relevant sync data bases have been registered (eg. via applyPacmanConfig()). */ void Manager::initAlpmDataBases(bool computeRequiredBy) { // call the init method { QList loaders; loaders.reserve(m_syncDbs.size() + 1); loaders << localDataBase()->init(); for(auto &syncDbEntry : m_syncDbs) { loaders << syncDbEntry.second->init(); } for(auto *loader : loaders) { loader->future().waitForFinished(); delete loader; } } // compute required-by and optional-for if(computeRequiredBy) { QList > futures; futures.reserve(m_syncDbs.size() + 1); futures << localDataBase()->computeRequiredBy(*this); for(auto &syncDbEntry : m_syncDbs) { futures << syncDbEntry.second->computeRequiredBy(*this); } for(auto &future : futures) { future.waitForFinished(); } } } /*! * \brief Writes the cache for all repositories where caching makes sense. */ void Manager::writeCache() { // could iterate through all repos and check isCachingUseful() but // currently its just the AUR which is needed to be cached if(userRepository()) { QFile file(config().cacheDir() % QChar('/') % userRepository()->name() % QStringLiteral(".cache")); if(file.open(QFileDevice::WriteOnly)) { QDataStream stream(&file); userRepository()->writeToCacheStream(stream); // if warnings/errors occur, these will be printed directly by writeToCacheStream() } else { cerr << shchar << "Warning: Unable to write cache file for the AUR." << endl; } } } void Manager::restoreCache() { // could iterate through all repos and check isCachingUseful() but // currently its just the AUR which is needed to be cached if(userRepository()) { QFile file(config().cacheDir() % QChar('/') % userRepository()->name() % QStringLiteral(".cache")); if(file.exists()) { if(file.open(QFileDevice::ReadOnly)) { QDataStream stream(&file); userRepository()->restoreFromCacheStream(stream); // if warnings/errors occur, these will be printed directly by restoreFromCacheStream() } else { cerr << shchar << "Warning: Unable to open cache file for the AUR." << endl; } } } } /*! * \brief Unregisters all registred sync databases. */ void Manager::unregisterSyncDataBases() { if(alpm_unregister_all_syncdbs(m_handle)) { throw runtime_error(string("Cannot unregister sync databases: ") + alpm_strerror(alpm_errno(m_handle))); } } /*! * \brief Returns a list of all sync databases. * \remarks Sync databases must be registered with parsePacmanConfig() before. */ const map > &Manager::syncDatabases() const { return m_syncDbs; // m_syncDbs has been filled when the databases were registered } /*! * \brief Returns basic information about all repositories known to the manager. * * The results include the local database ("local") and the names of * the registered sync databases. */ const QJsonArray &Manager::basicRepoInfo() const { if(m_basicRepoInfo.isEmpty()) { QMutexLocker locker(&m_basicRepoInfoMutex); if(m_basicRepoInfo.isEmpty()) { // add local data base m_basicRepoInfo << localDataBase()->basicInfo(); // add sync data bases for(const auto &syncDb : syncDatabases()) { // check if the "sync" database is actually used for syncing auto usage = syncDb.second->usage(); if((usage & ALPM_DB_USAGE_SYNC) || (usage & ALPM_DB_USAGE_INSTALL) || (usage & ALPM_DB_USAGE_UPGRADE)) { m_basicRepoInfo << syncDb.second->basicInfo(); } else { m_basicRepoInfo << syncDb.second->basicInfo(); } } // add AUR if(config().isAurEnabled()) { m_basicRepoInfo << userRepository()->basicInfo(); } } } return m_basicRepoInfo; } /*! * \brief Returns package information for the specified selection of packages. */ const QJsonArray Manager::packageInfo(const QJsonObject &pkgSelection, bool full) const { QJsonArray pkgInfos; for(auto i = pkgSelection.constBegin(), end = pkgSelection.constEnd(); i != end; ++i) { if(auto *repo = repositoryByName(i.key())) { for(const auto &entry : i.value().toArray()) { const auto entryObj = entry.toObject(); const auto pkgName = entryObj.value(QStringLiteral("name")).toString(); if(!pkgName.isEmpty()) { QJsonObject pkgInfo; if(auto *pkg = repo->packageByName(pkgName)) { pkgInfo = full ? pkg->fullInfo() : pkg->basicInfo(); } else { pkgInfo.insert(QStringLiteral("error"), QStringLiteral("na")); } pkgInfo.insert(QStringLiteral("name"), pkgName); pkgInfo.insert(QStringLiteral("repo"), repo->name()); const auto index = entryObj.value(QStringLiteral("index")); if(!index.isNull() && !index.isUndefined()) { pkgInfo.insert(QStringLiteral("index"), index); } pkgInfos << pkgInfo; } } } else { // specified repository can not be found QJsonObject errorObj; errorObj.insert(QStringLiteral("repo"), i.key()); errorObj.insert(QStringLiteral("error"), QStringLiteral("na")); pkgInfos << errorObj; } } return pkgInfos; } /*! * \brief Returns group information for the local database and all registred sync databases. */ const QJsonArray &Manager::groupInfo() const { if(m_groupInfo.empty()) { QMutexLocker locker(&m_groupInfoMutex); if(m_groupInfo.empty()) { m_groupInfo << localDataBase()->groupInfo(); for(const auto &db : m_syncDbs) { m_groupInfo << db.second->groupInfo(); } } } return m_groupInfo; } /*! * \brief Returns the ALPM database with the specified name. */ const AlpmDatabase *Manager::databaseByName(const QString &dbName) const { if(dbName.compare(QLatin1String("local"), Qt::CaseInsensitive) == 0) { return localDataBase(); } else { try { return m_syncDbs.at(dbName).get(); } catch(const out_of_range &) { return nullptr; } } } /*! * \brief Returns the ALPM database with the specified name. */ AlpmDatabase *Manager::databaseByName(const QString &dbName) { if(dbName.compare(QLatin1String("local"), Qt::CaseInsensitive) == 0) { return localDataBase(); } else { try { return m_syncDbs.at(dbName).get(); } catch(const out_of_range &) { return nullptr; } } } /*! * \brief Returns the package source with the specified name. */ const Repository *Manager::repositoryByName(const QString &name) const { if(name.compare(QLatin1String("local"), Qt::CaseInsensitive) == 0) { return localDataBase(); } else if(config().isAurEnabled() && (name.compare(QLatin1String("aur"), Qt::CaseInsensitive) == 0)) { return userRepository(); } else { try { return m_syncDbs.at(name).get(); } catch(const out_of_range &) { return nullptr; } } } /*! * \brief Returns the package source with the specified name. */ Repository *Manager::repositoryByName(const QString &name) { if(name.compare(QLatin1String("local"), Qt::CaseInsensitive) == 0) { return localDataBase(); } else if(config().isAurEnabled() && (name.compare(QLatin1String("aur"), Qt::CaseInsensitive) == 0)) { return userRepository(); } else { try { return m_syncDbs.at(name).get(); } catch(const out_of_range &) { return nullptr; } } } /*! * \brief Returns a list of all repositories excluding the local database. */ QList Manager::repositories() const { QList repos; repos.reserve(m_syncDbs.size() + 1); for(const auto &dbEntry : m_syncDbs) { repos << dbEntry.second.get(); } repos << m_userRepo.get(); return repos; } /*! * \brief Returns a list of all repositories excluding the local database. */ QList Manager::repositories() { QList repos; repos.reserve(m_syncDbs.size() + 1); for(auto &dbEntry : m_syncDbs) { repos << dbEntry.second.get(); } repos << m_userRepo.get(); return repos; } /*! * \brief Checks the specified database for upgrades. * * Appropriate upgrade sources will be determined automatically; does not check the AUR. */ const UpgradeLookupResults Manager::checkForUpgrades(AlpmDatabase *db) const { UpgradeLookupResults results; db->checkForUpgrades(results); return results; } }