Advance Wayland and KDE package bring-up

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-14 10:51:06 +01:00
parent 51f3c21121
commit cf12defd28
15214 changed files with 20594243 additions and 269 deletions
@@ -0,0 +1,8 @@
add_subdirectory(kpackage)
add_subdirectory(kpackagetool)
ecm_qt_install_logging_categories(
EXPORT KPACKAGE
FILES kpackage.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)
@@ -0,0 +1,11 @@
#!/bin/sh
# Invoke the extractrc script on all .ui, .rc, and .kcfg files in the sources.
# The results are stored in a pseudo .cpp file to be picked up by xgettext.
lst=`find . -name \*.rc -o -name \*.ui -o -name \*.kcfg`
if [ -n "$lst" ] ; then
$EXTRACTRC $lst >> rc.cpp
fi
# Run xgettext to extract strings from all source files.
$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/libkpackage6.pot
@@ -0,0 +1,111 @@
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-package.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-package.h)
add_library(KF6Package)
add_library(KF6::Package ALIAS KF6Package)
set_target_properties(KF6Package PROPERTIES
VERSION ${PACKAGE_VERSION}
SOVERSION ${PACKAGE_SOVERSION}
EXPORT_NAME Package
)
target_sources(KF6Package PRIVATE
package.cpp
packagestructure.cpp
packageloader.cpp
packagejob.cpp
private/packages.cpp
private/packagejobthread.cpp
)
ecm_qt_declare_logging_category(KF6Package
HEADER kpackage_debug.h
IDENTIFIER KPACKAGE_LOG
CATEGORY_NAME kf.package
OLD_CATEGORY_NAMES kf5.kpackage
DESCRIPTION "kpackage (lib)"
EXPORT KPACKAGE
)
ecm_generate_export_header(KF6Package
EXPORT_FILE_NAME kpackage/package_export.h
BASE_NAME KPackage
GROUP_BASE_NAME KF
VERSION ${KF_VERSION}
USE_VERSION_HEADER
DEPRECATED_BASE_VERSION 0
DEPRECATION_VERSIONS
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
)
target_link_libraries(KF6Package
PUBLIC
KF6::CoreAddons
PRIVATE
KF6::Archive
KF6::I18n
)
if (HAVE_DBUS)
target_link_libraries(KF6Package
PRIVATE
Qt::DBus # notification
)
target_compile_definitions(KF6Package PRIVATE -DHAVE_QTDBUS=1)
else()
target_compile_definitions(KF6Package PRIVATE -DHAVE_QTDBUS=0)
endif()
target_include_directories(KF6Package PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..;${CMAKE_CURRENT_BINARY_DIR};${CMAKE_CURRENT_BINARY_DIR}/KPackage>"
)
target_include_directories(KF6Package INTERFACE
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KPackage>"
)
########### install files ###############
ecm_generate_headers(Package_CamelCase_HEADERS
HEADER_NAMES
Package
PackageStructure
PackageLoader
PackageJob
packagestructure_compat_p
REQUIRED_HEADERS Package_HEADERS
PREFIX KPackage
)
install(FILES
${Package_HEADERS}
${CMAKE_CURRENT_BINARY_DIR}/kpackage/package_export.h
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KPackage/kpackage COMPONENT Devel)
install(FILES
${Package_CamelCase_HEADERS}
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KPackage/KPackage COMPONENT Devel)
install(TARGETS KF6Package EXPORT KF6PackageTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
if (NOT BUILD_SHARED_LIBS)
install(TARGETS kpackage_common_STATIC EXPORT KF6PackageTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
endif()
if(BUILD_QCH)
ecm_add_qch(
KF6Package_QCH
NAME KPackage
BASE_NAME KF6Package
VERSION ${KF_VERSION}
ORG_DOMAIN org.kde
SOURCES # using only public headers, to cover only public API
${Package_HEADERS}
MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md"
LINK_QCHS
KF6CoreAddons_QCH
BLANK_MACROS
KPACKAGE_EXPORT
"KPACKAGE_DEPRECATED_VERSION(x, y, t)"
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
COMPONENT Devel
)
endif()
@@ -0,0 +1,4 @@
#define KPACKAGE_RELATIVE_DATA_INSTALL_DIR "@KPACKAGE_RELATIVE_DATA_INSTALL_DIR@"
#define KDE_INSTALL_FULL_LIBEXECDIR_KF "@KDE_INSTALL_FULL_LIBEXECDIR_KF@"
@@ -0,0 +1,903 @@
/*
SPDX-FileCopyrightText: 2007 Aaron Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2010 Marco Martin <notmart@gmail.com>
SPDX-FileCopyrightText: 2010 Kevin Ottens <ervin@kde.org>
SPDX-FileCopyrightText: 2009 Rob Scheepmaker
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "package.h"
#include <QResource>
#include <qtemporarydir.h>
#include "kpackage_debug.h"
#include <KArchive>
#include <KLocalizedString>
#include <KTar>
#include <kzip.h>
#include "config-package.h"
#include <QMimeDatabase>
#include <QStandardPaths>
#include "packageloader.h"
#include "packagestructure.h"
#include "private/package_p.h"
#include "private/packageloader_p.h"
namespace KPackage
{
Package::Package(PackageStructure *structure)
: d(new PackagePrivate())
{
d->structure = structure;
if (d->structure) {
addFileDefinition("metadata", QStringLiteral("metadata.json"));
d->structure.data()->initPackage(this);
}
}
Package::Package(const Package &other)
: d(other.d)
{
}
Package::~Package() = default;
Package &Package::operator=(const Package &rhs)
{
if (&rhs != this) {
d = rhs.d;
}
return *this;
}
bool Package::hasValidStructure() const
{
qWarning() << d->structure << requiredFiles();
return d->structure;
}
bool Package::isValid() const
{
if (!d->structure) {
return false;
}
// Minimal packages with no metadata *are* supposed to be possible
// so if !metadata().isValid() go ahead
if (metadata().isValid() && metadata().value(QStringLiteral("isHidden"), QStringLiteral("false")) == QLatin1String("true")) {
return false;
}
if (d->checkedValid) {
return d->valid;
}
const QString rootPath = d->tempRoot.isEmpty() ? d->path : d->tempRoot;
if (rootPath.isEmpty()) {
return false;
}
d->valid = true;
// search for the file in all prefixes and in all possible paths for each prefix
// even if it's a big nested loop, usually there is one prefix and one location
// so shouldn't cause too much disk access
for (auto it = d->contents.cbegin(), end = d->contents.cend(); it != end; ++it) {
if (it.value().required && filePath(it.key()).isEmpty()) {
qCWarning(KPACKAGE_LOG) << "Could not find required" << (it.value().directory ? "directory" : "file") << it.key() << "for package" << path()
<< "should be" << it.value().paths;
d->valid = false;
break;
}
}
return d->valid;
}
bool Package::isRequired(const QByteArray &key) const
{
auto it = d->contents.constFind(key);
if (it == d->contents.constEnd()) {
return false;
}
return it.value().required;
}
QStringList Package::mimeTypes(const QByteArray &key) const
{
auto it = d->contents.constFind(key);
if (it == d->contents.constEnd()) {
return QStringList();
}
if (it.value().mimeTypes.isEmpty()) {
return d->mimeTypes;
}
return it.value().mimeTypes;
}
QString Package::defaultPackageRoot() const
{
return d->defaultPackageRoot;
}
void Package::setDefaultPackageRoot(const QString &packageRoot)
{
d.detach();
d->defaultPackageRoot = packageRoot;
if (!d->defaultPackageRoot.isEmpty() && !d->defaultPackageRoot.endsWith(QLatin1Char('/'))) {
d->defaultPackageRoot.append(QLatin1Char('/'));
}
}
void Package::setFallbackPackage(const KPackage::Package &package)
{
if ((d->fallbackPackage && d->fallbackPackage->path() == package.path() && d->fallbackPackage->metadata() == package.metadata()) ||
// can't be fallback of itself
(package.path() == path() && package.metadata() == metadata()) || d->hasCycle(package)) {
return;
}
d->fallbackPackage = std::make_unique<Package>(package);
}
KPackage::Package Package::fallbackPackage() const
{
if (d->fallbackPackage) {
return (*d->fallbackPackage);
} else {
return Package();
}
}
bool Package::allowExternalPaths() const
{
return d->externalPaths;
}
void Package::setMetadata(const KPluginMetaData &data)
{
Q_ASSERT(data.isValid());
d->metadata = data;
}
void Package::setAllowExternalPaths(bool allow)
{
d.detach();
d->externalPaths = allow;
}
KPluginMetaData Package::metadata() const
{
// qCDebug(KPACKAGE_LOG) << "metadata: " << d->path << filePath("metadata");
if (!d->metadata && !d->path.isEmpty()) {
const QString metadataPath = filePath("metadata", QStringLiteral("metadata.json"));
if (!metadataPath.isEmpty()) {
d->createPackageMetadata(metadataPath);
} else {
// d->path might still be a file, if its path has a trailing /,
// the fileInfo lookup will fail, so remove it.
QString p = d->path;
if (p.endsWith(QLatin1Char('/'))) {
p.chop(1);
}
QFileInfo fileInfo(p);
if (fileInfo.isDir()) {
d->createPackageMetadata(d->path);
} else if (fileInfo.exists()) {
d->path = fileInfo.canonicalFilePath();
d->tempRoot = d->unpack(p);
}
}
}
// Set a dummy KPluginMetaData object, this way we don't try to do the expensive
// search for the metadata again if none of the paths have changed
if (!d->metadata) {
d->metadata = KPluginMetaData();
}
return d->metadata.value();
}
QString PackagePrivate::unpack(const QString &filePath)
{
KArchive *archive = nullptr;
QMimeDatabase db;
QMimeType mimeType = db.mimeTypeForFile(filePath);
if (mimeType.inherits(QStringLiteral("application/zip"))) {
archive = new KZip(filePath);
} else if (mimeType.inherits(QStringLiteral("application/x-compressed-tar")) || //
mimeType.inherits(QStringLiteral("application/x-gzip")) || //
mimeType.inherits(QStringLiteral("application/x-tar")) || //
mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || //
mimeType.inherits(QStringLiteral("application/x-xz")) || //
mimeType.inherits(QStringLiteral("application/x-lzma"))) {
archive = new KTar(filePath);
} else {
// qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << filePath << mimeType.name();
}
QString tempRoot;
if (archive && archive->open(QIODevice::ReadOnly)) {
const KArchiveDirectory *source = archive->directory();
QTemporaryDir tempdir;
tempdir.setAutoRemove(false);
tempRoot = tempdir.path() + QLatin1Char('/');
source->copyTo(tempRoot);
if (!QFile::exists(tempdir.path() + QLatin1String("/metadata.json"))) {
// search metadata.json, the zip file might have the package contents in a subdirectory
QDir unpackedPath(tempdir.path());
const auto entries = unpackedPath.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const auto &pack : entries) {
if (QFile::exists(pack.filePath() + QLatin1String("/metadata.json"))) {
tempRoot = pack.filePath() + QLatin1Char('/');
}
}
}
createPackageMetadata(tempRoot);
} else {
// qCWarning(KPACKAGE_LOG) << "Could not open package file:" << path;
}
delete archive;
return tempRoot;
}
bool PackagePrivate::isInsidePackageDir(const QString &canonicalPath) const
{
// make sure that the target file is actually inside the package dir to prevent
// path traversal using symlinks or "../" path segments
// make sure we got passed a valid path
Q_ASSERT(QFileInfo::exists(canonicalPath));
Q_ASSERT(QFileInfo(canonicalPath).canonicalFilePath() == canonicalPath);
// make sure that the base path is also canonical
// this was not the case until 5.8, making this check fail e.g. if /home is a symlink
// which in turn would make plasmashell not find the .qml files
// installed package
if (tempRoot.isEmpty()) {
Q_ASSERT(QDir(path).exists());
Q_ASSERT(path == QStringLiteral("/") || QDir(path).canonicalPath() + QLatin1Char('/') == path);
if (canonicalPath.startsWith(path)) {
return true;
}
// temporary compressed package
} else {
Q_ASSERT(QDir(tempRoot).exists());
Q_ASSERT(tempRoot == QStringLiteral("/") || QDir(tempRoot).canonicalPath() + QLatin1Char('/') == tempRoot);
if (canonicalPath.startsWith(tempRoot)) {
return true;
}
}
qCWarning(KPACKAGE_LOG) << "Path traversal attempt detected:" << canonicalPath << "is not inside" << path;
return false;
}
QString Package::filePath(const QByteArray &fileType, const QString &filename) const
{
if (!d->valid && d->checkedValid) { // Don't check the validity here, because we'd have infinite recursion
QString result = d->fallbackFilePath(fileType, filename);
if (result.isEmpty()) {
// qCDebug(KPACKAGE_LOG) << fileType << "file with name" << filename
// << "was not found in package with path" << d->path;
}
return result;
}
const QString discoveryKey(QString::fromUtf8(fileType) + filename);
const auto path = d->discoveries.value(discoveryKey);
if (!path.isEmpty()) {
return path;
}
QStringList paths;
if (!fileType.isEmpty()) {
const auto contents = d->contents.constFind(fileType);
if (contents == d->contents.constEnd()) {
// qCDebug(KPACKAGE_LOG) << "package does not contain" << fileType << filename;
return d->fallbackFilePath(fileType, filename);
}
paths = contents->paths;
if (paths.isEmpty()) {
// qCDebug(KPACKAGE_LOG) << "no matching path came of it, while looking for" << fileType << filename;
d->discoveries.insert(discoveryKey, QString());
return d->fallbackFilePath(fileType, filename);
}
} else {
// when filetype is empty paths is always empty, so try with an empty string
paths << QString();
}
// Nested loop, but in the medium case resolves to just one iteration
// qCDebug(KPACKAGE_LOG) << "prefixes:" << d->contentsPrefixPaths.count() << d->contentsPrefixPaths;
for (const QString &contentsPrefix : std::as_const(d->contentsPrefixPaths)) {
QString prefix;
// We are an installed package
if (d->tempRoot.isEmpty()) {
prefix = fileType == "metadata" ? d->path : (d->path + contentsPrefix);
// We are a compressed package temporarily uncompressed in /tmp
} else {
prefix = fileType == "metadata" ? d->tempRoot : (d->tempRoot + contentsPrefix);
}
for (const QString &path : std::as_const(paths)) {
QString file = prefix + path;
if (!filename.isEmpty()) {
file.append(QLatin1Char('/') + filename);
}
QFileInfo fi(file);
if (fi.exists()) {
if (d->externalPaths) {
// qCDebug(KPACKAGE_LOG) << "found" << file;
d->discoveries.insert(discoveryKey, file);
return file;
}
// ensure that we don't return files outside of our base path
// due to symlink or ../ games
if (d->isInsidePackageDir(fi.canonicalFilePath())) {
// qCDebug(KPACKAGE_LOG) << "found" << file;
d->discoveries.insert(discoveryKey, file);
return file;
}
}
}
}
// qCDebug(KPACKAGE_LOG) << fileType << filename << "does not exist in" << prefixes << "at root" << d->path;
return d->fallbackFilePath(fileType, filename);
}
QUrl Package::fileUrl(const QByteArray &fileType, const QString &filename) const
{
QString path = filePath(fileType, filename);
// construct a qrc:/ url or a file:/ url, the only two protocols supported
if (path.startsWith(QStringLiteral(":"))) {
return QUrl(QStringLiteral("qrc") + path);
} else {
return QUrl::fromLocalFile(path);
}
}
QStringList Package::entryList(const QByteArray &key) const
{
if (!d->valid) {
return QStringList();
}
const auto it = d->contents.constFind(key);
if (it == d->contents.constEnd()) {
qCWarning(KPACKAGE_LOG) << "couldn't find" << key << "when trying to list entries";
return QStringList();
}
QStringList list;
for (const QString &prefix : std::as_const(d->contentsPrefixPaths)) {
// qCDebug(KPACKAGE_LOG) << " looking in" << prefix;
const QStringList paths = it.value().paths;
for (const QString &path : paths) {
// qCDebug(KPACKAGE_LOG) << " looking in" << path;
if (it.value().directory) {
// qCDebug(KPACKAGE_LOG) << "it's a directory, so trying out" << d->path + prefix + path;
QDir dir(d->path + prefix + path);
if (d->externalPaths) {
list += dir.entryList(QDir::Files | QDir::Readable);
} else {
// ensure that we don't return files outside of our base path
// due to symlink or ../ games
QString canonicalized = dir.canonicalPath();
if (canonicalized.startsWith(d->path)) {
list += dir.entryList(QDir::Files | QDir::Readable);
}
}
} else {
const QString fullPath = d->path + prefix + path;
// qCDebug(KPACKAGE_LOG) << "it's a file at" << fullPath << QFile::exists(fullPath);
if (!QFile::exists(fullPath)) {
continue;
}
if (d->externalPaths) {
list += fullPath;
} else {
QDir dir(fullPath);
QString canonicalized = dir.canonicalPath() + QDir::separator();
// qCDebug(KPACKAGE_LOG) << "testing that" << canonicalized << "is in" << d->path;
if (canonicalized.startsWith(d->path)) {
list += fullPath;
}
}
}
}
}
return list;
}
void Package::setPath(const QString &path)
{
// if the path is already what we have, don't bother
if (path == d->path) {
return;
}
// our dptr is shared, and it is almost certainly going to change.
// hold onto the old pointer just in case it does not, however!
QExplicitlySharedDataPointer<PackagePrivate> oldD(d);
d.detach();
d->metadata = std::nullopt;
// without structure we're doomed
if (!d->structure) {
d->path.clear();
d->discoveries.clear();
d->valid = false;
d->checkedValid = true;
qCWarning(KPACKAGE_LOG) << "Cannot set a path in a package without structure" << path;
return;
}
// empty path => nothing to do
if (path.isEmpty()) {
d->path.clear();
d->discoveries.clear();
d->valid = false;
d->structure.data()->pathChanged(this);
return;
}
// now we look for all possible paths, including resolving
// relative paths against the system search paths
QStringList paths;
if (QDir::isRelativePath(path)) {
QString p;
if (d->defaultPackageRoot.isEmpty()) {
p = path % QLatin1Char('/');
} else {
p = d->defaultPackageRoot % path % QLatin1Char('/');
}
if (QDir::isRelativePath(p)) {
// FIXME: can searching of the qrc be better?
paths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, p, QStandardPaths::LocateDirectory);
} else {
const QDir dir(p);
if (QFile::exists(dir.canonicalPath())) {
paths << p;
}
}
// qCDebug(KPACKAGE_LOG) << "paths:" << p << paths << d->defaultPackageRoot;
} else {
const QDir dir(path);
if (QFile::exists(dir.canonicalPath())) {
paths << path;
}
}
QFileInfo fileInfo(path);
if (fileInfo.isFile() && d->tempRoot.isEmpty()) {
d->path = fileInfo.canonicalFilePath();
d->tempRoot = d->unpack(path);
}
// now we search each path found, caching our previous path to know if
// anything actually really changed
const QString previousPath = d->path;
for (const QString &p : std::as_const(paths)) {
d->checkedValid = false;
QDir dir(p);
Q_ASSERT(QFile::exists(dir.canonicalPath()));
d->path = dir.canonicalPath();
// canonicalPath() does not include a trailing slash (unless it is the root dir)
if (!d->path.endsWith(QLatin1Char('/'))) {
d->path.append(QLatin1Char('/'));
}
const QString fallbackPath = metadata().value(QStringLiteral("X-Plasma-RootPath"));
if (!fallbackPath.isEmpty()) {
const KPackage::Package fp = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), fallbackPath);
setFallbackPackage(fp);
}
// we need to tell the structure we're changing paths ...
d->structure.data()->pathChanged(this);
// ... and then testing the results for validity
if (isValid()) {
break;
}
}
// if nothing did change, then we go back to the old dptr
if (d->path == previousPath) {
d = oldD;
return;
}
// .. but something did change, so we get rid of our discovery cache
d->discoveries.clear();
// Do NOT override the metadata when the PackageStructure has set it
if (!previousPath.isEmpty()) {
d->metadata = std::nullopt;
}
// uh-oh, but we didn't end up with anything valid, so we sadly reset ourselves
// to futility.
if (!d->valid) {
d->path.clear();
d->structure.data()->pathChanged(this);
}
}
const QString Package::path() const
{
return d->path;
}
QStringList Package::contentsPrefixPaths() const
{
return d->contentsPrefixPaths;
}
void Package::setContentsPrefixPaths(const QStringList &prefixPaths)
{
d.detach();
d->contentsPrefixPaths = prefixPaths;
if (d->contentsPrefixPaths.isEmpty()) {
d->contentsPrefixPaths << QString();
} else {
// the code assumes that the prefixes have a trailing slash
// so let's make that true here
QMutableStringListIterator it(d->contentsPrefixPaths);
while (it.hasNext()) {
it.next();
if (!it.value().endsWith(QLatin1Char('/'))) {
it.setValue(it.value() % QLatin1Char('/'));
}
}
}
}
QByteArray Package::cryptographicHash(QCryptographicHash::Algorithm algorithm) const
{
if (!d->valid) {
qCWarning(KPACKAGE_LOG) << "can not create hash due to Package being invalid";
return QByteArray();
}
QCryptographicHash hash(algorithm);
const QString guessedMetaDataJson = d->path + QLatin1String("metadata.json");
const QString metadataPath = QFile::exists(guessedMetaDataJson) ? guessedMetaDataJson : QString();
if (!metadataPath.isEmpty()) {
QFile f(metadataPath);
if (f.open(QIODevice::ReadOnly)) {
while (!f.atEnd()) {
hash.addData(f.read(1024));
}
} else {
qCWarning(KPACKAGE_LOG) << "could not add" << f.fileName() << "to the hash; file could not be opened for reading.";
}
} else {
qCWarning(KPACKAGE_LOG) << "no metadata at" << metadataPath;
}
for (const QString &prefix : std::as_const(d->contentsPrefixPaths)) {
const QString basePath = d->path + prefix;
QDir dir(basePath);
if (!dir.exists()) {
return QByteArray();
}
d->updateHash(basePath, QString(), dir, hash);
}
return hash.result().toHex();
}
void Package::addDirectoryDefinition(const QByteArray &key, const QString &path)
{
const auto contentsIt = d->contents.constFind(key);
ContentStructure s;
if (contentsIt != d->contents.constEnd()) {
if (contentsIt->paths.contains(path) && contentsIt->directory == true) {
return;
}
s = *contentsIt;
}
d.detach();
s.paths.append(path);
s.directory = true;
d->contents[key] = s;
}
void Package::addFileDefinition(const QByteArray &key, const QString &path)
{
const auto contentsIt = d->contents.constFind(key);
ContentStructure s;
if (contentsIt != d->contents.constEnd()) {
if (contentsIt->paths.contains(path) && contentsIt->directory == true) {
return;
}
s = *contentsIt;
}
d.detach();
s.paths.append(path);
s.directory = false;
d->contents[key] = s;
}
void Package::removeDefinition(const QByteArray &key)
{
if (d->contents.contains(key)) {
d.detach();
d->contents.remove(key);
}
if (d->discoveries.contains(QString::fromLatin1(key))) {
d.detach();
d->discoveries.remove(QString::fromLatin1(key));
}
}
void Package::setRequired(const QByteArray &key, bool required)
{
QHash<QByteArray, ContentStructure>::iterator it = d->contents.find(key);
if (it == d->contents.end()) {
qCWarning(KPACKAGE_LOG) << key << "is now a known key for the package. File is thus not set to being required";
return;
}
d.detach();
// have to find the item again after detaching: d->contents is a different object now
it = d->contents.find(key);
it.value().required = required;
}
void Package::setDefaultMimeTypes(const QStringList &mimeTypes)
{
d.detach();
d->mimeTypes = mimeTypes;
}
void Package::setMimeTypes(const QByteArray &key, const QStringList &mimeTypes)
{
if (!d->contents.contains(key)) {
return;
}
d.detach();
d->contents[key].mimeTypes = mimeTypes;
}
QList<QByteArray> Package::directories() const
{
QList<QByteArray> dirs;
for (auto it = d->contents.cbegin(); it != d->contents.cend(); ++it) {
if (it.value().directory) {
dirs << it.key();
}
}
return dirs;
}
QList<QByteArray> Package::requiredDirectories() const
{
QList<QByteArray> dirs;
for (auto it = d->contents.cbegin(); it != d->contents.cend(); ++it) {
if (it.value().directory && it.value().required) {
dirs << it.key();
}
}
return dirs;
}
QList<QByteArray> Package::files() const
{
QList<QByteArray> files;
for (auto it = d->contents.cbegin(); it != d->contents.cend(); ++it) {
if (!it.value().directory) {
files << it.key();
}
}
return files;
}
QList<QByteArray> Package::requiredFiles() const
{
QList<QByteArray> files;
for (auto it = d->contents.cbegin(); it != d->contents.cend(); ++it) {
if (!it.value().directory && it.value().required) {
files << it.key();
}
}
return files;
}
PackagePrivate::PackagePrivate()
: QSharedData()
{
contentsPrefixPaths << QStringLiteral("contents/");
}
PackagePrivate::PackagePrivate(const PackagePrivate &other)
: QSharedData()
{
*this = other;
if (other.metadata && other.metadata.value().isValid()) {
metadata = other.metadata;
}
}
PackagePrivate::~PackagePrivate()
{
if (!tempRoot.isEmpty()) {
QDir dir(tempRoot);
dir.removeRecursively();
}
}
PackagePrivate &PackagePrivate::operator=(const PackagePrivate &rhs)
{
if (&rhs == this) {
return *this;
}
structure = rhs.structure;
if (rhs.fallbackPackage) {
fallbackPackage = std::make_unique<Package>(*rhs.fallbackPackage);
} else {
fallbackPackage = nullptr;
}
if (rhs.metadata && rhs.metadata.value().isValid()) {
metadata = rhs.metadata;
}
path = rhs.path;
contentsPrefixPaths = rhs.contentsPrefixPaths;
contents = rhs.contents;
mimeTypes = rhs.mimeTypes;
defaultPackageRoot = rhs.defaultPackageRoot;
externalPaths = rhs.externalPaths;
valid = rhs.valid;
return *this;
}
void PackagePrivate::updateHash(const QString &basePath, const QString &subPath, const QDir &dir, QCryptographicHash &hash)
{
// hash is calculated as a function of:
// * files ordered alphabetically by name, with each file's:
// * path relative to the content root
// * file data
// * directories ordered alphabetically by name, with each dir's:
// * path relative to the content root
// * file listing (recursing)
// symlinks (in both the file and dir case) are handled by adding
// the name of the symlink itself and the abs path of what it points to
const QDir::SortFlags sorting = QDir::Name | QDir::IgnoreCase;
const QDir::Filters filters = QDir::Hidden | QDir::System | QDir::NoDotAndDotDot;
const auto lstEntries = dir.entryList(QDir::Files | filters, sorting);
for (const QString &file : lstEntries) {
if (!subPath.isEmpty()) {
hash.addData(subPath.toUtf8());
}
hash.addData(file.toUtf8());
QFileInfo info(dir.path() + QLatin1Char('/') + file);
if (info.isSymLink()) {
hash.addData(info.symLinkTarget().toUtf8());
} else {
QFile f(info.filePath());
if (f.open(QIODevice::ReadOnly)) {
while (!f.atEnd()) {
hash.addData(f.read(1024));
}
} else {
qCWarning(KPACKAGE_LOG) << "could not add" << f.fileName() << "to the hash; file could not be opened for reading. "
<< "permissions fail?" << info.permissions() << info.isFile();
}
}
}
const auto lstEntries2 = dir.entryList(QDir::Dirs | filters, sorting);
for (const QString &subDirPath : lstEntries2) {
const QString relativePath = subPath + subDirPath + QLatin1Char('/');
hash.addData(relativePath.toUtf8());
QDir subDir(dir.path());
subDir.cd(subDirPath);
if (subDir.path() != subDir.canonicalPath()) {
hash.addData(subDir.canonicalPath().toUtf8());
} else {
updateHash(basePath, relativePath, subDir, hash);
}
}
}
void PackagePrivate::createPackageMetadata(const QString &path)
{
if (QFileInfo(path).isDir()) {
if (const QString jsonPath = path + QLatin1String("/metadata.json"); QFileInfo::exists(jsonPath)) {
metadata = KPluginMetaData::fromJsonFile(jsonPath);
} else {
qCDebug(KPACKAGE_LOG) << "No metadata file in the package, expected it at:" << jsonPath;
}
} else {
metadata = KPluginMetaData::fromJsonFile(path);
}
}
QString PackagePrivate::fallbackFilePath(const QByteArray &key, const QString &filename) const
{
// don't fallback if the package isn't valid and never fallback the metadata file
if (key != "metadata" && fallbackPackage && fallbackPackage->isValid()) {
return fallbackPackage->filePath(key, filename);
} else {
return QString();
}
}
bool PackagePrivate::hasCycle(const KPackage::Package &package)
{
if (!package.d->fallbackPackage) {
return false;
}
// This is the Floyd cycle detection algorithm
// http://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare
const KPackage::Package *slowPackage = &package;
const KPackage::Package *fastPackage = &package;
while (fastPackage && fastPackage->d->fallbackPackage) {
// consider two packages the same if they have the same metadata
if ((fastPackage->d->fallbackPackage->metadata().isValid() && fastPackage->d->fallbackPackage->metadata() == slowPackage->metadata())
|| (fastPackage->d->fallbackPackage->d->fallbackPackage && fastPackage->d->fallbackPackage->d->fallbackPackage->metadata().isValid()
&& fastPackage->d->fallbackPackage->d->fallbackPackage->metadata() == slowPackage->metadata())) {
qCWarning(KPACKAGE_LOG) << "Warning: the fallback chain of " << package.metadata().pluginId() << "contains a cyclical dependency.";
return true;
}
fastPackage = fastPackage->d->fallbackPackage->d->fallbackPackage.get();
slowPackage = slowPackage->d->fallbackPackage.get();
}
return false;
}
} // Namespace
@@ -0,0 +1,317 @@
/*
SPDX-FileCopyrightText: 2007-2011 Aaron Seigo <aseigo@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPACKAGE_PACKAGE_H
#define KPACKAGE_PACKAGE_H
#include <QCryptographicHash>
#include <QMetaType>
#include <QStringList>
#include <QUrl>
#include <KPluginMetaData>
#include <kpackage/package_export.h>
#include <KJob>
namespace KPackage
{
/**
* @class Package kpackage/package.h <KPackage/Package>
*
* @short object representing an installed package
*
* Package defines what is in a package and provides easy access to the contents.
*
* To define a package, one might write the following code:
*
@code
Package package;
package.addDirectoryDefinition("images", "pics/");
package.setMimeTypes("images", QStringList{"image/svg", "image/png", "image/jpeg"});
package.addDirectoryDefinition("scripts", "code/");
package.setMimeTypes("scripts", QStringList{"text/\*"});
package.addFileDefinition("mainscript", "code/main.js");
package.setRequired("mainscript", true);
@endcode
* One may also choose to create a subclass of PackageStructure and include the setup
* in the constructor.
*
* Either way, Package creates a self-documenting contract between the packager and
* the application without exposing package internals such as actual on-disk structure
* of the package or requiring that all contents be explicitly known ahead of time.
*
* Subclassing PackageStructure does have provide a number of potential const benefits:
* * the package can be notified of path changes via the virtual pathChanged() method
* * the subclass may implement mechanisms to install and remove packages using the
* virtual install and uninstall methods
* * subclasses can be compiled as plugins for easy re-use
**/
// TODO: write documentation on USING a package
class PackagePrivate;
class PackageStructure;
class KPACKAGE_EXPORT Package
{
public:
/**
* Default constructor
*
* @param structure if a null pointer is passed in, this will creates an empty (invalid) Package;
* otherwise the structure is allowed to set up the Package's initial layout
*/
explicit Package(PackageStructure *structure = nullptr);
/**
* Copy constructor
*/
Package(const Package &other);
virtual ~Package();
/**
* Assignment operator
*/
Package &operator=(const Package &rhs);
/**
* @return true if this package has a valid PackageStructure associatedw it with it.
* A package may not be valid, but have a valid structure. Useful when dealing with
* Package objects in a semi-initialized state (e.g. before calling setPath())
* @since 5.1
*/
bool hasValidStructure() const;
/**
* @return true if all the required components exist
**/
bool isValid() const;
/**
* Sets the path to the root of this package
* @param path an absolute path, or a relative path to the default package root
*/
void setPath(const QString &path);
/**
* @return the path to the root of this particular package
*/
const QString path() const;
/**
* Get the path to a given file based on the key and an optional filename.
* Example: finding the main script in a scripting package:
* filePath("mainscript")
*
* Example: finding a specific image in the images directory:
* filePath("images", "myimage.png")
*
* @param key the key of the file type to look for,
* @param filename optional name of the file to locate within the package
* @return path to the file on disk. QString() if not found.
**/
QString filePath(const QByteArray &key, const QString &filename = QString()) const;
/**
* Get the url to a given file based on the key and an optional filename, is the file:// or qrc:// format
* Example: finding the main script in a scripting package:
* filePath("mainscript")
*
* Example: finding a specific image in the images directory:
* filePath("images", "myimage.png")
*
* @param key the key of the file type to look for,
* @param filename optional name of the file to locate within the package
* @return path to the file on disk. QUrl() if not found.
* @since 5.41
**/
QUrl fileUrl(const QByteArray &key, const QString &filename = QString()) const;
/**
* Get the list of files of a given type.
*
* @param fileType the type of file to look for, as defined in the
* package structure.
* @return list of files by name, suitable for passing to filePath
**/
QStringList entryList(const QByteArray &key) const;
/**
* @return true if the item at path exists and is required
**/
bool isRequired(const QByteArray &key) const;
/**
* @return the mimeTypes associated with the path, if any
**/
QStringList mimeTypes(const QByteArray &key) const;
/**
* @return the prefix paths inserted between the base path and content entries, in order of priority.
* When searching for a file, all paths will be tried in order.
*/
QStringList contentsPrefixPaths() const;
/**
* @return preferred package root. This defaults to kpackage/generic/
*/
QString defaultPackageRoot() const;
/**
* @return true if paths/symlinks outside the package itself should be followed.
* By default this is set to false for security reasons.
*/
bool allowExternalPaths() const;
/**
* Sets the metadata for the KPackage. This overwrites the current metadata.
* This should be used in case a kpackage gets loaded by name, based
* on the path a C++ plugin which has embedded metadata.
* @since 5.88
*/
void setMetadata(const KPluginMetaData &data);
/**
* @return the package metadata object.
*/
KPluginMetaData metadata() const;
/**
* @return a hash digest of the contents of the package in hexadecimal form
* @since 5.21
*/
QByteArray cryptographicHash(QCryptographicHash::Algorithm algorithm) const;
/**
* Adds a directory to the structure of the package. It is added as
* a not-required element with no associated mimeTypes.
* If an entry with the given key already exists, the path
* is added to it as a search alternative.
*
* @param key used as an internal label for this directory
* @param path the path within the package for this directory
*/
void addDirectoryDefinition(const QByteArray &key, const QString &path);
/**
* Adds a file to the structure of the package. It is added as
* a not-required element with no associated mimeTypes.
* If an entry with the given key already exists, the path
* is added to it as a search alternative.
*
* @param key used as an internal label for this file
* @param path the path within the package for this file
*/
void addFileDefinition(const QByteArray &key, const QString &path);
/**
* Removes a definition from the structure of the package.
* @param key the internal label of the file or directory to remove
*/
void removeDefinition(const QByteArray &key);
/**
* Sets whether or not a given part of the structure is required or not.
* The path must already have been added using addDirectoryDefinition
* or addFileDefinition.
*
* @param key the entry within the package
* @param required true if this entry is required, false if not
*/
void setRequired(const QByteArray &key, bool required);
/**
* Defines the default mimeTypes for any definitions that do not have
* associated mimeTypes. Handy for packages with only one or predominantly
* one file type.
*
* @param mimeTypes a list of mimeTypes
**/
void setDefaultMimeTypes(const QStringList &mimeTypes);
/**
* Define mimeTypes for a given part of the structure
* The path must already have been added using addDirectoryDefinition
* or addFileDefinition.
*
* @param key the entry within the package
* @param mimeTypes a list of mimeTypes
**/
void setMimeTypes(const QByteArray &key, const QStringList &mimeTypes);
/**
* Sets the prefixes that all the contents in this package should
* appear under. This defaults to "contents/" and is added automatically
* between the base path and the entries as defined by the package
* structure. Multiple entries can be added.
* In this case each file request will be searched in all prefixes in order,
* and the first found will be returned.
*
* @param prefix paths the directory prefix to use
*/
void setContentsPrefixPaths(const QStringList &prefixPaths);
/**
* Sets whether or not external paths/symlinks can be followed by a package
* @param allow true if paths/symlinks outside of the package should be followed,
* false if they should be rejected.
*/
void setAllowExternalPaths(bool allow);
/**
* Sets preferred package root.
*/
void setDefaultPackageRoot(const QString &packageRoot);
/**
* Sets the fallback package root path
* If a file won't be found in this package, it will search it in the package
* with the same structure identified by path
* It is intended to be used by the packageStructure
* @param path package root path @see setPath
*/
void setFallbackPackage(const KPackage::Package &package);
/**
* @return The fallback package root path
*/
KPackage::Package fallbackPackage() const;
// Content structure description methods
/**
* @return all directories registered as part of this Package's structure
*/
QList<QByteArray> directories() const;
/**
* @return all directories registered as part of this Package's required structure
*/
QList<QByteArray> requiredDirectories() const;
/**
* @return all files registered as part of this Package's structure
*/
QList<QByteArray> files() const;
/**
* @return all files registered as part of this Package's required structure
*/
QList<QByteArray> requiredFiles() const;
private:
QExplicitlySharedDataPointer<PackagePrivate> d;
friend class PackagePrivate;
};
}
Q_DECLARE_METATYPE(KPackage::Package)
#endif
@@ -0,0 +1,192 @@
/*
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "packagejob.h"
#include "config-package.h"
#include "packageloader.h"
#include "packagestructure.h"
#include "private/package_p.h"
#include "private/packagejobthread_p.h"
#include "private/utils.h"
#include "kpackage_debug.h"
#if HAVE_QTDBUS
#include <QDBusConnection>
#include <QDBusMessage>
#endif
#include <QDebug>
#include <QStandardPaths>
#include <QThreadPool>
#include <QTimer>
namespace KPackage
{
struct StructureOrErrorJob {
PackageStructure *structure = nullptr;
PackageJob *errorJob = nullptr;
};
class PackageJobPrivate
{
public:
static StructureOrErrorJob loadStructure(const QString &packageFormat)
{
if (auto structure = PackageLoader::self()->loadPackageStructure(packageFormat)) {
return StructureOrErrorJob{structure, nullptr};
} else {
auto job = new PackageJob(PackageJob::Install, Package(), QString(), QString());
job->setErrorText(QStringLiteral("Could not load package structure ") + packageFormat);
job->setError(PackageJob::JobError::InvalidPackageStructure);
QTimer::singleShot(0, job, [job]() {
job->emitResult();
});
return StructureOrErrorJob{nullptr, job};
}
}
PackageJobThread *thread = nullptr;
Package package;
QString installPath;
};
PackageJob::PackageJob(OperationType type, const Package &package, const QString &src, const QString &dest)
: KJob()
, d(new PackageJobPrivate)
{
d->thread = new PackageJobThread(type, src, dest, package);
d->package = package;
connect(d->thread, &PackageJobThread::installPathChanged, this, [this](const QString &installPath) {
d->package.setPath(installPath);
});
// setupNotificationsOnJobFinished connects to jobThreadFinished,
// don't connect to it again
if (type == Install) {
setupNotificationsOnJobFinished(QStringLiteral("packageInstalled"));
} else if (type == Update) {
setupNotificationsOnJobFinished(QStringLiteral("packageUpdated"));
d->thread->update(src, dest, package);
} else if (type == Uninstall) {
setupNotificationsOnJobFinished(QStringLiteral("packageUninstalled"));
} else {
Q_UNREACHABLE();
}
}
PackageJob::~PackageJob() = default;
void PackageJob::start()
{
if (d->thread) {
QThreadPool::globalInstance()->start(d->thread);
d->thread = nullptr;
} else {
qCWarning(KPACKAGE_LOG) << "The KPackage::PackageJob was already started";
}
}
PackageJob *PackageJob::install(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot)
{
auto structOrErr = PackageJobPrivate::loadStructure(packageFormat);
if (auto structure = structOrErr.structure) {
Package package(structure);
package.setPath(sourcePackage);
QString dest = packageRoot.isEmpty() ? package.defaultPackageRoot() : packageRoot;
PackageLoader::invalidateCache();
// use absolute paths if passed, otherwise go under share
if (!QDir::isAbsolutePath(dest)) {
dest = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + dest;
}
auto job = new PackageJob(Install, package, sourcePackage, dest);
job->start();
return job;
} else {
return structOrErr.errorJob;
}
}
PackageJob *PackageJob::update(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot)
{
auto structOrErr = PackageJobPrivate::loadStructure(packageFormat);
if (auto structure = structOrErr.structure) {
Package package(structure);
package.setPath(sourcePackage);
QString dest = packageRoot.isEmpty() ? package.defaultPackageRoot() : packageRoot;
PackageLoader::invalidateCache();
// use absolute paths if passed, otherwise go under share
if (!QDir::isAbsolutePath(dest)) {
dest = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + dest;
}
auto job = new PackageJob(Update, package, sourcePackage, dest);
job->start();
return job;
} else {
return structOrErr.errorJob;
}
}
PackageJob *PackageJob::uninstall(const QString &packageFormat, const QString &pluginId, const QString &packageRoot)
{
auto structOrErr = PackageJobPrivate::loadStructure(packageFormat);
if (auto structure = structOrErr.structure) {
Package package(structure);
QString uninstallPath;
// We handle the empty path when uninstalling the package
// If the dir already got deleted the pluginId is an empty string, without this
// check we would delete the package root, BUG: 410682
if (!pluginId.isEmpty()) {
uninstallPath = packageRoot + QLatin1Char('/') + pluginId;
}
package.setPath(uninstallPath);
PackageLoader::invalidateCache();
auto job = new PackageJob(Uninstall, package, QString(), QString());
job->start();
return job;
} else {
return structOrErr.errorJob;
}
}
KPackage::Package PackageJob::package() const
{
return d->package;
}
void PackageJob::setupNotificationsOnJobFinished(const QString &messageName)
{
// capture first as uninstalling wipes d->package
// or d-package can become dangling during the job if deleted externally
const QString pluginId = d->package.metadata().pluginId();
const QString kpackageType = readKPackageType(d->package.metadata());
auto onJobFinished = [=, this](bool ok, JobError errorCode, const QString &error) {
#if HAVE_QTDBUS
if (ok) {
auto msg = QDBusMessage::createSignal(QStringLiteral("/KPackage/") + kpackageType, QStringLiteral("org.kde.plasma.kpackage"), messageName);
msg.setArguments({pluginId});
QDBusConnection::sessionBus().send(msg);
}
#endif
if (ok) {
setError(NoError);
} else {
setError(errorCode);
setErrorText(error);
}
emitResult();
};
connect(d->thread, &PackageJobThread::jobThreadFinished, this, onJobFinished, Qt::QueuedConnection);
}
} // namespace KPackage
#include "moc_packagejob.cpp"
@@ -0,0 +1,79 @@
/*
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPACKAGE_PACKAGEJOB_H
#define KPACKAGE_PACKAGEJOB_H
#include <kpackage/package_export.h>
#include <KJob>
#include <memory>
namespace KPackage
{
class PackageJobPrivate;
class Package;
class PackageStructure;
/**
* @class PackageJob kpackage/packagejob.h <KPackage/PackageJob>
* @short KJob subclass that allows async install/update/uninstall operations for packages
*/
class KPACKAGE_EXPORT PackageJob : public KJob
{
Q_OBJECT
public:
/**
* Error codes for the install/update/remove jobs
*/
enum JobError {
InvalidPackageStructure = KJob::UserDefinedError + 1, /**< Could not find/load the given package structure */
RootCreationError, /**< Cannot create package root directory */
PackageFileNotFoundError, /**< The package file does not exist */
UnsupportedArchiveFormatError, /**< The archive format of the package is not supported */
PackageOpenError, /**< Can't open the package file for reading */
PluginIdInvalidError, /**< The plugin id is not specified in the metadata.json file or contains
characters different from letters, digits, dots and underscores */
UpdatePackageTypeMismatchError, /**< A package with this plugin id was already installed, but has a different type in the metadata.json file */
OldVersionRemovalError, /**< Failed to remove the old version of the package during an upgrade */
NewerVersionAlreadyInstalledError, /**< We tried to update, but the same version or a newer one is already installed */
PackageAlreadyInstalledError, /**< The package is already installed and a normal install (not update) was performed */
PackageMoveError, /**< Failure to move a package from the system temporary folder to its final destination */
PackageCopyError, /**< Failure to copy a package folder from somewhere in the filesystem to its final destination */
PackageUninstallError, /**< Failure to uninstall a package */
};
~PackageJob() override;
/// Installs the given package. The returned job is already started
static PackageJob *install(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot = QString());
/// Installs the given package. The returned job is already started
static PackageJob *update(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot = QString());
/// Installs the given package. The returned job is already started
static PackageJob *uninstall(const QString &packageFormat, const QString &pluginId, const QString &packageRoot = QString());
KPackage::Package package() const;
private:
friend class PackageJobThread;
enum OperationType {
Install,
Update,
Uninstall,
};
void start() override;
KPACKAGE_NO_EXPORT explicit PackageJob(OperationType type, const Package &package, const QString &src, const QString &dest);
KPACKAGE_NO_EXPORT void setupNotificationsOnJobFinished(const QString &messageName);
const std::unique_ptr<PackageJobPrivate> d;
friend PackageJobPrivate;
};
}
#endif
@@ -0,0 +1,291 @@
/*
SPDX-FileCopyrightText: 2010 Ryan Rix <ry@n.rix.si>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "packageloader.h"
#include "private/packageloader_p.h"
#include "private/utils.h"
#include "kpackage_debug.h"
#include <QCoreApplication>
#include <QDateTime>
#include <QDirIterator>
#include <QList>
#include <QStandardPaths>
#include <KLazyLocalizedString>
#include <KPluginFactory>
#include <KPluginMetaData>
#include <unordered_set>
#include "config-package.h"
#include "package.h"
#include "packagestructure.h"
#include "private/packagejobthread_p.h"
#include "private/packages_p.h"
namespace KPackage
{
PackageLoader::PackageLoader()
: d(new PackageLoaderPrivate)
{
}
PackageLoader::~PackageLoader()
{
for (auto wp : std::as_const(d->structures)) {
delete wp.data();
}
delete d;
}
PackageLoader *PackageLoader::self()
{
static PackageLoader *s_packageTrader = new PackageLoader;
return s_packageTrader;
}
Package PackageLoader::loadPackage(const QString &packageFormat, const QString &packagePath)
{
if (packageFormat.isEmpty()) {
return Package();
}
if (PackageStructure *structure = loadPackageStructure(packageFormat)) {
Package p(structure);
if (!packagePath.isEmpty()) {
p.setPath(packagePath);
}
return p;
}
return Package();
}
QList<Package> PackageLoader::listKPackages(const QString &packageFormat, const QString &packageRoot)
{
QList<Package> lst;
// has been a root specified?
QString actualRoot = packageRoot;
PackageStructure *structure = d->structures.value(packageFormat).data();
// try to take it from the package structure
if (actualRoot.isEmpty()) {
if (!structure) {
if (packageFormat == QLatin1String("KPackage/Generic")) {
structure = new GenericPackage();
} else if (packageFormat == QLatin1String("KPackage/GenericQML")) {
structure = new GenericQMLPackage();
} else {
structure = loadPackageStructure(packageFormat);
}
}
if (structure) {
d->structures.insert(packageFormat, structure);
actualRoot = Package(structure).defaultPackageRoot();
}
}
if (actualRoot.isEmpty()) {
actualRoot = packageFormat;
}
QStringList paths;
if (QDir::isAbsolutePath(actualRoot)) {
paths = QStringList(actualRoot);
} else {
const auto listPath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
for (const QString &path : listPath) {
paths += path + QLatin1Char('/') + actualRoot;
}
}
for (auto const &plugindir : std::as_const(paths)) {
QDirIterator it(plugindir, QDir::Dirs | QDir::NoDotAndDotDot);
std::unordered_set<QString> dirs;
while (it.hasNext()) {
it.next();
const QString dir = it.filePath();
if (!dirs.insert(it.fileInfo().fileName()).second) {
continue;
}
Package package(structure);
package.setPath(dir);
if (package.isValid()) {
// Ignore packages with empty metadata here
if (packageFormat.isEmpty() || !package.metadata().isValid() || readKPackageType(package.metadata()) == packageFormat) {
lst << package;
} else {
qInfo() << "KPackage in" << package.path() << readKPackageType(package.metadata()) << "does not match requested format" << packageFormat;
}
}
}
}
return lst;
}
QList<KPluginMetaData> PackageLoader::listPackages(const QString &packageFormat, const QString &packageRoot)
{
// Note: Use QDateTime::currentSecsSinceEpoch() once we can depend on Qt 5.8
const qint64 now = qRound64(QDateTime::currentMSecsSinceEpoch() / 1000.0);
bool useRuntimeCache = true;
if (now - d->pluginCacheAge > d->maxCacheAge && d->pluginCacheAge != 0) {
// cache is old and we're not within a few seconds of startup anymore
useRuntimeCache = false;
d->pluginCache.clear();
}
const QString cacheKey = packageFormat + QLatin1Char('.') + packageRoot;
if (useRuntimeCache) {
auto it = d->pluginCache.constFind(cacheKey);
if (it != d->pluginCache.constEnd()) {
return *it;
}
}
if (d->pluginCacheAge == 0) {
d->pluginCacheAge = now;
}
QList<KPluginMetaData> lst;
// has been a root specified?
QString actualRoot = packageRoot;
// try to take it from the package structure
if (actualRoot.isEmpty()) {
PackageStructure *structure = d->structures.value(packageFormat).data();
if (!structure) {
if (packageFormat == QLatin1String("KPackage/Generic")) {
structure = new GenericPackage();
} else if (packageFormat == QLatin1String("KPackage/GenericQML")) {
structure = new GenericQMLPackage();
} else {
structure = loadPackageStructure(packageFormat);
}
}
if (structure) {
d->structures.insert(packageFormat, structure);
Package p(structure);
actualRoot = p.defaultPackageRoot();
}
}
if (actualRoot.isEmpty()) {
actualRoot = packageFormat;
}
QSet<QString> uniqueIds;
QStringList paths;
if (QDir::isAbsolutePath(actualRoot)) {
paths = QStringList(actualRoot);
} else {
const auto listPath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
for (const QString &path : listPath) {
paths += path + QLatin1Char('/') + actualRoot;
}
}
for (auto const &plugindir : std::as_const(paths)) {
QDirIterator it(plugindir, QStringList{QStringLiteral("metadata.json")}, QDir::Files, QDirIterator::Subdirectories);
std::unordered_set<QString> dirs;
while (it.hasNext()) {
it.next();
const QString dir = it.fileInfo().absoluteDir().path();
if (!dirs.insert(dir).second) {
continue;
}
const QString metadataPath = it.fileInfo().absoluteFilePath();
KPluginMetaData info = KPluginMetaData::fromJsonFile(metadataPath);
if (!info.isValid() || uniqueIds.contains(info.pluginId())) {
continue;
}
if (packageFormat.isEmpty() || readKPackageType(info) == packageFormat) {
uniqueIds << info.pluginId();
lst << info;
} else {
qInfo() << "KPackageStructure of" << info << "does not match requested format" << packageFormat;
}
}
}
if (useRuntimeCache) {
d->pluginCache.insert(cacheKey, lst);
}
return lst;
}
QList<KPluginMetaData> PackageLoader::listPackagesMetadata(const QString &packageFormat, const QString &packageRoot)
{
return listPackages(packageFormat, packageRoot);
}
QList<KPluginMetaData>
PackageLoader::findPackages(const QString &packageFormat, const QString &packageRoot, std::function<bool(const KPluginMetaData &)> filter)
{
QList<KPluginMetaData> lst;
const auto lstPlugins = listPackages(packageFormat, packageRoot);
for (auto const &plugin : lstPlugins) {
if (!filter || filter(plugin)) {
lst << plugin;
}
}
return lst;
}
KPackage::PackageStructure *PackageLoader::loadPackageStructure(const QString &packageFormat)
{
PackageStructure *structure = d->structures.value(packageFormat).data();
if (!structure) {
if (packageFormat == QLatin1String("KPackage/Generic")) {
structure = new GenericPackage();
d->structures.insert(packageFormat, structure);
} else if (packageFormat == QLatin1String("KPackage/GenericQML")) {
structure = new GenericQMLPackage();
d->structures.insert(packageFormat, structure);
}
}
if (structure) {
return structure;
}
const KPluginMetaData metaData = structureForKPackageType(packageFormat);
if (!metaData.isValid()) {
qCWarning(KPACKAGE_LOG) << "Invalid metadata for package structure" << packageFormat;
return nullptr;
}
auto result = KPluginFactory::instantiatePlugin<PackageStructure>(metaData);
if (!result) {
qCWarning(KPACKAGE_LOG).noquote() << "Could not load installer for package of type" << packageFormat << "Error reported was: " << result.errorString;
return nullptr;
}
structure = result.plugin;
d->structures.insert(packageFormat, structure);
return structure;
}
void PackageLoader::addKnownPackageStructure(const QString &packageFormat, KPackage::PackageStructure *structure)
{
d->structures.insert(packageFormat, structure);
}
void PackageLoader::invalidateCache()
{
self()->d->maxCacheAge = -1;
}
} // KPackage Namespace
@@ -0,0 +1,132 @@
/*
SPDX-FileCopyrightText: 2010 Ryan Rix <ry@n.rix.si>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPACKAGE_LOADER_H
#define KPACKAGE_LOADER_H
#include <kpackage/package.h>
#include <kpackage/package_export.h>
namespace KPackage
{
class PackageLoaderPrivate;
/**
* @class PackageLoader kpackage/packageloader.h <KPackage/PackageLoader>
*
* This is an abstract base class which defines an interface to which the package
* loading logic can communicate with a parent application. The plugin loader
* must be set before any plugins are loaded, otherwise (for safety reasons), the
* default PackageLoader implementation will be used. The reimplemented version should
* not do more than simply returning a loaded plugin. It should not init() it, and it should not
* hang on to it.
*
* @author Ryan Rix <ry@n.rix.si>
**/
class KPACKAGE_EXPORT PackageLoader
{
public:
/**
* Load a Package plugin.
*
* @param packageFormat the format of the package to load
* @param packagePath the package name: the path of the package relative to the
* packageFormat root path. If not specified it will have to be set manually
* with Package::setPath() by the caller.
*
* @return a Package object matching name, or an invalid package on failure
**/
Package loadPackage(const QString &packageFormat, const QString &packagePath = QString());
/**
* List all available packages of a certain type
*
* @param packageFormat the format of the packages to list
* @param packageRoot the root folder where the packages are installed.
* If not specified the default from the packageformat will be taken.
*
* @return metadata for all the matching packages
*/
QList<KPluginMetaData> listPackages(const QString &packageFormat, const QString &packageRoot = QString());
/**
* @overload
* @since 6.0
*/
QList<KPluginMetaData> listPackagesMetadata(const QString &packageFormat, const QString &packageRoot = QString());
/**
* List all available packages of a certain type. This should be used in case the package structure modifies the metadata or you need to access the
* contained files of the package.
*
* @param packageFormat the format of the packages to list
* @param packageRoot the root folder where the packages are installed.
* If not specified the default from the packageformat will be taken.
*
* @since 6.0
*/
QList<Package> listKPackages(const QString &packageFormat, const QString &packageRoot = QString());
/**
* List package of a certain type that match a certain filter function
*
* @param packageFormat the format of the packages to list
* @param packageRoot the root folder where the packages are installed.
* If not specified the default from the packageformat will be taken.
* @param filter a filter function that will be called on each package:
* will return true for the matching ones
*
* @return metadata for all the matching packages
* @since 5.10
*/
QList<KPluginMetaData> findPackages(const QString &packageFormat,
const QString &packageRoot = QString(),
std::function<bool(const KPluginMetaData &)> filter = std::function<bool(const KPluginMetaData &)>());
/**
* Loads a PackageStructure for a given format. The structure can then be used as
* paramenter for a Package instance constructor
*
* @note The returned pointer is managed by KPackage, and should never be deleted
*
* @param packageFormat the package format, such as "KPackage/GenericQML"
* @return the structure instance (ownership retained by KPackage)
*/
KPackage::PackageStructure *loadPackageStructure(const QString &packageFormat);
/**
* Adds a new known package structure that can be used by the functions to load packages such
* as loadPackage, findPackages etc
* @param packageFormat the package format, such as "KPackage/GenericQML"
* @param structure the package structure we want to be able to load packages from
* @since 5.10
*/
void addKnownPackageStructure(const QString &packageFormat, KPackage::PackageStructure *structure);
/**
* Return the active plugin loader
**/
static PackageLoader *self();
protected:
PackageLoader();
virtual ~PackageLoader();
private:
friend class Package;
friend class PackageJob;
KPACKAGE_NO_EXPORT static void invalidateCache();
PackageLoaderPrivate *const d;
Q_DISABLE_COPY(PackageLoader)
};
}
Q_DECLARE_METATYPE(KPackage::PackageLoader *)
#endif
@@ -0,0 +1,34 @@
/*
SPDX-FileCopyrightText: 2011 Aaron Seigo <aseigo@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "packagestructure.h"
#include "kpackage_debug.h"
#include "packagejob.h"
#include "private/package_p.h"
namespace KPackage
{
PackageStructure::PackageStructure(QObject *parent, const QVariantList & /*args*/)
: QObject(parent)
{
Q_UNUSED(d)
}
PackageStructure::~PackageStructure()
{
}
void PackageStructure::initPackage(Package * /*package*/)
{
}
void PackageStructure::pathChanged(Package * /*package*/)
{
}
}
#include "moc_packagestructure.cpp"
@@ -0,0 +1,71 @@
/*
SPDX-FileCopyrightText: 2011 Aaron Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPACKAGE_PACKAGESTRUCTURE_H
#define KPACKAGE_PACKAGESTRUCTURE_H
#include <QStringList>
#include <KPluginFactory>
#include <kpackage/package.h>
#include <kpackage/package_export.h>
namespace KPackage
{
/**
* @class PackageStructure kpackage/packagestructure.h <KPackage/PackageStructure>
*
* This class is used to define the filesystem structure of a package type.
* A PackageStructure is implemented as a dynamically loaded plugin, in the reimplementation
* of initPackage the allowed fines and directories in the package are set into the package,
* for instance:
*
* @code
* package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml"));
* package->setDefaultPackageRoot(QStringLiteral("plasma/wallpapers/"));
* package->addDirectoryDefinition("images", QStringLiteral("images"));
* package->addDirectoryDefinition("theme", QStringLiteral("theme"));
* QStringList mimetypes{QStringLiteral("image/svg+xml"), QStringLiteral("image/png"), QStringLiteral("image/jpeg")};
* package->setMimeTypes("images", mimetypes);
* @endcode
*/
class KPACKAGE_EXPORT PackageStructure : public QObject
{
Q_OBJECT
public:
explicit PackageStructure(QObject *parent = nullptr, const QVariantList &args = QVariantList());
~PackageStructure() override;
/**
* Called when a the PackageStructure should initialize a Package with the initial
* structure. This allows setting paths before setPath is called.
*
* Note: one special value is "metadata" which can be set to the location of KPluginMetaData
* compatible .json file within the package. If not defined, it is assumed that this file
* exists under the top level directory of the package.
*
* @param package the Package to set up. The object is empty of all definition when
* first passed in.
*/
virtual void initPackage(Package *package);
/**
* Called whenever the path changes so that subclasses may take
* package specific actions.
*/
virtual void pathChanged(Package *package);
private:
void *d;
};
} // KPackage namespace
#endif
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
// SPDX-License-Identifier: LGPL-2.0-or-later
#ifndef KPACKAGE_PACKAGESTRUCTURE_COMPAT_P_H
#define KPACKAGE_PACKAGESTRUCTURE_COMPAT_P_H
#include <KPackage/Package>
#include <QMap>
class KConfigGroup;
class KDesktopFile;
namespace KPackagePrivate
{
template<typename DesktopFile = KDesktopFile, typename ConfigGroup = KConfigGroup>
/**
* @param package KPackage which will have the desktop file metadata set to (if present)
* @param customValueTypes Additional keys and their types that should be read from the desktop file
*/
void convertCompatMetaDataDesktopFile(KPackage::Package *package, const QMap<QString, QMetaType::Type> &customValueTypes = {})
{
if (const QString legacyPath = package->filePath("metadata"); !legacyPath.isEmpty() && legacyPath.endsWith(QLatin1String(".desktop"))) {
DesktopFile file(legacyPath);
const ConfigGroup grp = file.desktopGroup();
QJsonObject kplugin{
{QLatin1String("Name"), grp.readEntry("Name")},
{QLatin1String("Description"), grp.readEntry("Comment")},
{QLatin1String("Version"), grp.readEntry("X-KDE-PluginInfo-Version")},
{QLatin1String("Id"), grp.readEntry("X-KDE-PluginInfo-Name")},
};
QJsonObject obj{
{QLatin1String("KPlugin"), kplugin},
{QLatin1String("KPackageStructure"), grp.readEntry("X-KDE-ServiceTypes")},
};
for (auto it = customValueTypes.begin(), end = customValueTypes.end(); it != end; ++it) {
if (const QString value = grp.readEntry(it.key()); !value.isEmpty()) {
if (QVariant variant(value); variant.convert(QMetaType(it.value()))) { // Make sure the type in resulting json is what the API caller needs
obj.insert(it.key(), QJsonValue::fromVariant(variant));
}
}
}
package->setMetadata(KPluginMetaData(obj, legacyPath));
}
}
};
#endif
@@ -0,0 +1,77 @@
/*
SPDX-FileCopyrightText: 2009 Rob Scheepmaker <r.scheepmaker@student.utwente.nl>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPACKAGE_PACKAGE_P_H
#define KPACKAGE_PACKAGE_P_H
#include "../package.h"
#include <QCryptographicHash>
#include <QDir>
#include <QHash>
#include <QPointer>
#include <QSharedData>
#include <QString>
#include <optional>
namespace KPackage
{
class ContentStructure
{
public:
ContentStructure()
{
}
ContentStructure(const ContentStructure &other)
{
paths = other.paths;
mimeTypes = other.mimeTypes;
directory = other.directory;
required = other.required;
}
ContentStructure &operator=(const ContentStructure &) = default;
QStringList paths;
QStringList mimeTypes;
bool directory = false;
bool required = false;
};
class PackagePrivate : public QSharedData
{
public:
PackagePrivate();
PackagePrivate(const PackagePrivate &other);
~PackagePrivate();
PackagePrivate &operator=(const PackagePrivate &rhs);
void createPackageMetadata(const QString &path);
QString unpack(const QString &filePath);
void updateHash(const QString &basePath, const QString &subPath, const QDir &dir, QCryptographicHash &hash);
QString fallbackFilePath(const QByteArray &key, const QString &filename = QString()) const;
bool hasCycle(const KPackage::Package &package);
bool isInsidePackageDir(const QString &canonicalPath) const;
QPointer<PackageStructure> structure;
QString path;
QString tempRoot;
QStringList contentsPrefixPaths;
QString defaultPackageRoot;
QHash<QString, QString> discoveries;
QHash<QByteArray, ContentStructure> contents;
std::unique_ptr<Package> fallbackPackage;
QStringList mimeTypes;
std::optional<KPluginMetaData> metadata;
bool externalPaths = false;
bool valid = false;
bool checkedValid = false;
};
}
#endif
@@ -0,0 +1,403 @@
/*
SPDX-FileCopyrightText: 2007-2009 Aaron Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "private/packagejobthread_p.h"
#include "private/utils.h"
#include "config-package.h"
#include "package.h"
#include <KArchive>
#include <KLocalizedString>
#include <KTar>
#include <kzip.h>
#include "kpackage_debug.h"
#include <QDir>
#include <QFile>
#include <QIODevice>
#include <QJsonDocument>
#include <QMimeDatabase>
#include <QMimeType>
#include <QProcess>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QUrl>
#include <qtemporarydir.h>
namespace KPackage
{
bool copyFolder(QString sourcePath, QString targetPath)
{
QDir source(sourcePath);
if (!source.exists()) {
return false;
}
QDir target(targetPath);
if (!target.exists()) {
QString targetName = target.dirName();
target.cdUp();
target.mkdir(targetName);
target = QDir(targetPath);
}
const auto lstEntries = source.entryList(QDir::Files);
for (const QString &fileName : lstEntries) {
QString sourceFilePath = sourcePath + QDir::separator() + fileName;
QString targetFilePath = targetPath + QDir::separator() + fileName;
if (!QFile::copy(sourceFilePath, targetFilePath)) {
return false;
}
}
const auto lstEntries2 = source.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
for (const QString &subFolderName : lstEntries2) {
QString sourceSubFolderPath = sourcePath + QDir::separator() + subFolderName;
QString targetSubFolderPath = targetPath + QDir::separator() + subFolderName;
if (!copyFolder(sourceSubFolderPath, targetSubFolderPath)) {
return false;
}
}
return true;
}
bool removeFolder(QString folderPath)
{
QDir folder(folderPath);
return folder.removeRecursively();
}
class PackageJobThreadPrivate
{
public:
QString installPath;
QString errorMessage;
std::function<void()> run;
int errorCode;
};
PackageJobThread::PackageJobThread(PackageJob::OperationType type, const QString &src, const QString &dest, const KPackage::Package &package)
: QObject()
, QRunnable()
{
d = new PackageJobThreadPrivate;
d->errorCode = KJob::NoError;
if (type == PackageJob::Install) {
d->run = [this, src, dest, package]() {
install(src, dest, package);
};
} else if (type == PackageJob::Update) {
d->run = [this, src, dest, package]() {
update(src, dest, package);
};
} else if (type == PackageJob::Uninstall) {
const QString packagePath = package.path();
d->run = [this, packagePath]() {
uninstall(packagePath);
};
} else {
Q_UNREACHABLE();
}
}
PackageJobThread::~PackageJobThread()
{
delete d;
}
void PackageJobThread::run()
{
Q_ASSERT(d->run);
d->run();
}
bool PackageJobThread::install(const QString &src, const QString &dest, const Package &package)
{
bool ok = installPackage(src, dest, package, PackageJob::Install);
Q_EMIT installPathChanged(d->installPath);
Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
return ok;
}
static QString resolveHandler(const QString &scheme)
{
QString envOverride = qEnvironmentVariable("KPACKAGE_DEP_RESOLVERS_PATH");
QStringList searchDirs;
if (!envOverride.isEmpty()) {
searchDirs.push_back(envOverride);
}
searchDirs.append(QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kpackagehandlers"));
// We have to use QStandardPaths::findExecutable here to handle the .exe suffix on Windows.
return QStandardPaths::findExecutable(scheme + QLatin1String("handler"), searchDirs);
}
bool PackageJobThread::installDependency(const QUrl &destUrl)
{
auto handler = resolveHandler(destUrl.scheme());
if (handler.isEmpty()) {
return false;
}
QProcess process;
process.setProgram(handler);
process.setArguments({destUrl.toString()});
process.setProcessChannelMode(QProcess::ForwardedChannels);
process.start();
process.waitForFinished();
return process.exitCode() == 0;
}
bool PackageJobThread::installPackage(const QString &src, const QString &dest, const Package &package, PackageJob::OperationType operation)
{
QDir root(dest);
if (!root.exists()) {
QDir().mkpath(dest);
if (!root.exists()) {
d->errorMessage = i18n("Could not create package root directory: %1", dest);
d->errorCode = PackageJob::JobError::RootCreationError;
// qCWarning(KPACKAGE_LOG) << "Could not create package root directory: " << dest;
return false;
}
}
QFileInfo fileInfo(src);
if (!fileInfo.exists()) {
d->errorMessage = i18n("No such file: %1", src);
d->errorCode = PackageJob::JobError::PackageFileNotFoundError;
return false;
}
QString path;
QTemporaryDir tempdir;
bool archivedPackage = false;
if (fileInfo.isDir()) {
// we have a directory, so let's just install what is in there
path = src;
// make sure we end in a slash!
if (!path.endsWith(QLatin1Char('/'))) {
path.append(QLatin1Char('/'));
}
} else {
KArchive *archive = nullptr;
QMimeDatabase db;
QMimeType mimetype = db.mimeTypeForFile(src);
if (mimetype.inherits(QStringLiteral("application/zip"))) {
archive = new KZip(src);
} else if (mimetype.inherits(QStringLiteral("application/x-compressed-tar")) || //
mimetype.inherits(QStringLiteral("application/x-tar")) || //
mimetype.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || //
mimetype.inherits(QStringLiteral("application/x-xz")) || //
mimetype.inherits(QStringLiteral("application/x-lzma"))) {
archive = new KTar(src);
} else {
// qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << src << mimetype.name();
d->errorMessage = i18n("Could not open package file, unsupported archive format: %1 %2", src, mimetype.name());
d->errorCode = PackageJob::JobError::UnsupportedArchiveFormatError;
return false;
}
if (!archive->open(QIODevice::ReadOnly)) {
// qCWarning(KPACKAGE_LOG) << "Could not open package file:" << src;
delete archive;
d->errorMessage = i18n("Could not open package file: %1", src);
d->errorCode = PackageJob::JobError::PackageOpenError;
return false;
}
archivedPackage = true;
path = tempdir.path() + QLatin1Char('/');
d->installPath = path;
const KArchiveDirectory *source = archive->directory();
source->copyTo(path);
QStringList entries = source->entries();
if (entries.count() == 1) {
const KArchiveEntry *entry = source->entry(entries[0]);
if (entry->isDirectory()) {
path = path + entry->name() + QLatin1Char('/');
}
}
delete archive;
}
Package copyPackage = package;
copyPackage.setPath(path);
if (!copyPackage.isValid()) {
d->errorMessage = i18n("Package is not considered valid");
d->errorCode = PackageJob::JobError::InvalidPackageStructure;
return false;
}
KPluginMetaData meta = copyPackage.metadata(); // The packagestructure might have set the metadata, so use that
QString pluginName = meta.pluginId().isEmpty() ? QFileInfo(src).baseName() : meta.pluginId();
qCDebug(KPACKAGE_LOG) << "pluginname: " << meta.pluginId();
if (pluginName == QLatin1String("metadata")) {
// qCWarning(KPACKAGE_LOG) << "Package plugin id not specified";
d->errorMessage = i18n("Package plugin id not specified: %1", src);
d->errorCode = PackageJob::JobError::PluginIdInvalidError;
return false;
}
// Ensure that package names are safe so package uninstall can't inject
// bad characters into the paths used for removal.
const QRegularExpression validatePluginName(QStringLiteral("^[\\w\\-\\.]+$")); // Only allow letters, numbers, underscore and period.
if (!validatePluginName.match(pluginName).hasMatch()) {
// qCDebug(KPACKAGE_LOG) << "Package plugin id " << pluginName << "contains invalid characters";
d->errorMessage = i18n("Package plugin id %1 contains invalid characters", pluginName);
d->errorCode = PackageJob::JobError::PluginIdInvalidError;
return false;
}
QString targetName = dest;
if (targetName[targetName.size() - 1] != QLatin1Char('/')) {
targetName.append(QLatin1Char('/'));
}
targetName.append(pluginName);
if (QFile::exists(targetName)) {
if (operation == PackageJob::Update) {
KPluginMetaData oldMeta;
if (QString jsonPath = targetName + QLatin1String("/metadata.json"); QFileInfo::exists(jsonPath)) {
oldMeta = KPluginMetaData::fromJsonFile(jsonPath);
}
if (readKPackageType(oldMeta) != readKPackageType(meta)) {
d->errorMessage = i18n("The new package has a different type from the old version already installed.");
d->errorCode = PackageJob::JobError::UpdatePackageTypeMismatchError;
} else if (isVersionNewer(oldMeta.version(), meta.version())) {
const bool ok = uninstallPackage(targetName);
if (!ok) {
d->errorMessage = i18n("Impossible to remove the old installation of %1 located at %2. error: %3", pluginName, targetName, d->errorMessage);
d->errorCode = PackageJob::JobError::OldVersionRemovalError;
}
} else {
d->errorMessage = i18n("Not installing version %1 of %2. Version %3 already installed.", meta.version(), meta.pluginId(), oldMeta.version());
d->errorCode = PackageJob::JobError::NewerVersionAlreadyInstalledError;
}
} else {
d->errorMessage = i18n("%1 already exists", targetName);
d->errorCode = PackageJob::JobError::PackageAlreadyInstalledError;
}
if (d->errorCode != KJob::NoError) {
d->installPath = targetName;
return false;
}
}
// install dependencies
const QStringList optionalDependencies{QStringLiteral("sddmtheme.knsrc")};
const QStringList dependencies = meta.value(QStringLiteral("X-KPackage-Dependencies"), QStringList());
for (const QString &dep : dependencies) {
QUrl depUrl(dep);
const QString knsrcFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("knsrcfiles/") + depUrl.host());
if (knsrcFilePath.isEmpty() && optionalDependencies.contains(depUrl.host())) {
qWarning() << "Skipping depdendency due to knsrc files being missing" << depUrl;
continue;
}
if (!installDependency(depUrl)) {
d->errorMessage = i18n("Could not install dependency: '%1'", dep);
d->errorCode = PackageJob::JobError::PackageCopyError;
return false;
}
}
if (archivedPackage) {
// it's in a temp dir, so just move it over.
const bool ok = copyFolder(path, targetName);
removeFolder(path);
if (!ok) {
// qCWarning(KPACKAGE_LOG) << "Could not move package to destination:" << targetName;
d->errorMessage = i18n("Could not move package to destination: %1", targetName);
d->errorCode = PackageJob::JobError::PackageMoveError;
return false;
}
} else {
// it's a directory containing the stuff, so copy the contents rather
// than move them
const bool ok = copyFolder(path, targetName);
if (!ok) {
// qCWarning(KPACKAGE_LOG) << "Could not copy package to destination:" << targetName;
d->errorMessage = i18n("Could not copy package to destination: %1", targetName);
d->errorCode = PackageJob::JobError::PackageCopyError;
return false;
}
}
if (archivedPackage) {
// no need to remove the temp dir (which has been successfully moved if it's an archive)
tempdir.setAutoRemove(false);
}
d->installPath = targetName;
return true;
}
bool PackageJobThread::update(const QString &src, const QString &dest, const Package &package)
{
bool ok = installPackage(src, dest, package, PackageJob::Update);
Q_EMIT installPathChanged(d->installPath);
Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
return ok;
}
bool PackageJobThread::uninstall(const QString &packagePath)
{
bool ok = uninstallPackage(packagePath);
// Do not emit the install path changed, information about the removed package might be useful for consumers
// qCDebug(KPACKAGE_LOG) << "Thread: installFinished" << ok;
Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
return ok;
}
bool PackageJobThread::uninstallPackage(const QString &packagePath)
{
if (!QFile::exists(packagePath)) {
d->errorMessage = packagePath.isEmpty() ? i18n("package path was deleted manually") : i18n("%1 does not exist", packagePath);
d->errorCode = PackageJob::JobError::PackageFileNotFoundError;
return false;
}
QString pkg;
QString root;
{
// TODO KF6 remove, pass in packageroot, type and pluginName separately?
QStringList ps = packagePath.split(QLatin1Char('/'));
int ix = ps.count() - 1;
if (packagePath.endsWith(QLatin1Char('/'))) {
ix = ps.count() - 2;
}
pkg = ps[ix];
ps.pop_back();
root = ps.join(QLatin1Char('/'));
}
bool ok = removeFolder(packagePath);
if (!ok) {
d->errorMessage = i18n("Could not delete package from: %1", packagePath);
d->errorCode = PackageJob::JobError::PackageUninstallError;
return false;
}
return true;
}
PackageJob::JobError PackageJobThread::errorCode() const
{
return static_cast<PackageJob::JobError>(d->errorCode);
}
} // namespace KPackage
#include "moc_packagejobthread_p.cpp"
@@ -0,0 +1,53 @@
/*
SPDX-FileCopyrightText: 2007-2009 Aaron Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPACKAGE_PACKAGEJOBTHREAD_P_H
#define KPACKAGE_PACKAGEJOBTHREAD_P_H
#include "package.h"
#include "packagejob.h"
#include <QRunnable>
namespace KPackage
{
class PackageJobThreadPrivate;
bool indexDirectory(const QString &dir, const QString &dest);
class PackageJobThread : public QObject, public QRunnable
{
Q_OBJECT
public:
explicit PackageJobThread(PackageJob::OperationType type, const QString &src, const QString &dest, const KPackage::Package &package);
~PackageJobThread() override;
void run() override;
bool install(const QString &src, const QString &dest, const Package &package);
bool update(const QString &src, const QString &dest, const Package &package);
bool uninstall(const QString &packagePath);
PackageJob::JobError errorCode() const;
Q_SIGNALS:
void jobThreadFinished(bool success, PackageJob::JobError errorCode, const QString &errorMessage = QString());
void percentChanged(int percent);
void error(const QString &errorMessage);
void installPathChanged(const QString &installPath);
private:
// OperationType says whether we want to install, update or any
// new similar operation it will be expanded
bool installDependency(const QUrl &src);
bool installPackage(const QString &src, const QString &dest, const Package &package, PackageJob::OperationType operation);
bool uninstallPackage(const QString &packagePath);
PackageJobThreadPrivate *d;
};
}
#endif
@@ -0,0 +1,34 @@
/*
SPDX-FileCopyrightText: 2010 Ryan Rix <ry@n.rix.si>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KPACKAGE_PACKAGELOADER_P_H
#define KPACKAGE_PACKAGELOADER_P_H
#include "packagestructure.h"
#include <KPluginMetaData>
#include <QHash>
#include <QPointer>
namespace KPackage
{
class PackageLoaderPrivate
{
public:
QHash<QString, QPointer<PackageStructure>> structures;
// We only use this cache during start of the process to speed up many consecutive calls
// After that, we're too afraid to produce race conditions and it's not that time-critical anyway
// the 20 seconds here means that the cache is only used within 20sec during startup, after that,
// complexity goes up and we'd have to update the cache in order to avoid subtle bugs
// just not using the cache is way easier then, since it doesn't make *that* much of a difference,
// anyway
int maxCacheAge = 20;
qint64 pluginCacheAge = 0;
QHash<QString, QList<KPluginMetaData>> pluginCache;
};
}
#endif
@@ -0,0 +1,49 @@
/*
SPDX-FileCopyrightText: 2007-2009 Aaron Seigo <aseigo@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "packages_p.h"
#include <math.h>
#include <KLocalizedString>
#include "kpackage/package.h"
void GenericPackage::initPackage(KPackage::Package *package)
{
KPackage::PackageStructure::initPackage(package);
package->setDefaultPackageRoot(QStringLiteral("kpackage/generic/"));
package->addDirectoryDefinition("images", QStringLiteral("images"));
package->addDirectoryDefinition("theme", QStringLiteral("theme"));
const QStringList mimetypes{QStringLiteral("image/svg+xml"), QStringLiteral("image/png"), QStringLiteral("image/jpeg")};
package->setMimeTypes("images", mimetypes);
package->setMimeTypes("theme", mimetypes);
package->addDirectoryDefinition("config", QStringLiteral("config"));
package->setMimeTypes("config", QStringList{QStringLiteral("text/xml")});
package->addDirectoryDefinition("ui", QStringLiteral("ui"));
package->addDirectoryDefinition("data", QStringLiteral("data"));
package->addDirectoryDefinition("scripts", QStringLiteral("code"));
package->setMimeTypes("scripts", QStringList{QStringLiteral("text/plain")});
package->addDirectoryDefinition("translations", QStringLiteral("locale"));
}
void GenericQMLPackage::initPackage(KPackage::Package *package)
{
GenericPackage::initPackage(package);
package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml"));
package->setRequired("mainscript", true);
package->setDefaultPackageRoot(QStringLiteral("kpackage/genericqml/"));
}
#include "moc_packages_p.cpp"
@@ -0,0 +1,23 @@
/*
SPDX-FileCopyrightText: 2007 Aaron Seigo <aseigo@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#pragma once
#include "kpackage/packagestructure.h"
class GenericPackage : public KPackage::PackageStructure
{
Q_OBJECT
public:
void initPackage(KPackage::Package *package) override;
};
class GenericQMLPackage : public GenericPackage
{
Q_OBJECT
public:
void initPackage(KPackage::Package *package) override;
};
@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
// SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
// SPDX-License-Identifier: LGPL-2.1-or-later
#pragma once
#include "kpackage_debug.h"
#include <KPluginMetaData>
#include <QString>
#include <QVersionNumber>
inline QString readKPackageType(const KPluginMetaData &metaData)
{
return metaData.value(QStringLiteral("KPackageStructure"));
}
inline KPluginMetaData structureForKPackageType(const QString &packageFormat)
{
const QString guessedPath = QStringLiteral("kf6/packagestructure/") + QString(packageFormat).toLower().replace(QLatin1Char('/'), QLatin1Char('_'));
KPluginMetaData guessedData(guessedPath);
if (guessedData.isValid() && readKPackageType(guessedData) == packageFormat) {
return guessedData;
}
qCDebug(KPACKAGE_LOG) << "Could not find package structure for" << packageFormat << "by plugin path. The guessed path was" << guessedPath;
const QList<KPluginMetaData> plugins =
KPluginMetaData::findPlugins(QStringLiteral("kf6/packagestructure"), [packageFormat](const KPluginMetaData &metaData) {
return readKPackageType(metaData) == packageFormat;
});
return plugins.isEmpty() ? KPluginMetaData() : plugins.first();
}
inline bool isVersionNewer(const QString &version1, const QString &version2)
{
const auto v1 = QVersionNumber::fromString(version1);
const auto v2 = QVersionNumber::fromString(version2);
return v2 > v1;
}
@@ -0,0 +1,16 @@
add_executable(kpackagetool6)
ecm_mark_nongui_executable(kpackagetool6)
target_sources(kpackagetool6 PRIVATE
main.cpp
kpackagetool.cpp
)
ecm_qt_declare_logging_category(kpackagetool6
HEADER kpackage_debug.h
IDENTIFIER KPACKAGE_LOG
CATEGORY_NAME kf.package
)
target_link_libraries(kpackagetool6 KF6::Archive KF6::Package KF6::I18n KF6::CoreAddons)
install(TARGETS kpackagetool6 EXPORT KF6PackageToolsTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
@@ -0,0 +1,558 @@
/*
SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2012-2017 Sebastian Kügler <sebas@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "kpackagetool.h"
#include <KAboutData>
#include <KLocalizedString>
#include <KShell>
#include <QDebug>
#include <KJob>
#include <kpackage/package.h>
#include <kpackage/packageloader.h>
#include <kpackage/packagestructure.h>
#include <kpackage/private/utils.h>
#include <QCommandLineParser>
#include <QDir>
#include <QFileInfo>
#include <QList>
#include <QMap>
#include <QRegularExpression>
#include <QStandardPaths>
#include <QStringList>
#include <QTimer>
#include <QUrl>
#include <QXmlStreamWriter>
#include <iomanip>
#include <iostream>
#include "options.h"
#include "../kpackage/config-package.h"
#include "kpackage_debug.h"
Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cout, (stdout))
Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cerr, (stderr))
namespace KPackage
{
class PackageToolPrivate
{
public:
QString packageRoot;
QString packageFile;
QString package;
QString kpackageType = QStringLiteral("KPackage/Generic");
KPluginMetaData metadata;
QString installPath;
void output(const QString &msg);
QStringList packages(const QString &type, const QString &path = QString());
void renderTypeTable(const QMap<QString, QString> &plugins);
void listTypes();
void coutput(const QString &msg);
void cerror(const QString &msg);
QCommandLineParser *parser = nullptr;
};
PackageTool::PackageTool(int &argc, char **argv, QCommandLineParser *parser)
: QCoreApplication(argc, argv)
{
d = new PackageToolPrivate;
d->parser = parser;
QTimer::singleShot(0, this, &PackageTool::runMain);
}
PackageTool::~PackageTool()
{
delete d;
}
void PackageTool::runMain()
{
if (d->parser->isSet(Options::hash())) {
const QString path = d->parser->value(Options::hash());
KPackage::PackageStructure structure;
KPackage::Package package(&structure);
package.setPath(path);
const QString hash = QString::fromLocal8Bit(package.cryptographicHash(QCryptographicHash::Sha1));
if (hash.isEmpty()) {
d->coutput(i18n("Failed to generate a Package hash for %1", path));
exit(9);
} else {
d->coutput(i18n("SHA1 hash for Package at %1: '%2'", package.path(), hash));
exit(0);
}
return;
}
if (d->parser->isSet(Options::listTypes())) {
d->listTypes();
exit(0);
return;
}
if (d->parser->isSet(Options::type())) {
d->kpackageType = d->parser->value(Options::type());
}
d->packageRoot = KPackage::PackageLoader::self()->loadPackage(d->kpackageType).defaultPackageRoot();
if (d->parser->isSet(Options::remove())) {
d->package = d->parser->value(Options::remove());
} else if (d->parser->isSet(Options::upgrade())) {
d->package = d->parser->value(Options::upgrade());
} else if (d->parser->isSet(Options::install())) {
d->package = d->parser->value(Options::install());
} else if (d->parser->isSet(Options::show())) {
d->package = d->parser->value(Options::show());
} else if (d->parser->isSet(Options::appstream())) {
d->package = d->parser->value(Options::appstream());
}
if (!QDir::isAbsolutePath(d->package)) {
d->packageFile = QDir(QDir::currentPath() + QLatin1Char('/') + d->package).absolutePath();
d->packageFile = QFileInfo(d->packageFile).canonicalFilePath();
if (d->parser->isSet(Options::upgrade())) {
d->package = d->packageFile;
}
} else {
d->packageFile = d->package;
}
if (!PackageLoader::self()->loadPackageStructure(d->kpackageType)) {
qWarning() << "Package type" << d->kpackageType << "not found";
}
if (d->parser->isSet(Options::show())) {
const QString pluginName = d->package;
showPackageInfo(pluginName);
return;
} else if (d->parser->isSet(Options::appstream())) {
const QString pluginName = d->package;
showAppstreamInfo(pluginName);
return;
}
if (d->parser->isSet(Options::list())) {
QString packageRoot = resolvePackageRootWithOptions();
d->coutput(i18n("Listing KPackageType: %1 in %2", d->kpackageType, packageRoot));
listPackages(d->kpackageType, packageRoot);
exit(0);
} else {
// install, remove or upgrade
d->packageRoot = resolvePackageRootWithOptions();
if (d->parser->isSet(Options::remove()) || d->parser->isSet(Options::upgrade())) {
QString pkgPath;
KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
pkg.setPath(d->package);
if (pkg.isValid()) {
pkgPath = pkg.path();
if (pkgPath.isEmpty() && !d->packageFile.isEmpty()) {
pkgPath = d->packageFile;
}
}
if (pkgPath.isEmpty()) {
pkgPath = d->package;
}
QString _p = d->packageRoot;
if (!_p.endsWith(QLatin1Char('/'))) {
_p.append(QLatin1Char('/'));
}
_p.append(d->package);
if (!d->parser->isSet(Options::type())) {
d->kpackageType = readKPackageType(pkg.metadata());
}
QString pluginName;
if (pkg.metadata().isValid()) {
d->metadata = pkg.metadata();
if (!d->metadata.isValid()) {
pluginName = d->package;
} else if (!d->metadata.isValid() && d->metadata.pluginId().isEmpty()) {
// plugin id given in command line
pluginName = d->package;
} else {
// Parameter was a plasma package, get plugin id from the package
pluginName = d->metadata.pluginId();
}
}
QStringList installed = d->packages(d->kpackageType);
// Uninstalling ...
if (installed.contains(pluginName)) { // Assume it's a plugin id
KPackage::PackageJob *uninstallJob = KPackage::PackageJob::uninstall(d->kpackageType, pluginName, d->packageRoot);
connect(uninstallJob, &KPackage::PackageJob::finished, this, [uninstallJob, this]() {
packageUninstalled(uninstallJob);
});
return;
} else {
d->coutput(i18n("Error: Plugin %1 is not installed.", pluginName));
exit(2);
}
}
if (d->parser->isSet(Options::install())) {
auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot);
connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() {
packageInstalled(installJob);
});
return;
}
if (d->package.isEmpty()) {
qWarning() << i18nc(
"No option was given, this is the error message telling the user he needs at least one, do not translate install, remove, upgrade nor list",
"One of install, remove, upgrade or list is required.");
exit(6);
}
}
}
void PackageToolPrivate::coutput(const QString &msg)
{
*cout << msg << '\n';
(*cout).flush();
}
void PackageToolPrivate::cerror(const QString &msg)
{
*cerr << msg << '\n';
(*cerr).flush();
}
QStringList PackageToolPrivate::packages(const QString &type, const QString &path)
{
QStringList result;
const QList<KPluginMetaData> dataList = KPackage::PackageLoader::self()->listPackages(type, path);
for (const KPluginMetaData &data : dataList) {
if (!result.contains(data.pluginId())) {
result << data.pluginId();
}
}
return result;
}
void PackageTool::showPackageInfo(const QString &pluginName)
{
KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
pkg.setDefaultPackageRoot(d->packageRoot);
if (QFile::exists(d->packageFile)) {
pkg.setPath(d->packageFile);
} else {
pkg.setPath(pluginName);
}
KPluginMetaData i = pkg.metadata();
if (!i.isValid()) {
*cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName);
exit(3);
return;
}
d->coutput(i18n("Showing info for package: %1", pluginName));
d->coutput(i18n(" Name : %1", i.name()));
d->coutput(i18n(" Description: %1", i.description()));
d->coutput(i18n(" Plugin : %1", i.pluginId()));
auto const authors = i.authors();
QStringList authorNames;
for (const KAboutPerson &author : authors) {
authorNames << author.name();
}
d->coutput(i18n(" Author : %1", authorNames.join(QLatin1String(", "))));
d->coutput(i18n(" Path : %1", pkg.path()));
exit(0);
}
bool translateKPluginToAppstream(const QString &tagName,
const QString &configField,
const QJsonObject &configObject,
QXmlStreamWriter &writer,
bool canEndWithDot)
{
const QRegularExpression rx(QStringLiteral("%1\\[(.*)\\]").arg(configField));
const QJsonValue native = configObject.value(configField);
if (native.isUndefined()) {
return false;
}
QString content = native.toString();
if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) {
content.chop(1);
}
writer.writeTextElement(tagName, content);
for (auto it = configObject.begin(), itEnd = configObject.end(); it != itEnd; ++it) {
const auto match = rx.match(it.key());
if (match.hasMatch()) {
QString content = it->toString();
if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) {
content.chop(1);
}
writer.writeStartElement(tagName);
writer.writeAttribute(QStringLiteral("xml:lang"), match.captured(1));
writer.writeCharacters(content);
writer.writeEndElement();
}
}
return true;
}
void PackageTool::showAppstreamInfo(const QString &pluginName)
{
KPluginMetaData i;
// if the path passed is an absolute path, and a metadata file is found under it, use that metadata file to generate the appstream info.
// This can happen in the case an application wanting to support kpackage based extensions includes in the same project both the packagestructure plugin and
// the packages themselves. In that case at build time the packagestructure plugin wouldn't be installed yet
if (QFile::exists(pluginName + QStringLiteral("/metadata.json"))) {
i = KPluginMetaData::fromJsonFile(pluginName + QStringLiteral("/metadata.json"));
} else {
KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
pkg.setDefaultPackageRoot(d->packageRoot);
if (QFile::exists(d->packageFile)) {
pkg.setPath(d->packageFile);
} else {
pkg.setPath(pluginName);
}
i = pkg.metadata();
}
if (!i.isValid()) {
*cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName);
std::exit(3);
return;
}
QString parentApp = i.value(QLatin1String("X-KDE-ParentApp"));
if (i.value(QStringLiteral("NoDisplay"), false)) {
std::exit(0);
}
QXmlStreamAttributes componentAttributes;
if (!parentApp.isEmpty()) {
componentAttributes << QXmlStreamAttribute(QLatin1String("type"), QLatin1String("addon"));
}
// Compatibility: without appstream-metainfo-output argument we print the XML output to STDOUT
// with the argument we'll print to the defined path.
// TODO: in KF6 we should switch to argument-only.
QIODevice *outputDevice = cout->device();
std::unique_ptr<QFile> outputFile;
const auto outputPath = d->parser->value(Options::appstreamOutput());
if (!outputPath.isEmpty()) {
auto outputUrl = QUrl::fromUserInput(outputPath);
outputFile.reset(new QFile(outputUrl.toLocalFile()));
if (!outputFile->open(QFile::WriteOnly | QFile::Text)) {
*cerr << "Failed to open output file for writing.";
exit(1);
}
outputDevice = outputFile.get();
}
if (i.description().isEmpty()) {
*cerr << "Error: description missing, will result in broken appdata field as <summary/> is mandatory at " << QFileInfo(i.fileName()).absoluteFilePath();
std::exit(10);
}
QXmlStreamWriter writer(outputDevice);
writer.setAutoFormatting(true);
writer.writeStartDocument();
writer.writeStartElement(QStringLiteral("component"));
writer.writeAttributes(componentAttributes);
writer.writeTextElement(QStringLiteral("id"), i.pluginId());
if (!parentApp.isEmpty()) {
writer.writeTextElement(QStringLiteral("extends"), parentApp);
}
const QJsonObject rootObject = i.rawData()[QStringLiteral("KPlugin")].toObject();
translateKPluginToAppstream(QStringLiteral("name"), QStringLiteral("Name"), rootObject, writer, false);
translateKPluginToAppstream(QStringLiteral("summary"), QStringLiteral("Description"), rootObject, writer, false);
if (!i.website().isEmpty()) {
writer.writeStartElement(QStringLiteral("url"));
writer.writeAttribute(QStringLiteral("type"), QStringLiteral("homepage"));
writer.writeCharacters(i.website());
writer.writeEndElement();
}
if (i.pluginId().startsWith(QLatin1String("org.kde."))) {
writer.writeStartElement(QStringLiteral("url"));
writer.writeAttribute(QStringLiteral("type"), QStringLiteral("donation"));
writer.writeCharacters(QStringLiteral("https://www.kde.org/donate.php?app=%1").arg(i.pluginId()));
writer.writeEndElement();
}
const auto authors = i.authors();
if (!authors.isEmpty()) {
QStringList authorsText;
authorsText.reserve(authors.size());
for (const auto &author : authors) {
authorsText += QStringLiteral("%1").arg(author.name());
}
writer.writeStartElement(QStringLiteral("developer"));
writer.writeAttribute(QStringLiteral("id"), QStringLiteral("kde.org"));
writer.writeTextElement(QStringLiteral("name"), authorsText.join(QStringLiteral(", ")));
writer.writeEndElement();
}
if (!i.iconName().isEmpty()) {
writer.writeStartElement(QStringLiteral("icon"));
writer.writeAttribute(QStringLiteral("type"), QStringLiteral("stock"));
writer.writeCharacters(i.iconName());
writer.writeEndElement();
}
writer.writeTextElement(QStringLiteral("project_license"), KAboutLicense::byKeyword(i.license()).spdx());
writer.writeTextElement(QStringLiteral("metadata_license"), QStringLiteral("CC0-1.0"));
writer.writeEndElement();
writer.writeEndDocument();
exit(0);
}
QString PackageTool::resolvePackageRootWithOptions()
{
QString packageRoot;
if (d->parser->isSet(Options::packageRoot()) && d->parser->isSet(Options::global())) {
qWarning() << i18nc("The user entered conflicting options packageroot and global, this is the error message telling the user he can use only one",
"The packageroot and global options conflict with each other, please select only one.");
::exit(7);
} else if (d->parser->isSet(Options::packageRoot())) {
packageRoot = d->parser->value(Options::packageRoot());
// qDebug() << "(set via arg) d->packageRoot is: " << d->packageRoot;
} else if (d->parser->isSet(Options::global())) {
auto const paths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, d->packageRoot, QStandardPaths::LocateDirectory);
if (!paths.isEmpty()) {
packageRoot = paths.last();
}
} else {
packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + d->packageRoot;
}
return packageRoot;
}
void PackageTool::listPackages(const QString &kpackageType, const QString &path)
{
QStringList list = d->packages(kpackageType, path);
list.sort();
for (const QString &package : std::as_const(list)) {
d->coutput(package);
}
exit(0);
}
void PackageToolPrivate::renderTypeTable(const QMap<QString, QString> &plugins)
{
const QString nameHeader = i18n("KPackage Structure Name");
const QString pathHeader = i18n("Path");
int nameWidth = nameHeader.length();
int pathWidth = pathHeader.length();
QMapIterator<QString, QString> pluginIt(plugins);
while (pluginIt.hasNext()) {
pluginIt.next();
if (pluginIt.key().length() > nameWidth) {
nameWidth = pluginIt.key().length();
}
if (pluginIt.value().length() > pathWidth) {
pathWidth = pluginIt.value().length();
}
}
std::cout << nameHeader.toLocal8Bit().constData() << std::setw(nameWidth - nameHeader.length() + 2) << ' ' << pathHeader.toLocal8Bit().constData()
<< std::setw(pathWidth - pathHeader.length() + 2) << ' ' << std::endl;
std::cout << std::setfill('-') << std::setw(nameWidth) << '-' << " " << std::setw(pathWidth) << '-' << " " << std::endl;
std::cout << std::setfill(' ');
pluginIt.toFront();
while (pluginIt.hasNext()) {
pluginIt.next();
std::cout << pluginIt.key().toLocal8Bit().constData() << std::setw(nameWidth - pluginIt.key().length() + 2) << ' '
<< pluginIt.value().toLocal8Bit().constData() << std::setw(pathWidth - pluginIt.value().length() + 2) << std::endl;
}
}
void PackageToolPrivate::listTypes()
{
coutput(i18n("Package types that are installable with this tool:"));
coutput(i18n("Built in:"));
QMap<QString, QString> builtIns;
builtIns.insert(i18n("KPackage/Generic"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/packages/"));
builtIns.insert(i18n("KPackage/GenericQML"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/genericqml/"));
renderTypeTable(builtIns);
const QList<KPluginMetaData> offers = KPluginMetaData::findPlugins(QStringLiteral("kf6/packagestructure"));
if (!offers.isEmpty()) {
std::cout << std::endl;
coutput(i18n("Provided by plugins:"));
QMap<QString, QString> plugins;
for (const KPluginMetaData &info : offers) {
const QString type = readKPackageType(info);
if (type.isEmpty()) {
continue;
}
KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(type);
plugins.insert(type, pkg.defaultPackageRoot());
}
renderTypeTable(plugins);
}
}
void PackageTool::packageInstalled(KPackage::PackageJob *job)
{
bool success = (job->error() == KJob::NoError);
int exitcode = 0;
if (success) {
if (d->parser->isSet(Options::upgrade())) {
d->coutput(i18n("Successfully upgraded %1", job->package().path()));
} else {
d->coutput(i18n("Successfully installed %1", job->package().path()));
}
} else {
d->cerror(i18n("Error: Installation of %1 failed: %2", d->packageFile, job->errorText()));
exitcode = 4;
}
exit(exitcode);
}
void PackageTool::packageUninstalled(KPackage::PackageJob *job)
{
bool success = (job->error() == KJob::NoError);
int exitcode = 0;
if (success) {
if (d->parser->isSet(Options::upgrade())) {
d->coutput(i18n("Upgrading package from file: %1", d->packageFile));
auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot);
connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() {
packageInstalled(installJob);
});
return;
}
d->coutput(i18n("Successfully uninstalled %1", job->package().path()));
} else {
d->cerror(i18n("Error: Uninstallation of %1 failed: %2", d->packageFile, job->errorText()));
exitcode = 7;
}
exit(exitcode);
}
} // namespace KPackage
#include "moc_kpackagetool.cpp"
@@ -0,0 +1,46 @@
/*
SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef PACKAGETOOL_H
#define PACKAGETOOL_H
#include "package.h"
#include "packagejob.h"
#include <QCoreApplication>
class QCommandLineParser;
class KJob;
namespace KPackage
{
class PackageToolPrivate;
class PackageTool : public QCoreApplication
{
Q_OBJECT
public:
PackageTool(int &argc, char **argv, QCommandLineParser *parser);
~PackageTool() override;
void listPackages(const QString &kpackageType, const QString &path = QString());
void showPackageInfo(const QString &pluginName);
void showAppstreamInfo(const QString &pluginName);
QString resolvePackageRootWithOptions();
private Q_SLOTS:
void runMain();
void packageInstalled(KPackage::PackageJob *job);
void packageUninstalled(KPackage::PackageJob *job);
private:
PackageToolPrivate *d;
};
}
#endif
@@ -0,0 +1,65 @@
/*
SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
/**
* kpackagetool6 exit codes used in this program
0 No error
1 Unspecified error
2 Plugin is not installed
3 Plugin or package invalid
4 Installation failed, see stderr for reason
5 Could not find a suitable installer for package type
6 No install option given
7 Conflicting arguments supplied
8 Uninstallation failed, see stderr for reason
9 Failed to generate package hash
*/
#include <KLocalizedString>
#include <QCommandLineParser>
#include "kpackagetool.h"
#include "options.h"
int main(int argc, char **argv)
{
QCommandLineParser parser;
KPackage::PackageTool app(argc, argv, &parser);
const QString description = i18n("KPackage Manager");
const auto version = QStringLiteral("2.0");
app.setApplicationVersion(version);
parser.addVersionOption();
parser.addHelpOption();
parser.setApplicationDescription(description);
parser.addOptions({Options::hash(),
Options::global(),
Options::type(),
Options::install(),
Options::show(),
Options::upgrade(),
Options::list(),
Options::listTypes(),
Options::remove(),
Options::packageRoot(),
Options::appstream(),
Options::appstreamOutput()});
parser.process(app);
// at least one operation should be specified
if (!parser.isSet(QStringLiteral("hash")) && !parser.isSet(QStringLiteral("g")) && !parser.isSet(QStringLiteral("i")) && !parser.isSet(QStringLiteral("s"))
&& !parser.isSet(QStringLiteral("appstream-metainfo")) && !parser.isSet(QStringLiteral("u")) && !parser.isSet(QStringLiteral("l"))
&& !parser.isSet(QStringLiteral("list-types")) && !parser.isSet(QStringLiteral("r")) && !parser.isSet(QStringLiteral("generate-index"))
&& !parser.isSet(QStringLiteral("remove-index"))) {
parser.showHelp(0);
}
return app.exec();
}
@@ -0,0 +1,95 @@
#ifndef OPTIONS_H
#define OPTIONS_H
#include <QCommandLineOption>
namespace Options
{
static QCommandLineOption hash()
{
static QCommandLineOption o{QStringLiteral("hash"),
i18nc("Do not translate <path>", "Generate a SHA1 hash for the package at <path>"),
QStringLiteral("path")};
return o;
}
static QCommandLineOption global()
{
static QCommandLineOption o{QStringList{QStringLiteral("g"), QStringLiteral("global")},
i18n("For install or remove, operates on packages installed for all users.")};
return o;
}
static QCommandLineOption type()
{
static QCommandLineOption o{QStringList{QStringLiteral("t"), QStringLiteral("type")},
i18nc("theme, wallpaper, etc. are keywords, but they may be translated, as both versions "
"are recognized by the application "
"(if translated, should be same as messages with 'package type' context below)",
"The type of package, corresponding to the service type of the package plugin, e.g. KPackage/Generic, Plasma/Theme, "
"Plasma/Wallpaper, Plasma/Applet, etc."),
QStringLiteral("type"),
QStringLiteral("KPackage/Generic")};
return o;
}
static QCommandLineOption install()
{
static QCommandLineOption o{QStringList{QStringLiteral("i"), QStringLiteral("install")},
i18nc("Do not translate <path>", "Install the package at <path>"),
QStringLiteral("path")};
return o;
}
static QCommandLineOption show()
{
static QCommandLineOption o{QStringList{QStringLiteral("s"), QStringLiteral("show")},
i18nc("Do not translate <name>", "Show information of package <name>"),
QStringLiteral("name")};
return o;
}
static QCommandLineOption upgrade()
{
static QCommandLineOption o{QStringList{QStringLiteral("u"), QStringLiteral("upgrade")},
i18nc("Do not translate <path>", "Upgrade the package at <path>"),
QStringLiteral("path")};
return o;
}
static QCommandLineOption list()
{
static QCommandLineOption o{QStringList{QStringLiteral("l"), QStringLiteral("list")}, i18n("List installed packages")};
return o;
}
static QCommandLineOption listTypes()
{
static QCommandLineOption o{QStringList{QStringLiteral("list-types")}, i18n("List all known package types that can be installed")};
return o;
}
static QCommandLineOption remove()
{
static QCommandLineOption o{QStringList{QStringLiteral("r"), QStringLiteral("remove")},
i18nc("Do not translate <name>", "Remove the package named <name>"),
QStringLiteral("name")};
return o;
}
static QCommandLineOption packageRoot()
{
static QCommandLineOption o{QStringList{QStringLiteral("p"), QStringLiteral("packageroot")},
i18n("Absolute path to the package root. If not supplied, then the standard data"
" directories for this KDE session will be searched instead."),
QStringLiteral("path")};
return o;
}
static QCommandLineOption appstream()
{
static QCommandLineOption o{QStringLiteral("appstream-metainfo"),
i18nc("Do not translate <path>", "Outputs the metadata for the package <path>"),
QStringLiteral("path")};
return o;
}
static QCommandLineOption appstreamOutput()
{
static QCommandLineOption o{QStringLiteral("appstream-metainfo-output"),
i18nc("Do not translate <path>", "Outputs the metadata for the package into <path>"),
QStringLiteral("path")};
return o;
}
}
#endif // OPTIONS_H