File indexing completed on 2023-12-03 10:58:38

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