#include "manager.h" #include "database.h" #include "utilities.h" #include "list.h" #include "config.h" #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace IoUtilities; using namespace ConversionUtilities; namespace PackageManagement { constexpr int defaultSigLevel = ALPM_SIG_PACKAGE | ALPM_SIG_PACKAGE_OPTIONAL | ALPM_SIG_DATABASE | ALPM_SIG_DATABASE_OPTIONAL; /*! * \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_sigLevel(defaultSigLevel), m_localFileSigLevel(ALPM_SIG_USE_DEFAULT), m_aur(m_networkAccessManager) { 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)); } } /*! * \brief Releases the associated ALPM handle. */ Manager::~Manager() { alpm_release(m_handle); } /*! * \brief Returns the package with the specified name from the specified database. */ AlpmPackage Manager::packageFromSyncDataBase(const QString &dbName, const QString &pkgName) const { try { if(dbName == QLatin1String("local")) { return localDataBase().packages().at(pkgName); } else { return syncDataBases().at(dbName).packages().at(pkgName); } } catch(out_of_range &) { return AlpmPackage(); } } /*! * \brief Creates a new package instance for the specified package file. * * Verifies the integrity of the file if the option is set. */ AlpmOwnershipPackage 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 AlpmOwnershipPackage(pkg); } } /*! * \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() + ": " + 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 << "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 << "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 << "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 << "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 << "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 << "Added database [" << scope.first << "]" << endl; } } else { if(m_config.isVerbose() || m_config.runServer()) { cerr << "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 << "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 << "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 << "Added server: " << url << endl; } } } catch (const out_of_range &) { cerr << "Warning: Included file \"" << path << "\" has no values." << endl; } } // add sync db to internal map if(usage & ALPM_DB_USAGE_UPGRADE) { // -> db is used to upgrade local database localDataBase().upgradeSources() << dbName; } m_syncDbs.emplace(dbName, AlpmDataBase(db, QStringLiteral("%1/sync/%2").arg(m_config.alpmDbPath(), dbName))); } else { cerr << "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() { for(const RepoEntry &repoEntry : m_config.repoEntries()) { AlpmDataBase *syncDb; try { syncDb = &m_syncDbs.at(repoEntry.name()); cerr << "Applying config for database [" << syncDb->name() << "]" << endl; if(!repoEntry.dataBasePath().isEmpty()) { cerr << "Warning: Can't use data base path specified in repo index config because the repo \"" << repoEntry.name().toLocal8Bit().data() << "\" has already been added from the Pacman config." << endl; } if(repoEntry.sigLevel()) { cerr << "Warning: Can't use sig level path specified in repo index config because the repo \"" << repoEntry.name().toLocal8Bit().data() << "\" 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 << "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 << "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 { auto *db = alpm_register_syncdb(m_handle, repoEntry.name().toLocal8Bit().data(), static_cast(repoEntry.sigLevel())); auto emplaced = m_syncDbs.emplace(repoEntry.name(), AlpmDataBase(db, repoEntry.dataBasePath(), repoEntry.sourceDir(), repoEntry.packageDir())); if(emplaced.second) { syncDb = &emplaced.first->second; if(m_config.isVerbose() || m_config.runServer()) { cerr << "Added database [" << repoEntry.name().toLocal8Bit().data() << "]" << endl; } } } } if(syncDb) { syncDb->addServerUrls(repoEntry.servers()); syncDb->upgradeSources() << repoEntry.upgradeSources(); } } } /*! * \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 the local data base. */ const AlpmDataBase &Manager::localDataBase() const { if(!m_localDb) { QMutexLocker locker(&m_localDbMutex); if(!m_localDb) { m_localDb = alpm_get_localdb(m_handle); } } return m_localDb; } /*! * \brief Returns the local data base. */ AlpmDataBase &Manager::localDataBase() { if(!m_localDb) { m_localDb = alpm_get_localdb(m_handle); } return m_localDb; } /*! * \brief Returns a list of all sync databases. * \remarks Sync databases must be registered with parsePacmanConfig() before. */ const std::map &Manager::syncDataBases() const { return m_syncDbs; // m_syncDbs has been filled when the databases were registered } /*! * \brief Returns basic information about the specified repository. */ QJsonObject Manager::basicRepoInfo(AlpmDataBase db, const QString &name, const QString &desc) const { QJsonObject repoInfo; repoInfo.insert(QStringLiteral("name"), name); repoInfo.insert(QStringLiteral("desc"), desc); repoInfo.insert(QStringLiteral("servers"), db.serverUrls()); repoInfo.insert(QStringLiteral("usage"), Utilities::usageStrings(db.usage())); repoInfo.insert(QStringLiteral("sigLevel"), Utilities::sigLevelStrings(db.sigLevel())); repoInfo.insert(QStringLiteral("upgradeSources"), QJsonArray::fromStringList(db.upgradeSources())); repoInfo.insert(QStringLiteral("packages"), db.packageNameJsonArray()); return repoInfo; } /*! * \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()) { m_basicRepoInfo << basicRepoInfo(localDataBase(), QStringLiteral("local"), QStringLiteral("The local database.")); auto const &syncDbs = syncDataBases(); for(const auto &syncDb : syncDbs) { // 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 << basicRepoInfo(syncDb.second, syncDb.first, QStringLiteral("The sync database »%1«.").arg(syncDb.first)); } else { m_basicRepoInfo << basicRepoInfo(syncDb.second, syncDb.first, QStringLiteral("The database »%1«.").arg(syncDb.first)); } } } } return m_basicRepoInfo; } /*! * \brief Returns package information for the specified selection of packages. */ const QJsonArray Manager::packageInfo(const QJsonArray &pkgSelection, bool full) const { QJsonArray pkgInfos; for(const auto &pkgSelJsonVal : pkgSelection) { QJsonObject pkgSel = pkgSelJsonVal.toObject(); if(!pkgSel.isEmpty()) { QString repoName = pkgSel.value(QStringLiteral("repo")).toString(); QString pkgName = pkgSel.value(QStringLiteral("name")).toString(); AlpmPackage pkg; QJsonObject pkgInfo; if(!repoName.isEmpty() && !pkgName.isEmpty() && (pkg = packageFromSyncDataBase(repoName, pkgName))) { pkgInfo = full ? pkg.fullInfo() : pkg.basicInfo(); } else { pkgInfo.insert(QStringLiteral("error"), QStringLiteral("na")); } pkgInfo.insert(QStringLiteral("name"), pkgName); pkgInfo.insert(QStringLiteral("repo"), repoName); pkgInfo.insert(QStringLiteral("index"), pkgSel.value(QStringLiteral("index"))); pkgInfos << pkgInfo; } } 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. * \throws Throws std::out_of_range if the specified database is unknown to the manager. */ const AlpmDataBase &Manager::dataBaseByName(const QString &dbName) const { if(dbName == QLatin1String("local")) { return localDataBase(); } else { return m_syncDbs.at(dbName); } } /*! * \brief Returns the ALPM database with the specified name. * \throws Throws std::out_of_range if the specified database is unknown to the manager. */ AlpmDataBase &Manager::dataBaseByName(const QString &dbName) { if(dbName == QLatin1String("local")) { return localDataBase(); } else { return m_syncDbs.at(dbName); } } /*! * \brief Checks for upgrades availabel to the specified database. * * The \a request must have the following values: * - db: Specifies the name of the database to check for upgrades. * - syncdbs: Array with the names of the databases used as upgrade sources. * If not present, appropriate upgrade sources will be determined automatically. */ void Manager::invokeUpgradeLookup(const QJsonObject &request, UpdateLookupCallback callback) const { new UpdateLookup(*this, request, callback); // this object will delete itself } /*! * \brief Checks the specified database for upgrades. * * Appropriate upgrade sources will be determined automatically; does not check the AUR. */ const UpdateLookupResults Manager::checkForUpgrades(const AlpmDataBase &db) const { UpdateLookupResults results; QList syncDbSel; for(const auto &syncDbName : db.upgradeSources()) { try { syncDbSel << &(syncDataBases().at(syncDbName)); } catch(out_of_range &) { ; } } db.checkForUpgrades(syncDbSel, results); return results; } }