File indexing completed on 2024-04-14 03:54:16

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