612 lines
19 KiB
C++
612 lines
19 KiB
C++
#include "./repository.h"
|
|
#include "./upgradelookup.h"
|
|
#include "./utilities.h"
|
|
#include "./config.h"
|
|
|
|
#include <QJsonObject>
|
|
#include <QNetworkReply>
|
|
#include <QDataStream>
|
|
#include <QtConcurrent>
|
|
|
|
#include <iostream>
|
|
|
|
using namespace std;
|
|
|
|
namespace RepoIndex {
|
|
|
|
/*!
|
|
* \brief Constructs a new reply for a single network reply.
|
|
*/
|
|
Reply::Reply(QNetworkReply *networkReply)
|
|
{
|
|
networkReply->setParent(this);
|
|
connect(networkReply, &QNetworkReply::finished, this, &Reply::processData);
|
|
m_networkReplies.reserve(1);
|
|
m_networkReplies << networkReply;
|
|
}
|
|
|
|
/*!
|
|
* \brief Constructs a new reply for multiple network replies.
|
|
*/
|
|
Reply::Reply(const QList<QNetworkReply *> networkReplies) :
|
|
m_networkReplies(networkReplies)
|
|
{
|
|
for(auto *networkReply : networkReplies) {
|
|
networkReply->setParent(this);
|
|
connect(networkReply, &QNetworkReply::finished, this, &Reply::processData);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \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 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 &) const
|
|
{
|
|
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_usage(static_cast<alpm_db_usage_t>(0)),
|
|
m_sigLevel(static_cast<alpm_siglevel_t>(ALPM_SIGSTATUS_INVALID))
|
|
{}
|
|
|
|
/*!
|
|
* \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 ) const
|
|
{
|
|
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 ) const
|
|
{
|
|
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();
|
|
} else {
|
|
// check whether at least one of the provides matches
|
|
for(const auto &provide : entry.second->provides()) {
|
|
if(Package::matches(provide.name, provide.version, dependency)) {
|
|
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();
|
|
} else {
|
|
// check whether at least one of the provides matches
|
|
for(auto &provide : entry.second->provides()) {
|
|
if(Package::matches(provide.name, provide.version, dependency)) {
|
|
return entry.second.get();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns all packages providing the specified \a dependency.
|
|
*/
|
|
QList<const Package *> Repository::packagesProviding(const Dependency &dependency) const
|
|
{
|
|
QList<const Package *> res;
|
|
for(const auto &entry : m_packages) {
|
|
if(entry.second->matches(dependency)) {
|
|
// check whether package matches "directly"
|
|
res << entry.second.get();
|
|
} else {
|
|
// check whether at least one of the provides matches
|
|
for(const auto &provide : entry.second->provides()) {
|
|
if(Package::matches(provide.name, provide.version, dependency)) {
|
|
res << entry.second.get();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns all packages matching the specified predicate.
|
|
*/
|
|
QList<Package *> Repository::packageByFilter(std::function<bool (const Package *)> pred)
|
|
{
|
|
QList<Package *> 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<const QString, unique_ptr<Package> > &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<void> 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;
|
|
}
|
|
}
|
|
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<const Repository *> &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<int>(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<qint64>(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<quint32>(0x7265706F); // magic number
|
|
out << static_cast<quint32>(0x0); // version
|
|
out << static_cast<quint32>(type());
|
|
out << static_cast<quint32>(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<quint32>(headerEnd - headerStart - 4);
|
|
out.device()->seek(headerEnd);
|
|
// no extended header
|
|
out << static_cast<quint32>(0x0);
|
|
}
|
|
|
|
/*!
|
|
* \brief Restores the repository information from cache.
|
|
*/
|
|
void Repository::restoreFromCacheStream(QDataStream &in)
|
|
{
|
|
quint32 magic;
|
|
in >> magic;
|
|
if(magic == 0x7265706F) {
|
|
// read version
|
|
quint32 version;
|
|
in >> version;
|
|
// read type
|
|
quint32 denotedType;
|
|
in >> denotedType;
|
|
if(denotedType == static_cast<quint32>(type())) {
|
|
// read packages
|
|
quint32 packageCount;
|
|
in >> packageCount;
|
|
bool good = true;
|
|
for(quint32 i = 0; i < packageCount && good && in.status() == QDataStream::Ok; ++i) {
|
|
if(unique_ptr<Package> package = emptyPackage()) {
|
|
package->restoreFromCacheStream(in);
|
|
if(!package->name().isEmpty()) {
|
|
m_packages[package->name()] = move(package);
|
|
} 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<Package> Repository::emptyPackage()
|
|
{
|
|
return unique_ptr<Package>();
|
|
}
|
|
|
|
/*!
|
|
* \brief Restores the repo-type-specific cache header.
|
|
*/
|
|
void Repository::restoreSpecificCacheHeader(QDataStream &in)
|
|
{
|
|
Q_UNUSED(in)
|
|
}
|
|
|
|
/*!
|
|
* \brief Adds a package parsed from the specified \a srcInfo.
|
|
*/
|
|
void Repository::addPackagesFromSrcInfo(const QByteArray &srcInfo)
|
|
{
|
|
enum {
|
|
FieldName,
|
|
EquationSign,
|
|
Pad,
|
|
FieldValue
|
|
} state = FieldName;
|
|
QString currentFieldName;
|
|
QString currentFieldValue;
|
|
QString packageBase;
|
|
QList<QPair<QString, QString> > baseInfo;
|
|
QList<QPair<QString, QString> > packageInfo;
|
|
Package *currentPackage = nullptr;
|
|
for(char c : srcInfo) {
|
|
switch(state) {
|
|
case FieldName:
|
|
switch(c) {
|
|
case ' ':
|
|
if(!currentFieldName.isEmpty()) {
|
|
state = EquationSign;
|
|
}
|
|
break;
|
|
case '\n': case '\r':
|
|
if(!currentFieldName.isEmpty()) {
|
|
// TODO: handle error - field name contains newline character
|
|
}
|
|
break;
|
|
default:
|
|
currentFieldName.append(c);
|
|
}
|
|
break;
|
|
case EquationSign:
|
|
switch(c) {
|
|
case '=':
|
|
state = Pad;
|
|
break;
|
|
default:
|
|
;// TODO: handle error - no equation sign after pad
|
|
}
|
|
break;
|
|
case Pad:
|
|
switch(c) {
|
|
case ' ':
|
|
state = FieldValue;
|
|
break;
|
|
default:
|
|
;// TODO: handle error - no pad after equation sign
|
|
}
|
|
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);
|
|
}
|
|
auto &pkg = m_packages[currentFieldValue];
|
|
if(!pkg) {
|
|
pkg = emptyPackage();
|
|
}
|
|
currentPackage = pkg.get();
|
|
packageInfo.clear();
|
|
}
|
|
}
|
|
if(currentPackage) {
|
|
packageInfo << QPair<QString, QString>(currentFieldName, currentFieldValue);
|
|
} else {
|
|
baseInfo << QPair<QString, QString>(currentFieldName, currentFieldValue);
|
|
}
|
|
currentFieldName.clear();
|
|
currentFieldValue.clear();
|
|
break;
|
|
default:
|
|
currentFieldValue.append(c);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if(currentPackage) {
|
|
currentPackage->putInfo(baseInfo, packageInfo);
|
|
}
|
|
}
|
|
|
|
|
|
} // namespace PackageManagement
|