File indexing completed on 2025-04-20 03:41: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