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