#include "./repository.h" #include "./upgradelookup.h" #include "./utilities.h" #include "./config.h" #include #include #include #include #include #include using namespace std; using namespace ChronoUtilities; namespace RepoIndex { /*! * \brief Constructs a new reply for a single network reply. */ Reply::Reply(QNetworkReply *networkReply) : m_remainingReplies(1) { networkReply->setParent(this); connect(networkReply, &QNetworkReply::finished, this, &Reply::replyFinished); m_networkReplies.reserve(1); m_networkReplies << networkReply; } /*! * \brief Constructs a new reply for multiple network replies. */ Reply::Reply(const QList networkReplies) : m_networkReplies(networkReplies), m_remainingReplies(networkReplies.size()) { for(auto *networkReply : networkReplies) { networkReply->setParent(this); connect(networkReply, &QNetworkReply::finished, this, &Reply::replyFinished); } } /*! * \brief Called when a network reply has finished. */ void Reply::replyFinished() { assert(m_remainingReplies); processData(static_cast(sender())); if(!--m_remainingReplies) { emit resultsAvailable(); } } /*! * \fn Repository::type() * \brief Returns the type of the package source. */ /*! * \brief Returns a list of all package names. */ const QStringList RepoIndex::Repository::packageNames() const { QStringList names; names.reserve(m_packages.size()); for(const auto &entry : m_packages) { names << entry.first; } return names; } /*! * \brief Initializes the repository. * \remarks * - Does not restore cache. For restoring cache see restoreFromCacheStream(). * - Performs asynchronously and hence returns immidiately. Returns a PackageLoader * object which QFuture can be used to wait for initializing to finish. * - Might return nullptr if initialization is tivial. */ PackageLoader *Repository::init() { return nullptr; } /*! * \brief Requests suggestions for the specified search phrase. * \returns Returns a reply object used for the request. The reply must be destroyed by the caller * using destroyLater() after resultsAvailable() has been emitted. */ SuggestionsReply *Repository::requestSuggestions(const QString &) { return nullptr; } /*! * \class Repository * \brief The Repository class represents a repository (binary repositories as well as source-only repos). */ /*! * \brief Constructs a new repository (protected since this is a pure virtual class). */ Repository::Repository(const QString &name, uint32 index, QObject *parent) : QObject(parent), m_index(index), m_name(name), m_maxPackageAge(TimeSpan::infinity()), m_usage(static_cast(0)), m_sigLevel(static_cast(ALPM_SIGSTATUS_INVALID)), m_lock(QReadWriteLock::Recursive) {} /*! * \brief Destroys the repository. */ Repository::~Repository() {} /*! * \brief Returns whether explicit requests are required to get the specified information * about the package of this repository. * * AlpmDataBase instances load all available packages in the cache * at the beginning and hence do not require explicit requests for package names, version, * description, dependencies and most other information. However make dependencies are not available at all. * * UserRepository instances on the other hand have an empty package * cache at the beginning so packages must be requested explicitely * using the requestPackageInfo() method. */ PackageDetailAvailability Repository::requestsRequired(PackageDetail ) const { return PackageDetailAvailability::Never; } /*! * \brief Requests package information for the specified package. * \returns Returns a reply object used for the request. The reply must be destroyed by the caller * using destroyLater() after resultsAvailable() has been emitted. * \remarks * If \a forceUpdate is true, package information which has already been retrieved * and is still cached is requested again. Otherwise these packages will not be * requested again. If it turns out, that all packages are already cached, nullptr * is returned in this case. */ PackageReply *Repository::requestPackageInfo(const QStringList &, bool ) { return nullptr; } /*! * \brief Requests full package information for the specified package. * \returns Returns a reply object used for the request. The reply must be destroyed by the caller * using destroyLater() after resultsAvailable() has been emitted. * \remarks * If \a forceUpdate is true, package information which has already been retrieved * and is still cached is requested again. Otherwise these packages will not be * requested again. If it turns out, that all packages are already cached, nullptr * is returned in this case. */ PackageReply *Repository::requestFullPackageInfo(const QStringList &, bool ) { return nullptr; } /*! * \brief Returns the first package providing the specified \a dependency. * \remarks Returns nullptr if no packages provides the \a dependency. */ const Package *Repository::packageProviding(const Dependency &dependency) const { for(const auto &entry : m_packages) { if(entry.second->matches(dependency)) { // check whether package matches "directly" return entry.second.get(); } } return nullptr; } /*! * \brief Returns the first package providing the specified \a dependency. * \remarks Returns nullptr if no packages provides the \a dependency. */ Package *Repository::packageProviding(const Dependency &dependency) { for(auto &entry : m_packages) { if(entry.second->matches(dependency)) { // check whether package matches "directly" return entry.second.get(); } } return nullptr; } /*! * \brief Returns all packages providing the specified \a dependency. */ QList Repository::packagesProviding(const Dependency &dependency) const { QList res; for(const auto &entry : m_packages) { if(entry.second->matches(dependency)) { res << entry.second.get(); } } return res; } /*! * \brief Returns all packages matching the specified predicate. */ QList Repository::packageByFilter(std::function pred) { QList packages; for(const auto &entry : m_packages) { if(pred(entry.second.get())) { packages << entry.second.get(); } } return packages; } /*! * \cond */ class ComputeRequired { public: ComputeRequired(Manager &manager, bool forceUpdate) : m_manager(manager), m_forceUpdate(forceUpdate) {} void operator () (const pair > &packageEntry) { if(m_forceUpdate || !packageEntry.second->isRequiredByComputed()) { packageEntry.second->computeRequiredBy(m_manager); } } private: Manager &m_manager; bool m_forceUpdate; }; /*! * \endcond */ /*! * \brief Computes required-by and optional-for for all packages. * \remarks Computition is done async. */ QFuture Repository::computeRequiredBy(Manager &manager, bool forceUpdate) { return QtConcurrent::map(m_packages, ComputeRequired(manager, forceUpdate)); } /*! * \brief Returns suggestions for the specified \a term. */ QJsonObject Repository::suggestions(const QString &term) const { QJsonArray suggestions; // size_t remainingSuggestions = 20; // for(auto i = packages().lower_bound(term), end = packages().cend(); i != end && remainingSuggestions; ++i, --remainingSuggestions) { // if(i->first.startsWith(term, Qt::CaseInsensitive)) { // suggestions << i->first; // } else { // break; // } // } for(const auto &pkgEntry : packages()) { if(pkgEntry.first.contains(term, Qt::CaseInsensitive)) { suggestions << pkgEntry.first; } } QJsonObject res; res.insert(QStringLiteral("repo"), name()); res.insert(QStringLiteral("res"), suggestions); return res; } QJsonArray Repository::upgradeSourcesJsonArray() const { QJsonArray sources; for(const auto *source : upgradeSources()) { sources << source->name(); } return sources; } void Repository::checkForUpgrades(UpgradeLookupResults &results, const QList &upgradeSources) const { if(upgradeSources.isEmpty()) { results.noSources = true; } else { for(const auto &pkgEntry : packages()) { bool orphaned = true; for(const auto *src : upgradeSources) { if(const auto *syncPkg = src->packageByName(pkgEntry.first)) { switch(pkgEntry.second->compareVersion(syncPkg)) { case PackageVersionComparsion::Equal: break; // ignore equal packages case PackageVersionComparsion::SoftwareUpgrade: results.newVersions << UpgradeResult(syncPkg, pkgEntry.second->version()); break; case PackageVersionComparsion::PackageUpgradeOnly: results.newReleases << UpgradeResult(syncPkg, pkgEntry.second->version()); break; case PackageVersionComparsion::NewerThenSyncVersion: results.downgrades << UpgradeResult(syncPkg, pkgEntry.second->version()); } orphaned = false; } } if(orphaned) { results.orphaned << pkgEntry.second.get(); } } } } /*! * \brief Returns all package names as JSON array. */ QJsonArray Repository::packageNamesJsonArray() const { QJsonArray names; for(const auto &entry : m_packages) { names << entry.first; } return names; } /*! * \brief Returns an object with the package names of the repository as keys (and empty objects as value). */ QJsonObject Repository::packagesObjectSkeleton() const { QJsonObject skel; for(const auto &entry : m_packages) { skel.insert(entry.first, QJsonValue(QJsonValue::Object)); } return skel; } /*! * \cond */ inline void put(QJsonObject &obj, const QString &key, const QJsonValue &value) { if(!value.isNull()) { obj.insert(key, value); } } inline void put(QJsonObject &obj, const QString &key, const QStringList &values) { if(!values.isEmpty()) { put(obj, key, QJsonArray::fromStringList(values)); } } /*! * \endcond */ /*! * \brief Returns basic information about the repository. */ QJsonObject Repository::basicInfo(bool includeName) const { QJsonObject info; if(includeName) { put(info, QStringLiteral("name"), name()); } if(index() != invalidIndex) { info.insert(QStringLiteral("index"), static_cast(index())); } put(info, QStringLiteral("desc"), description()); put(info, QStringLiteral("servers"), serverUrls()); put(info, QStringLiteral("usage"), Utilities::usageStrings(usage())); put(info, QStringLiteral("sigLevel"), Utilities::sigLevelStrings(sigLevel())); put(info, QStringLiteral("upgradeSources"), upgradeSourcesJsonArray()); put(info, QStringLiteral("packages"), packagesObjectSkeleton()); if(requestsRequired(PackageDetail::Basics) == PackageDetailAvailability::Immediately) { info.insert(QStringLiteral("packageCount"), static_cast(m_packages.size())); } put(info, QStringLiteral("srcOnly"), isSourceOnly()); put(info, QStringLiteral("pkgOnly"), isPackageOnly()); return info; } /*! * \brief Returns group information as JSON object. */ QJsonObject Repository::groupInfo() const { QJsonObject info; put(info, QStringLiteral("repo"), name()); QJsonArray groupsArray; for(const auto &groupEntry : groups()) { QJsonObject info; put(info, QStringLiteral("name"), groupEntry.first); QJsonArray pkgNames; for(const auto *pkg : groupEntry.second) { pkgNames << pkg->name(); } put(info, QStringLiteral("pkgs"), pkgNames); groupsArray << info; } info.insert(QStringLiteral("groups"), groupsArray); return info; } /*! * \brief Writes the repository information to the specified cache stream. */ void Repository::writeToCacheStream(QDataStream &out) { out << static_cast(0x7265706F); // magic number out << static_cast(0x0); // version out << static_cast(type()); out << static_cast(m_packages.size()); for(const auto &pkg : m_packages) { pkg.second->writeToCacheStream(out); } // write specific header auto headerStart = out.device()->pos(); out.device()->seek(headerStart + 4); writeSpecificCacheHeader(out); auto headerEnd = out.device()->pos(); out.device()->seek(headerStart); out << static_cast(headerEnd - headerStart - 4); out.device()->seek(headerEnd); // no extended header out << static_cast(0x0); } /*! * \brief Restores the repository information from cache. */ void Repository::restoreFromCacheStream(QDataStream &in, bool skipOutdated) { quint32 magic; in >> magic; if(magic == 0x7265706F) { // read version quint32 version; in >> version; // read type quint32 denotedType; in >> denotedType; if(denotedType == static_cast(type())) { // read packages quint32 packageCount; in >> packageCount; bool good = true; const auto now = DateTime::now(); for(quint32 i = 0; i < packageCount && good && in.status() == QDataStream::Ok; ++i) { if(auto package = emptyPackage()) { package->restoreFromCacheStream(in); if(!package->name().isEmpty()) { if(!skipOutdated || !((now - package->timeStamp()) > maxPackageAge())) { m_packages[package->name()] = move(package); } else { cerr << shchar << "Info: Cache entry for package \"" << package->name().toLocal8Bit().data() << "\" is outdated and won't be restored." << endl; } } else { good = false; } } else { good = false; } } if(in.status() == QDataStream::Ok) { // specific header quint32 headerSize; in >> headerSize; quint64 headerEnd = in.device()->pos() + headerSize; restoreSpecificCacheHeader(in); in.device()->seek(headerEnd); if(in.status() == QDataStream::Ok) { // skip extended header in >> headerSize; quint64 headerEnd = in.device()->pos() + headerSize; in.device()->seek(headerEnd); } else { cerr << shchar << "Failed to restore cache for repository \"" << m_name.toLocal8Bit().data() << "\": unable to parse specific cache header" << endl; } } else { cerr << shchar << "Failed to restore cache for repository \"" << m_name.toLocal8Bit().data() << "\": unable to parse packages" << endl; } } else { cerr << shchar << "Failed to restore cache for repository \"" << m_name.toLocal8Bit().data() << "\": denoted type does not match expected type" << endl; } } else { cerr << shchar << "Failed to restore cache for repository \"" << m_name.toLocal8Bit().data() << "\": bad magic number" << endl; } } /*! * \brief Writes the repo-type-specific cache header. */ void Repository::writeSpecificCacheHeader(QDataStream &out) { Q_UNUSED(out) } /*! * \brief Returns an new, empty package. * \remarks Used to when restoring packages from cache. */ unique_ptr Repository::emptyPackage() { return unique_ptr(); } /*! * \brief Restores the repo-type-specific cache header. */ void Repository::restoreSpecificCacheHeader(QDataStream &in) { Q_UNUSED(in) } /*! * \brief Cleans the repository from outdated packages. * \remarks Does nothing if maxPackageAge() is infinity (which is the default). */ void Repository::cleanOutdatedPackages() { if(maxPackageAge().isInfinity()) { return; } auto now = DateTime::now(); for(auto i = m_packages.begin(); i != m_packages.end(); ) { const Package &pkg = *i->second; if((now - pkg.timeStamp()) > maxPackageAge()) { i = m_packages.erase(i); } else { ++i; } } } /*! * \brief Adds packages parsed from the specified \a srcInfo. * \returns Returns the added packages. */ QList Repository::addPackagesFromSrcInfo(const QByteArray &srcInfo) { enum { FieldName, EquationSign, Pad, FieldValue, Comment } state = FieldName; QString currentFieldName; QString currentFieldValue; QString packageBase; QList > baseInfo; QList > packageInfo; QList packages; Package *currentPackage = nullptr; for(const char c : srcInfo) { switch(state) { case FieldName: switch(c) { case '#': // discard truncated line currentFieldName.clear(); state = Comment; case ' ': // field name complete, expect equation sign if(!currentFieldName.isEmpty()) { state = EquationSign; } break; case '\n': case '\r': case '\t': // discard truncated line currentFieldName.clear(); break; default: currentFieldName.append(c); } break; case EquationSign: switch(c) { case '=': state = Pad; break; case '\n': case '\r': case '\t': // unexpected new line -> discard truncated line currentFieldName.clear(); break; default: ; // ignore unexpected characters } break; case Pad: switch(c) { case ' ': state = FieldValue; break; case '\n': case '\r': case '\t': // unexpected new line -> discard truncated line currentFieldName.clear(); break; default: ; // ignore unexpected characters } break; case FieldValue: switch(c) { case '\n': case '\r': state = FieldName; if(currentFieldName == QLatin1String("pkgbase")) { // pkgbase packageBase = currentFieldValue; } else if(currentFieldName == QLatin1String("pkgname")) { // next package if(packageBase.isEmpty()) { // TODO: handle error - pkgbase must be present } else { if(currentPackage) { currentPackage->putInfo(baseInfo, packageInfo, true); packages << currentPackage; } auto &pkg = m_packages[currentFieldValue]; if(!pkg) { pkg = emptyPackage(); } currentPackage = pkg.get(); packageInfo.clear(); } } if(currentPackage) { packageInfo << QPair(currentFieldName, currentFieldValue); } else { baseInfo << QPair(currentFieldName, currentFieldValue); } currentFieldName.clear(); currentFieldValue.clear(); break; default: currentFieldValue.append(c); } break; case Comment: switch(c) { case '\n': case '\r': case '\t': state = FieldName; break; default: ; // ignore outcommented characters } break; } } if(currentPackage) { currentPackage->putInfo(baseInfo, packageInfo, true); packages << currentPackage; } return packages; } } // namespace PackageManagement