File indexing completed on 2024-12-01 09:55:03

0001 /*
0002     SPDX-FileCopyrightText: 2007 Aaron Seigo <aseigo@kde.org>
0003     SPDX-FileCopyrightText: 2010 Marco Martin <notmart@gmail.com>
0004     SPDX-FileCopyrightText: 2010 Kevin Ottens <ervin@kde.org>
0005     SPDX-FileCopyrightText: 2009 Rob Scheepmaker
0006 
0007     SPDX-License-Identifier: LGPL-2.0-or-later
0008 */
0009 
0010 #include "package.h"
0011 
0012 #include <QResource>
0013 #include <qtemporarydir.h>
0014 
0015 #include "kpackage_debug.h"
0016 #include <KArchive>
0017 #include <KLocalizedString>
0018 #include <KTar>
0019 #include <kzip.h>
0020 
0021 #include "config-package.h"
0022 
0023 #include <QMimeDatabase>
0024 #include <QStandardPaths>
0025 
0026 #include "packageloader.h"
0027 #include "packagestructure.h"
0028 #include "private/package_p.h"
0029 // #include "private/packages_p.h"
0030 #include "private/packagejob_p.h"
0031 #include "private/packageloader_p.h"
0032 
0033 namespace KPackage
0034 {
0035 Package::Package(PackageStructure *structure)
0036     : d(new PackagePrivate())
0037 {
0038     d->structure = structure;
0039 
0040     if (d->structure) {
0041         d->structure.data()->initPackage(this);
0042         auto desc = i18n("Desktop file that describes this package.");
0043         addFileDefinition("metadata", QStringLiteral("metadata.json"), desc);
0044         addFileDefinition("metadata", QStringLiteral("metadata.desktop"), desc);
0045     }
0046 }
0047 
0048 Package::Package(const Package &other)
0049     : d(other.d)
0050 {
0051 }
0052 
0053 Package::~Package()
0054 {
0055     // guard against deletion on application shutdown
0056     if (PackageDeletionNotifier::self()) {
0057         Q_EMIT PackageDeletionNotifier::self()->packageDeleted(this);
0058     }
0059 }
0060 
0061 Package &Package::operator=(const Package &rhs)
0062 {
0063     if (&rhs != this) {
0064         d = rhs.d;
0065     }
0066 
0067     return *this;
0068 }
0069 
0070 bool Package::hasValidStructure() const
0071 {
0072     return d->structure;
0073 }
0074 
0075 bool Package::isValid() const
0076 {
0077     if (!d->structure) {
0078         return false;
0079     }
0080 
0081     // Minimal packages with no metadata *are* supposed to be possible
0082     // so if !metadata().isValid() go ahead
0083     if (metadata().isValid() && metadata().value(QStringLiteral("isHidden"), QStringLiteral("false")) == QLatin1String("true")) {
0084         return false;
0085     }
0086 
0087     if (d->checkedValid) {
0088         return d->valid;
0089     }
0090 
0091     const QString rootPath = d->tempRoot.isEmpty() ? d->path : d->tempRoot;
0092     if (rootPath.isEmpty()) {
0093         return false;
0094     }
0095 
0096     d->valid = true;
0097 
0098     // search for the file in all prefixes and in all possible paths for each prefix
0099     // even if it's a big nested loop, usually there is one prefix and one location
0100     // so shouldn't cause too much disk access
0101     QHashIterator<QByteArray, ContentStructure> it(d->contents);
0102 
0103     while (it.hasNext()) {
0104         it.next();
0105         if (!it.value().required) {
0106             continue;
0107         }
0108 
0109         const QString foundPath = filePath(it.key(), {});
0110         if (foundPath.isEmpty()) {
0111             // qCWarning(KPACKAGE_LOG) << "Could not find required" << (it.value().directory ? "directory" : "file") << it.key() << "for package" << path() <<
0112             // "should be" << it.value().paths;
0113             d->valid = false;
0114             break;
0115         }
0116     }
0117 
0118     return d->valid;
0119 }
0120 
0121 #if KPACKAGE_BUILD_DEPRECATED_SINCE(5, 106)
0122 QString Package::name(const QByteArray &key) const
0123 {
0124     QHash<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key);
0125     if (it == d->contents.constEnd()) {
0126         return QString();
0127     }
0128 
0129     return it.value().name;
0130 }
0131 #endif
0132 
0133 bool Package::isRequired(const QByteArray &key) const
0134 {
0135     QHash<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key);
0136     if (it == d->contents.constEnd()) {
0137         return false;
0138     }
0139 
0140     return it.value().required;
0141 }
0142 
0143 QStringList Package::mimeTypes(const QByteArray &key) const
0144 {
0145     QHash<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key);
0146     if (it == d->contents.constEnd()) {
0147         return QStringList();
0148     }
0149 
0150     if (it.value().mimeTypes.isEmpty()) {
0151         return d->mimeTypes;
0152     }
0153 
0154     return it.value().mimeTypes;
0155 }
0156 
0157 QString Package::defaultPackageRoot() const
0158 {
0159     return d->defaultPackageRoot;
0160 }
0161 
0162 void Package::setDefaultPackageRoot(const QString &packageRoot)
0163 {
0164     d.detach();
0165     d->defaultPackageRoot = packageRoot;
0166     if (!d->defaultPackageRoot.isEmpty() && !d->defaultPackageRoot.endsWith(QLatin1Char('/'))) {
0167         d->defaultPackageRoot.append(QLatin1Char('/'));
0168     }
0169 }
0170 
0171 void Package::setFallbackPackage(const KPackage::Package &package)
0172 {
0173     if ((d->fallbackPackage && d->fallbackPackage->path() == package.path() && d->fallbackPackage->metadata() == package.metadata()) ||
0174         // can't be fallback of itself
0175         (package.path() == path() && package.metadata() == metadata()) || d->hasCycle(package)) {
0176         return;
0177     }
0178 
0179     d->fallbackPackage = std::make_unique<Package>(package);
0180 }
0181 
0182 KPackage::Package Package::fallbackPackage() const
0183 {
0184     if (d->fallbackPackage) {
0185         return (*d->fallbackPackage);
0186     } else {
0187         return Package();
0188     }
0189 }
0190 
0191 bool Package::allowExternalPaths() const
0192 {
0193     return d->externalPaths;
0194 }
0195 
0196 void Package::setMetadata(const KPluginMetaData &data)
0197 {
0198     Q_ASSERT(data.isValid());
0199     d->metadata = data;
0200 }
0201 
0202 void Package::setAllowExternalPaths(bool allow)
0203 {
0204     d.detach();
0205     d->externalPaths = allow;
0206 }
0207 
0208 KPluginMetaData Package::metadata() const
0209 {
0210     // qCDebug(KPACKAGE_LOG) << "metadata: " << d->path << filePath("metadata");
0211     if (!d->metadata && !d->path.isEmpty()) {
0212         const QString metadataPath = filePath("metadata");
0213 
0214         if (!metadataPath.isEmpty()) {
0215             d->createPackageMetadata(metadataPath);
0216         } else {
0217             // d->path might still be a file, if its path has a trailing /,
0218             // the fileInfo lookup will fail, so remove it.
0219             QString p = d->path;
0220             if (p.endsWith(QLatin1Char('/'))) {
0221                 p.chop(1);
0222             }
0223             QFileInfo fileInfo(p);
0224 
0225             if (fileInfo.isDir()) {
0226                 d->createPackageMetadata(d->path);
0227             } else if (fileInfo.exists()) {
0228                 d->path = fileInfo.canonicalFilePath();
0229                 d->tempRoot = d->unpack(p);
0230             }
0231         }
0232     }
0233 
0234     // Set a dummy KPluginMetaData object, this way we don't try to do the expensive
0235     // search for the metadata again if none of the paths have changed
0236     if (!d->metadata) {
0237         d->metadata = KPluginMetaData();
0238     }
0239 
0240     return d->metadata.value();
0241 }
0242 
0243 QString PackagePrivate::unpack(const QString &filePath)
0244 {
0245     KArchive *archive = nullptr;
0246     QMimeDatabase db;
0247     QMimeType mimeType = db.mimeTypeForFile(filePath);
0248 
0249     if (mimeType.inherits(QStringLiteral("application/zip"))) {
0250         archive = new KZip(filePath);
0251     } else if (mimeType.inherits(QStringLiteral("application/x-compressed-tar")) || //
0252                mimeType.inherits(QStringLiteral("application/x-gzip")) || //
0253                mimeType.inherits(QStringLiteral("application/x-tar")) || //
0254                mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || //
0255                mimeType.inherits(QStringLiteral("application/x-xz")) || //
0256                mimeType.inherits(QStringLiteral("application/x-lzma"))) {
0257         archive = new KTar(filePath);
0258     } else {
0259         // qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << filePath << mimeType.name();
0260     }
0261     QString tempRoot;
0262     if (archive && archive->open(QIODevice::ReadOnly)) {
0263         const KArchiveDirectory *source = archive->directory();
0264         QTemporaryDir tempdir;
0265         tempdir.setAutoRemove(false);
0266         tempRoot = tempdir.path() + QLatin1Char('/');
0267         source->copyTo(tempRoot);
0268 
0269         if (!QFile::exists(tempdir.path() + QLatin1String("/metadata.json")) && !QFile::exists(tempdir.path() + QLatin1String("/metadata.desktop"))) {
0270             // search metadata.desktop, the zip file might have the package contents in a subdirectory
0271             QDir unpackedPath(tempdir.path());
0272             const auto entries = unpackedPath.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
0273             for (const auto &pack : entries) {
0274                 if (QFile::exists(pack.filePath() + QLatin1String("/metadata.json")) || QFile::exists(pack.filePath() + QLatin1String("/metadata.desktop"))) {
0275                     tempRoot = pack.filePath() + QLatin1Char('/');
0276                 }
0277             }
0278         }
0279 
0280         createPackageMetadata(tempRoot);
0281     } else {
0282         // qCWarning(KPACKAGE_LOG) << "Could not open package file:" << path;
0283     }
0284 
0285     delete archive;
0286     return tempRoot;
0287 }
0288 
0289 bool PackagePrivate::isInsidePackageDir(const QString &canonicalPath) const
0290 {
0291     // make sure that the target file is actually inside the package dir to prevent
0292     // path traversal using symlinks or "../" path segments
0293 
0294     // make sure we got passed a valid path
0295     Q_ASSERT(QFileInfo::exists(canonicalPath));
0296     Q_ASSERT(QFileInfo(canonicalPath).canonicalFilePath() == canonicalPath);
0297     // make sure that the base path is also canonical
0298     // this was not the case until 5.8, making this check fail e.g. if /home is a symlink
0299     // which in turn would make plasmashell not find the .qml files
0300     // installed package
0301     if (tempRoot.isEmpty()) {
0302         Q_ASSERT(QDir(path).exists());
0303         Q_ASSERT(path == QStringLiteral("/") || QDir(path).canonicalPath() + QLatin1Char('/') == path);
0304 
0305         if (canonicalPath.startsWith(path)) {
0306             return true;
0307         }
0308         // temporary compressed package
0309     } else {
0310         Q_ASSERT(QDir(tempRoot).exists());
0311         Q_ASSERT(tempRoot == QStringLiteral("/") || QDir(tempRoot).canonicalPath() + QLatin1Char('/') == tempRoot);
0312 
0313         if (canonicalPath.startsWith(tempRoot)) {
0314             return true;
0315         }
0316     }
0317     qCWarning(KPACKAGE_LOG) << "Path traversal attempt detected:" << canonicalPath << "is not inside" << path;
0318     return false;
0319 }
0320 
0321 QString Package::filePath(const QByteArray &fileType, const QString &filename) const
0322 {
0323     if (!d->valid) {
0324         QString result = d->fallbackFilePath(fileType, filename);
0325         if (result.isEmpty()) {
0326             // qCDebug(KPACKAGE_LOG) << fileType << "file with name" << filename
0327             //    << "was not found in package with path" << d->path;
0328         }
0329         return result;
0330     }
0331 
0332     const QString discoveryKey(QString::fromUtf8(fileType) + filename);
0333     const auto path = d->discoveries.value(discoveryKey);
0334     if (!path.isEmpty()) {
0335         return path;
0336     }
0337 
0338     QStringList paths;
0339 
0340     if (!fileType.isEmpty()) {
0341         const auto contents = d->contents.constFind(fileType);
0342         if (contents == d->contents.constEnd()) {
0343             // qCDebug(KPACKAGE_LOG) << "package does not contain" << fileType << filename;
0344             return d->fallbackFilePath(fileType, filename);
0345         }
0346 
0347         paths = contents->paths;
0348 
0349         if (paths.isEmpty()) {
0350             // qCDebug(KPACKAGE_LOG) << "no matching path came of it, while looking for" << fileType << filename;
0351             d->discoveries.insert(discoveryKey, QString());
0352             return d->fallbackFilePath(fileType, filename);
0353         }
0354     } else {
0355         // when filetype is empty paths is always empty, so try with an empty string
0356         paths << QString();
0357     }
0358 
0359     // Nested loop, but in the medium case resolves to just one iteration
0360     //     qCDebug(KPACKAGE_LOG) << "prefixes:" << d->contentsPrefixPaths.count() << d->contentsPrefixPaths;
0361     for (const QString &contentsPrefix : std::as_const(d->contentsPrefixPaths)) {
0362         QString prefix;
0363         // We are an installed package
0364         if (d->tempRoot.isEmpty()) {
0365             prefix = fileType == "metadata" ? d->path : (d->path + contentsPrefix);
0366             // We are a compressed package temporarily uncompressed in /tmp
0367         } else {
0368             prefix = fileType == "metadata" ? d->tempRoot : (d->tempRoot + contentsPrefix);
0369         }
0370 
0371         for (const QString &path : std::as_const(paths)) {
0372             QString file = prefix + path;
0373 
0374             if (!filename.isEmpty()) {
0375                 file.append(QLatin1Char('/') + filename);
0376             }
0377 
0378             QFileInfo fi(file);
0379             if (fi.exists()) {
0380                 if (d->externalPaths) {
0381                     // qCDebug(KPACKAGE_LOG) << "found" << file;
0382                     d->discoveries.insert(discoveryKey, file);
0383                     return file;
0384                 }
0385 
0386                 // ensure that we don't return files outside of our base path
0387                 // due to symlink or ../ games
0388                 if (d->isInsidePackageDir(fi.canonicalFilePath())) {
0389                     // qCDebug(KPACKAGE_LOG) << "found" << file;
0390                     d->discoveries.insert(discoveryKey, file);
0391                     return file;
0392                 }
0393             }
0394         }
0395     }
0396 
0397     // qCDebug(KPACKAGE_LOG) << fileType << filename << "does not exist in" << prefixes << "at root" << d->path;
0398     return d->fallbackFilePath(fileType, filename);
0399 }
0400 
0401 QUrl Package::fileUrl(const QByteArray &fileType, const QString &filename) const
0402 {
0403     QString path = filePath(fileType, filename);
0404     // construct a qrc:/ url or a file:/ url, the only two protocols supported
0405     if (path.startsWith(QStringLiteral(":"))) {
0406         return QUrl(QStringLiteral("qrc") + path);
0407     } else {
0408         return QUrl::fromLocalFile(path);
0409     }
0410 }
0411 
0412 QStringList Package::entryList(const QByteArray &key) const
0413 {
0414     if (!d->valid) {
0415         return QStringList();
0416     }
0417 
0418     QHash<QByteArray, ContentStructure>::const_iterator it = d->contents.constFind(key);
0419     if (it == d->contents.constEnd()) {
0420         // qCDebug(KPACKAGE_LOG) << "couldn't find" << key;
0421         return QStringList();
0422     }
0423 
0424     // qCDebug(KPACKAGE_LOG) << "going to list" << key;
0425     QStringList list;
0426     for (const QString &prefix : std::as_const(d->contentsPrefixPaths)) {
0427         // qCDebug(KPACKAGE_LOG) << "     looking in" << prefix;
0428         const QStringList paths = it.value().paths;
0429         for (const QString &path : paths) {
0430             // qCDebug(KPACKAGE_LOG) << "         looking in" << path;
0431             if (it.value().directory) {
0432                 // qCDebug(KPACKAGE_LOG) << "it's a directory, so trying out" << d->path + prefix + path;
0433                 QDir dir(d->path + prefix + path);
0434 
0435                 if (d->externalPaths) {
0436                     list += dir.entryList(QDir::Files | QDir::Readable);
0437                 } else {
0438                     // ensure that we don't return files outside of our base path
0439                     // due to symlink or ../ games
0440                     QString canonicalized = dir.canonicalPath();
0441                     if (canonicalized.startsWith(d->path)) {
0442                         list += dir.entryList(QDir::Files | QDir::Readable);
0443                     }
0444                 }
0445             } else {
0446                 const QString fullPath = d->path + prefix + path;
0447                 // qCDebug(KPACKAGE_LOG) << "it's a file at" << fullPath << QFile::exists(fullPath);
0448                 if (!QFile::exists(fullPath)) {
0449                     continue;
0450                 }
0451 
0452                 if (d->externalPaths) {
0453                     list += fullPath;
0454                 } else {
0455                     QDir dir(fullPath);
0456                     QString canonicalized = dir.canonicalPath() + QDir::separator();
0457 
0458                     // qCDebug(KPACKAGE_LOG) << "testing that" << canonicalized << "is in" << d->path;
0459                     if (canonicalized.startsWith(d->path)) {
0460                         list += fullPath;
0461                     }
0462                 }
0463             }
0464         }
0465     }
0466 
0467     return list;
0468 }
0469 
0470 void Package::setPath(const QString &path)
0471 {
0472     // if the path is already what we have, don't bother
0473     if (path == d->path) {
0474         return;
0475     }
0476 
0477     // our dptr is shared, and it is almost certainly going to change.
0478     // hold onto the old pointer just in case it does not, however!
0479     QExplicitlySharedDataPointer<PackagePrivate> oldD(d);
0480     d.detach();
0481     d->metadata = std::nullopt;
0482 
0483     // without structure we're doomed
0484     if (!d->structure) {
0485         d->path.clear();
0486         d->discoveries.clear();
0487         d->valid = false;
0488         d->checkedValid = true;
0489         qCWarning(KPACKAGE_LOG) << "Cannot set a path in a package without structure" << path;
0490         return;
0491     }
0492 
0493     // empty path => nothing to do
0494     if (path.isEmpty()) {
0495         d->path.clear();
0496         d->discoveries.clear();
0497         d->valid = false;
0498         d->structure.data()->pathChanged(this);
0499         return;
0500     }
0501 
0502     // now we look for all possible paths, including resolving
0503     // relative paths against the system search paths
0504     QStringList paths;
0505     if (QDir::isRelativePath(path)) {
0506         QString p;
0507 
0508         if (d->defaultPackageRoot.isEmpty()) {
0509             p = path % QLatin1Char('/');
0510         } else {
0511             p = d->defaultPackageRoot % path % QLatin1Char('/');
0512         }
0513 
0514         if (QDir::isRelativePath(p)) {
0515             // FIXME: can searching of the qrc be better?
0516             paths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, p, QStandardPaths::LocateDirectory);
0517         } else {
0518             const QDir dir(p);
0519             if (QFile::exists(dir.canonicalPath())) {
0520                 paths << p;
0521             }
0522         }
0523 
0524         // qCDebug(KPACKAGE_LOG) << "paths:" << p << paths << d->defaultPackageRoot;
0525     } else {
0526         const QDir dir(path);
0527         if (QFile::exists(dir.canonicalPath())) {
0528             paths << path;
0529         }
0530     }
0531 
0532     QFileInfo fileInfo(path);
0533     if (fileInfo.isFile() && d->tempRoot.isEmpty()) {
0534         d->path = fileInfo.canonicalFilePath();
0535         d->tempRoot = d->unpack(path);
0536     }
0537 
0538     // now we search each path found, caching our previous path to know if
0539     // anything actually really changed
0540     const QString previousPath = d->path;
0541     for (const QString &p : std::as_const(paths)) {
0542         d->checkedValid = false;
0543         QDir dir(p);
0544 
0545         Q_ASSERT(QFile::exists(dir.canonicalPath()));
0546 
0547         // if it has a contents.rcc, give priority to it
0548         if (dir.exists(QStringLiteral("contents.rcc"))) {
0549             d->rccPath = dir.absoluteFilePath(QStringLiteral("contents.rcc"));
0550             QResource::registerResource(d->rccPath);
0551 
0552             // we need just the plugin name here, never the absolute path
0553             dir = QDir(QStringLiteral(":/") + defaultPackageRoot() + QStringView(path).mid(path.lastIndexOf(QLatin1Char('/'))));
0554         }
0555 
0556         d->path = dir.canonicalPath();
0557         // canonicalPath() does not include a trailing slash (unless it is the root dir)
0558         if (!d->path.endsWith(QLatin1Char('/'))) {
0559             d->path.append(QLatin1Char('/'));
0560         }
0561 
0562         const QString fallbackPath = metadata().value(QStringLiteral("X-Plasma-RootPath"));
0563         if (!fallbackPath.isEmpty()) {
0564             const KPackage::Package fp = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), fallbackPath);
0565             setFallbackPackage(fp);
0566         }
0567 
0568         // we need to tell the structure we're changing paths ...
0569         d->structure.data()->pathChanged(this);
0570         // ... and then testing the results for validity
0571         if (isValid()) {
0572             break;
0573         }
0574     }
0575 
0576     // if nothing did change, then we go back to the old dptr
0577     if (d->path == previousPath) {
0578         d = oldD;
0579         return;
0580     }
0581 
0582     // .. but something did change, so we get rid of our discovery cache
0583     d->discoveries.clear();
0584     d->metadata = std::nullopt;
0585 
0586     // uh-oh, but we didn't end up with anything valid, so we sadly reset ourselves
0587     // to futility.
0588     if (!d->valid) {
0589         d->path.clear();
0590         d->structure.data()->pathChanged(this);
0591     }
0592 }
0593 
0594 const QString Package::path() const
0595 {
0596     return d->path;
0597 }
0598 
0599 QStringList Package::contentsPrefixPaths() const
0600 {
0601     return d->contentsPrefixPaths;
0602 }
0603 
0604 void Package::setContentsPrefixPaths(const QStringList &prefixPaths)
0605 {
0606     d.detach();
0607     d->contentsPrefixPaths = prefixPaths;
0608     if (d->contentsPrefixPaths.isEmpty()) {
0609         d->contentsPrefixPaths << QString();
0610     } else {
0611         // the code assumes that the prefixes have a trailing slash
0612         // so let's make that true here
0613         QMutableStringListIterator it(d->contentsPrefixPaths);
0614         while (it.hasNext()) {
0615             it.next();
0616 
0617             if (!it.value().endsWith(QLatin1Char('/'))) {
0618                 it.setValue(it.value() % QLatin1Char('/'));
0619             }
0620         }
0621     }
0622 }
0623 
0624 #if KPACKAGE_BUILD_DEPRECATED_SINCE(5, 21)
0625 QString Package::contentsHash() const
0626 {
0627     return QString::fromLocal8Bit(cryptographicHash(QCryptographicHash::Sha1));
0628 }
0629 #endif
0630 
0631 QByteArray Package::cryptographicHash(QCryptographicHash::Algorithm algorithm) const
0632 {
0633     if (!d->valid) {
0634         qCWarning(KPACKAGE_LOG) << "can not create hash due to Package being invalid";
0635         return QByteArray();
0636     }
0637 
0638     QCryptographicHash hash(algorithm);
0639     const QString metadataPath = QFile::exists(d->path + QLatin1String("metadata.json"))
0640         ? d->path + QLatin1String("metadata.json")
0641         : QFile::exists(d->path + QLatin1String("metadata.desktop")) ? d->path + QLatin1String("metadata.desktop") : QString();
0642     if (!metadataPath.isEmpty()) {
0643         QFile f(metadataPath);
0644         if (f.open(QIODevice::ReadOnly)) {
0645             while (!f.atEnd()) {
0646                 hash.addData(f.read(1024));
0647             }
0648         } else {
0649             qCWarning(KPACKAGE_LOG) << "could not add" << f.fileName() << "to the hash; file could not be opened for reading.";
0650         }
0651     } else {
0652         qCWarning(KPACKAGE_LOG) << "no metadata at" << metadataPath;
0653     }
0654 
0655     for (const QString &prefix : std::as_const(d->contentsPrefixPaths)) {
0656         const QString basePath = d->path + prefix;
0657         QDir dir(basePath);
0658 
0659         if (!dir.exists()) {
0660             return QByteArray();
0661         }
0662 
0663         d->updateHash(basePath, QString(), dir, hash);
0664     }
0665 
0666     return hash.result().toHex();
0667 }
0668 
0669 void Package::addDirectoryDefinition(const QByteArray &key, const QString &path, const QString &name)
0670 {
0671     const auto contentsIt = d->contents.constFind(key);
0672     ContentStructure s;
0673 
0674     if (contentsIt != d->contents.constEnd()) {
0675         if (contentsIt->paths.contains(path) && contentsIt->directory == true && contentsIt->name == name) {
0676             return;
0677         }
0678         s = *contentsIt;
0679     }
0680 
0681     d.detach();
0682 
0683     if (!name.isEmpty()) {
0684         s.name = name;
0685     }
0686 
0687     s.paths.append(path);
0688     s.directory = true;
0689 
0690     d->contents[key] = s;
0691 }
0692 
0693 void Package::addFileDefinition(const QByteArray &key, const QString &path, const QString &name)
0694 {
0695     const auto contentsIt = d->contents.constFind(key);
0696     ContentStructure s;
0697 
0698     if (contentsIt != d->contents.constEnd()) {
0699         if (contentsIt->paths.contains(path) && contentsIt->directory == true && contentsIt->name == name) {
0700             return;
0701         }
0702         s = *contentsIt;
0703     }
0704 
0705     d.detach();
0706     if (!name.isEmpty()) {
0707         s.name = name;
0708     }
0709 
0710     s.paths.append(path);
0711     s.directory = false;
0712 
0713     d->contents[key] = s;
0714 }
0715 
0716 void Package::removeDefinition(const QByteArray &key)
0717 {
0718     if (d->contents.contains(key)) {
0719         d.detach();
0720         d->contents.remove(key);
0721     }
0722 
0723     if (d->discoveries.contains(QString::fromLatin1(key))) {
0724         d.detach();
0725         d->discoveries.remove(QString::fromLatin1(key));
0726     }
0727 }
0728 
0729 void Package::setRequired(const QByteArray &key, bool required)
0730 {
0731     QHash<QByteArray, ContentStructure>::iterator it = d->contents.find(key);
0732     if (it == d->contents.end()) {
0733         return;
0734     }
0735 
0736     d.detach();
0737     // have to find the item again after detaching: d->contents is a different object now
0738     it = d->contents.find(key);
0739     it.value().required = required;
0740 }
0741 
0742 void Package::setDefaultMimeTypes(const QStringList &mimeTypes)
0743 {
0744     d.detach();
0745     d->mimeTypes = mimeTypes;
0746 }
0747 
0748 void Package::setMimeTypes(const QByteArray &key, const QStringList &mimeTypes)
0749 {
0750     QHash<QByteArray, ContentStructure>::iterator it = d->contents.find(key);
0751     if (it == d->contents.end()) {
0752         return;
0753     }
0754 
0755     d.detach();
0756     // have to find the item again after detaching: d->contents is a different object now
0757     it = d->contents.find(key);
0758     it.value().mimeTypes = mimeTypes;
0759 }
0760 
0761 QList<QByteArray> Package::directories() const
0762 {
0763     QList<QByteArray> dirs;
0764     QHash<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin();
0765     while (it != d->contents.constEnd()) {
0766         if (it.value().directory) {
0767             dirs << it.key();
0768         }
0769         ++it;
0770     }
0771     return dirs;
0772 }
0773 
0774 QList<QByteArray> Package::requiredDirectories() const
0775 {
0776     QList<QByteArray> dirs;
0777     QHash<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin();
0778     while (it != d->contents.constEnd()) {
0779         if (it.value().directory && it.value().required) {
0780             dirs << it.key();
0781         }
0782         ++it;
0783     }
0784     return dirs;
0785 }
0786 
0787 QList<QByteArray> Package::files() const
0788 {
0789     QList<QByteArray> files;
0790     QHash<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin();
0791     while (it != d->contents.constEnd()) {
0792         if (!it.value().directory) {
0793             files << it.key();
0794         }
0795         ++it;
0796     }
0797     return files;
0798 }
0799 
0800 QList<QByteArray> Package::requiredFiles() const
0801 {
0802     QList<QByteArray> files;
0803     QHash<QByteArray, ContentStructure>::const_iterator it = d->contents.constBegin();
0804     while (it != d->contents.constEnd()) {
0805         if (!it.value().directory && it.value().required) {
0806             files << it.key();
0807         }
0808         ++it;
0809     }
0810 
0811     return files;
0812 }
0813 
0814 KJob *Package::install(const QString &sourcePackage, const QString &packageRoot)
0815 {
0816     if (!d->structure) {
0817         return nullptr;
0818     }
0819 
0820     const QString src = sourcePackage;
0821     setPath(src);
0822     QString dest = packageRoot.isEmpty() ? defaultPackageRoot() : packageRoot;
0823     KPackage::PackageLoader::self()->d->maxCacheAge = -1;
0824 
0825     // use absolute paths if passed, otherwise go under share
0826     if (!QDir::isAbsolutePath(dest)) {
0827         dest = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + dest;
0828     }
0829 
0830     // qCDebug(KPACKAGE_LOG) << "Source: " << src;
0831     // qCDebug(KPACKAGE_LOG) << "PackageRoot: " << dest;
0832     KJob *j = d->structure.data()->install(this, src, dest);
0833     return j;
0834 }
0835 
0836 KJob *Package::update(const QString &sourcePackage, const QString &packageRoot)
0837 {
0838     if (!d->structure) {
0839         return nullptr;
0840     }
0841 
0842     const QString src = sourcePackage;
0843     setPath(src);
0844     QString dest = packageRoot.isEmpty() ? defaultPackageRoot() : packageRoot;
0845     KPackage::PackageLoader::self()->d->maxCacheAge = -1;
0846 
0847     // use absolute paths if passed, otherwise go under share
0848     if (!QDir::isAbsolutePath(dest)) {
0849         dest = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + dest;
0850     }
0851 
0852     // qCDebug(KPACKAGE_LOG) << "Source: " << src;
0853     // qCDebug(KPACKAGE_LOG) << "PackageRoot: " << dest;
0854     KJob *j = d->structure.data()->update(this, src, dest);
0855     return j;
0856 }
0857 
0858 KJob *Package::uninstall(const QString &packageName, const QString &packageRoot)
0859 {
0860     KPackage::PackageLoader::self()->d->maxCacheAge = -1;
0861     d->createPackageMetadata(packageRoot + QLatin1Char('/') + packageName);
0862     if (!d->structure) {
0863         return nullptr;
0864     }
0865     return d->structure.data()->uninstall(this, packageRoot);
0866 }
0867 
0868 PackagePrivate::PackagePrivate()
0869     : QSharedData()
0870     , fallbackPackage(nullptr)
0871     , externalPaths(false)
0872     , valid(false)
0873     , checkedValid(false)
0874 {
0875     contentsPrefixPaths << QStringLiteral("contents/");
0876 }
0877 
0878 PackagePrivate::PackagePrivate(const PackagePrivate &other)
0879     : QSharedData()
0880 {
0881     *this = other;
0882     if (other.metadata && other.metadata.value().isValid()) {
0883         metadata = other.metadata;
0884     }
0885 }
0886 
0887 PackagePrivate::~PackagePrivate()
0888 {
0889     if (!rccPath.isEmpty()) {
0890         // qresource register/unregisterpath is refcounted if we call it two times
0891         // on the same path, the resource will actually be unregistered only when
0892         // unregister is called 2 times
0893         QResource::unregisterResource(rccPath);
0894     }
0895 
0896     if (!tempRoot.isEmpty()) {
0897         QDir dir(tempRoot);
0898         dir.removeRecursively();
0899     }
0900 }
0901 
0902 PackagePrivate &PackagePrivate::operator=(const PackagePrivate &rhs)
0903 {
0904     if (&rhs == this) {
0905         return *this;
0906     }
0907 
0908     structure = rhs.structure;
0909     if (rhs.fallbackPackage) {
0910         fallbackPackage = std::make_unique<Package>(*rhs.fallbackPackage);
0911     } else {
0912         fallbackPackage = nullptr;
0913     }
0914     if (rhs.metadata && rhs.metadata.value().isValid()) {
0915         metadata = rhs.metadata;
0916     }
0917     path = rhs.path;
0918     contentsPrefixPaths = rhs.contentsPrefixPaths;
0919     contents = rhs.contents;
0920     mimeTypes = rhs.mimeTypes;
0921     defaultPackageRoot = rhs.defaultPackageRoot;
0922     externalPaths = rhs.externalPaths;
0923     valid = rhs.valid;
0924     return *this;
0925 }
0926 
0927 void PackagePrivate::updateHash(const QString &basePath, const QString &subPath, const QDir &dir, QCryptographicHash &hash)
0928 {
0929     // hash is calculated as a function of:
0930     // * files ordered alphabetically by name, with each file's:
0931     //      * path relative to the content root
0932     //      * file data
0933     // * directories ordered alphabetically by name, with each dir's:
0934     //      * path relative to the content root
0935     //      * file listing (recursing)
0936     // symlinks (in both the file and dir case) are handled by adding
0937     // the name of the symlink itself and the abs path of what it points to
0938 
0939     const QDir::SortFlags sorting = QDir::Name | QDir::IgnoreCase;
0940     const QDir::Filters filters = QDir::Hidden | QDir::System | QDir::NoDotAndDotDot;
0941     const auto lstEntries = dir.entryList(QDir::Files | filters, sorting);
0942     for (const QString &file : lstEntries) {
0943         if (!subPath.isEmpty()) {
0944             hash.addData(subPath.toUtf8());
0945         }
0946 
0947         hash.addData(file.toUtf8());
0948 
0949         QFileInfo info(dir.path() + QLatin1Char('/') + file);
0950         if (info.isSymLink()) {
0951             hash.addData(info.symLinkTarget().toUtf8());
0952         } else {
0953             QFile f(info.filePath());
0954             if (f.open(QIODevice::ReadOnly)) {
0955                 while (!f.atEnd()) {
0956                     hash.addData(f.read(1024));
0957                 }
0958             } else {
0959                 qCWarning(KPACKAGE_LOG) << "could not add" << f.fileName() << "to the hash; file could not be opened for reading. "
0960                                         << "permissions fail?" << info.permissions() << info.isFile();
0961             }
0962         }
0963     }
0964 
0965     const auto lstEntries2 = dir.entryList(QDir::Dirs | filters, sorting);
0966     for (const QString &subDirPath : lstEntries2) {
0967         const QString relativePath = subPath + subDirPath + QLatin1Char('/');
0968         hash.addData(relativePath.toUtf8());
0969 
0970         QDir subDir(dir.path());
0971         subDir.cd(subDirPath);
0972 
0973         if (subDir.path() != subDir.canonicalPath()) {
0974             hash.addData(subDir.canonicalPath().toUtf8());
0975         } else {
0976             updateHash(basePath, relativePath, subDir, hash);
0977         }
0978     }
0979 }
0980 
0981 void PackagePrivate::createPackageMetadata(const QString &path)
0982 {
0983     const bool isDir = QFileInfo(path).isDir();
0984 
0985     if (isDir && QFile::exists(path + QStringLiteral("/metadata.json"))) {
0986         metadata = KPluginMetaData::fromJsonFile(path + QStringLiteral("/metadata.json"));
0987 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 92)
0988     } else if (isDir && QFile::exists(path + QStringLiteral("/metadata.desktop"))) {
0989         metadata = KPluginMetaData::fromDesktopFile(path + QStringLiteral("/metadata.desktop"), {QStringLiteral(":/kservicetypes5/kpackage-generic.desktop")});
0990 #endif
0991     } else {
0992         if (isDir) {
0993             qCDebug(KPACKAGE_LOG) << "No metadata file in the package, expected it at:" << path;
0994 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 92)
0995         } else if (path.endsWith(QLatin1String(".desktop"))) {
0996             metadata = KPluginMetaData::fromDesktopFile(path, {QStringLiteral(":/kservicetypes5/kpackage-generic.desktop")});
0997 #endif
0998         } else {
0999             metadata = KPluginMetaData::fromJsonFile(path);
1000         }
1001     }
1002 }
1003 
1004 QString PackagePrivate::fallbackFilePath(const QByteArray &key, const QString &filename) const
1005 {
1006     // don't fallback if the package isn't valid and never fallback the metadata file
1007     if (key != "metadata" && fallbackPackage && fallbackPackage->isValid()) {
1008         return fallbackPackage->filePath(key, filename);
1009     } else {
1010         return QString();
1011     }
1012 }
1013 
1014 bool PackagePrivate::hasCycle(const KPackage::Package &package)
1015 {
1016     if (!package.d->fallbackPackage) {
1017         return false;
1018     }
1019 
1020     // This is the Floyd cycle detection algorithm
1021     // http://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare
1022     const KPackage::Package *slowPackage = &package;
1023     const KPackage::Package *fastPackage = &package;
1024 
1025     while (fastPackage && fastPackage->d->fallbackPackage) {
1026         // consider two packages the same if they have the same metadata
1027         if ((fastPackage->d->fallbackPackage->metadata().isValid() && fastPackage->d->fallbackPackage->metadata() == slowPackage->metadata())
1028             || (fastPackage->d->fallbackPackage->d->fallbackPackage && fastPackage->d->fallbackPackage->d->fallbackPackage->metadata().isValid()
1029                 && fastPackage->d->fallbackPackage->d->fallbackPackage->metadata() == slowPackage->metadata())) {
1030             qCWarning(KPACKAGE_LOG) << "Warning: the fallback chain of " << package.metadata().pluginId() << "contains a cyclical dependency.";
1031             return true;
1032         }
1033         fastPackage = fastPackage->d->fallbackPackage->d->fallbackPackage.get();
1034         slowPackage = slowPackage->d->fallbackPackage.get();
1035     }
1036     return false;
1037 }
1038 
1039 Q_GLOBAL_STATIC(PackageDeletionNotifier, s_packageDeletionNotifier)
1040 PackageDeletionNotifier *PackageDeletionNotifier::self()
1041 {
1042     return s_packageDeletionNotifier;
1043 }
1044 
1045 } // Namespace
1046 
1047 #include "private/moc_package_p.cpp"