diff --git a/autotests/jsonplugin.cpp b/autotests/jsonplugin.cpp index 03b2bdb..7cda8bf 100644 --- a/autotests/jsonplugin.cpp +++ b/autotests/jsonplugin.cpp @@ -1,34 +1,33 @@ /* * Copyright 2013 Sebastian Kügler * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "jsonplugin.h" #include -#include JsonPlugin::JsonPlugin(QObject *parent, const QVariantList &args) : QObject(parent) { Q_UNUSED(args) } K_PLUGIN_FACTORY_WITH_JSON(jsonpluginfa, "jsonplugin.json", registerPlugin();) #include "jsonplugin.moc" diff --git a/autotests/jsonplugin2.cpp b/autotests/jsonplugin2.cpp index d12588f..a1b0a09 100644 --- a/autotests/jsonplugin2.cpp +++ b/autotests/jsonplugin2.cpp @@ -1,34 +1,33 @@ /* * Copyright 2013 Sebastian Kügler * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #include "jsonplugin2.h" #include -#include JsonPlugin2::JsonPlugin2(QObject *parent, const QVariantList &args) : QObject(parent) { Q_UNUSED(args) } K_PLUGIN_FACTORY_WITH_JSON(jsonplugin2, "jsonplugin2.json", registerPlugin();) #include "jsonplugin2.moc" diff --git a/autotests/kdelibs4configmigratortest.cpp b/autotests/kdelibs4configmigratortest.cpp index 0b51f54..2fcdd16 100644 --- a/autotests/kdelibs4configmigratortest.cpp +++ b/autotests/kdelibs4configmigratortest.cpp @@ -1,142 +1,141 @@ /* * Copyright 2014 Montel Laurent * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ // test object #include "kdelibs4configmigrator.h" // Qt #include #include #include #include -#include #include class Kdelibs4ConfigMigratorTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void shouldNotMigrateIfKde4HomeDirDoesntExist(); void shouldMigrateIfKde4HomeDirExist(); void shouldMigrateConfigFiles(); void shouldMigrateUiFiles(); }; void Kdelibs4ConfigMigratorTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); const QString configHome = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); QDir(configHome).removeRecursively(); const QString dataHome = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); QDir(dataHome).removeRecursively(); } void Kdelibs4ConfigMigratorTest::shouldNotMigrateIfKde4HomeDirDoesntExist() { qputenv("KDEHOME", ""); Kdelibs4ConfigMigrator migration(QLatin1String("foo")); QCOMPARE(migration.migrate(), false); } void Kdelibs4ConfigMigratorTest::shouldMigrateIfKde4HomeDirExist() { QTemporaryDir kdehomeDir; QVERIFY(kdehomeDir.isValid()); const QString kdehome = kdehomeDir.path(); qputenv("KDEHOME", QFile::encodeName(kdehome)); Kdelibs4ConfigMigrator migration(QLatin1String("foo")); QCOMPARE(migration.migrate(), true); } void Kdelibs4ConfigMigratorTest::shouldMigrateConfigFiles() { QTemporaryDir kdehomeDir; const QString kdehome = kdehomeDir.path(); qputenv("KDEHOME", QFile::encodeName(kdehome)); //Generate kde4 config dir const QString configPath = kdehome + QLatin1Char('/') + QLatin1String("share/config/"); QDir().mkpath(configPath); QVERIFY(QDir(configPath).exists()); QStringList listConfig; listConfig << QLatin1String("foorc") << QLatin1String("foo1rc"); for (const QString &config : qAsConst(listConfig)) { QFile fooConfigFile(QLatin1String(KDELIBS4CONFIGMIGRATOR_DATA_DIR) + QLatin1Char('/') + config); QVERIFY(fooConfigFile.exists()); const QString storedConfigFilePath = configPath + QLatin1Char('/') + config; QVERIFY(QFile::copy(fooConfigFile.fileName(), storedConfigFilePath)); QCOMPARE(QStandardPaths::locate(QStandardPaths::ConfigLocation, config), QString()); } Kdelibs4ConfigMigrator migration(QLatin1String("foo")); migration.setConfigFiles(QStringList() << listConfig); QVERIFY(migration.migrate()); for (const QString &config : qAsConst(listConfig)) { const QString migratedConfigFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, config); QVERIFY(!migratedConfigFile.isEmpty()); QVERIFY(QFile(migratedConfigFile).exists()); QFile::remove(migratedConfigFile); } } void Kdelibs4ConfigMigratorTest::shouldMigrateUiFiles() { QTemporaryDir kdehomeDir; const QString kdehome = kdehomeDir.path(); qputenv("KDEHOME", QFile::encodeName(kdehome)); const QString appName = QLatin1String("foo"); //Generate kde4 data dir const QString dataPath = kdehome + QLatin1Char('/') + QLatin1String("share/apps/"); QDir().mkpath(dataPath); QVERIFY(QDir(dataPath).exists()); const QString xdgDatahome = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); QStringList listUi; listUi << QLatin1String("appuirc") << QLatin1String("appui1rc"); for (const QString &uifile : qAsConst(listUi)) { QFile fooConfigFile(QLatin1String(KDELIBS4CONFIGMIGRATOR_DATA_DIR) + QLatin1Char('/') + uifile); QVERIFY(fooConfigFile.exists()); QDir().mkpath(dataPath + QLatin1Char('/') + appName); const QString storedConfigFilePath = dataPath + QLatin1Char('/') + appName + QLatin1Char('/') + uifile; QVERIFY(QFile::copy(fooConfigFile.fileName(), storedConfigFilePath)); const QString xdgUiFile = xdgDatahome + QLatin1String("/kxmlgui5/") + appName + QLatin1Char('/') + uifile; QVERIFY(!QFile::exists(xdgUiFile)); } Kdelibs4ConfigMigrator migration(appName); migration.setUiFiles(QStringList() << listUi); QVERIFY(migration.migrate()); for (const QString &uifile : qAsConst(listUi)) { const QString xdgUiFile = xdgDatahome + QLatin1String("/kxmlgui5/") + appName + QLatin1Char('/') + uifile; QVERIFY(QFile(xdgUiFile).exists()); QFile::remove(xdgUiFile); } } QTEST_MAIN(Kdelibs4ConfigMigratorTest) #include "kdelibs4configmigratortest.moc" diff --git a/autotests/kshareddatacachetest.cpp b/autotests/kshareddatacachetest.cpp index ac163aa..da4ff93 100644 --- a/autotests/kshareddatacachetest.cpp +++ b/autotests/kshareddatacachetest.cpp @@ -1,78 +1,76 @@ /* This file is part of the KDE libraries * Copyright (c) 2012 David Faure * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License or ( at * your option ) version 3 or, at the discretion of KDE e.V. ( which shall * act as a proxy as in section 14 of the GPLv3 ), any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include -#include -#include #include #include // strcpy class KSharedDataCacheTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void simpleInsert(); }; void KSharedDataCacheTest::initTestCase() { } void KSharedDataCacheTest::simpleInsert() { const QLatin1String cacheName("myTestCache"); const QLatin1String key("mypic"); // clear the cache QString cacheFile = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/") + cacheName + QLatin1String(".kcache"); QFile file(cacheFile); if (file.exists()) { QVERIFY(file.remove()); } // insert something into it KSharedDataCache cache(cacheName, 5 * 1024 * 1024); #ifndef Q_OS_WIN // the windows implementation is currently only memory based and not really shared QVERIFY(file.exists()); // make sure we got the cache filename right #endif QByteArray data; data.resize(9228); strcpy(data.data(), "Hello world"); QVERIFY(cache.insert(key, data)); // read it out again QByteArray result; QVERIFY(cache.find(key, &result)); QCOMPARE(result, data); // another insert strcpy(data.data(), "Hello KDE"); QVERIFY(cache.insert(key, data)); // and another read QVERIFY(cache.find(key, &result)); QCOMPARE(result, data); } QTEST_MAIN(KSharedDataCacheTest) #include "kshareddatacachetest.moc" diff --git a/src/desktoptojson/desktoptojson.cpp b/src/desktoptojson/desktoptojson.cpp index 36e42c0..b478495 100644 --- a/src/desktoptojson/desktoptojson.cpp +++ b/src/desktoptojson/desktoptojson.cpp @@ -1,146 +1,144 @@ /****************************************************************************** * Copyright 2013 Sebastian Kügler * * Copyright 2014 Alex Richardson * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * * * License as published by the Free Software Foundation; either * * version 2.1 of the License, or (at your option) version 3, or any * * later version accepted by the membership of KDE e.V. (or its * * successor approved by the membership of KDE e.V.), which shall * * act as a proxy defined in Section 6 of version 3 of the license. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with this library. If not, see * * . * * * ******************************************************************************/ #include "desktoptojson.h" #include "../lib/plugin/desktopfileparser_p.h" -#include #include +#include #include #include -#include -#include #include DesktopToJson::DesktopToJson(QCommandLineParser *parser, const QCommandLineOption &i, const QCommandLineOption &o, const QCommandLineOption &v, const QCommandLineOption &c, const QCommandLineOption &s) : m_parser(parser), input(i), output(o), verbose(v), compat(c), serviceTypesOption(s) { } bool DesktopFileParser::s_verbose = false; bool DesktopFileParser::s_compatibilityMode = false; int DesktopToJson::runMain() { if (!m_parser->isSet(input)) { m_parser->showHelp(1); return 1; } if (m_parser->isSet(verbose)) { DesktopFileParser::s_verbose = true; } if (m_parser->isSet(compat)) { DesktopFileParser::s_compatibilityMode = true; } if (!resolveFiles()) { qCCritical(DESKTOPPARSER) << "Failed to resolve filenames" << m_inFile << m_outFile << endl; return 1; } #pragma message("TODO: make it an error if one of the service type files is invalid or not found") const QStringList serviceTypes = m_parser->values(serviceTypesOption); return convert(m_inFile, m_outFile, serviceTypes) ? EXIT_SUCCESS : EXIT_FAILURE; } bool DesktopToJson::resolveFiles() { if (m_parser->isSet(input)) { m_inFile = m_parser->value(input); const QFileInfo fi(m_inFile); if (!fi.exists()) { qCCritical(DESKTOPPARSER) << "File not found: " << m_inFile << endl; return false; } if (!fi.isAbsolute()) { m_inFile = fi.absoluteFilePath(); } } if (m_parser->isSet(output)) { m_outFile = m_parser->value(output); } else if (!m_inFile.isEmpty()) { m_outFile = m_inFile; m_outFile.replace(QStringLiteral(".desktop"), QStringLiteral(".json")); } return m_inFile != m_outFile && !m_inFile.isEmpty() && !m_outFile.isEmpty(); } void DesktopFileParser::convertToCompatibilityJson(const QString &key, const QString &value, QJsonObject &json, int lineNr) { // XXX: Hidden=true doesn't make sense with json plugins since the metadata is inside the .so static const QStringList boolkeys = QStringList() << QStringLiteral("Hidden") << QStringLiteral("X-KDE-PluginInfo-EnabledByDefault"); static const QStringList stringlistkeys = QStringList() << QStringLiteral("X-KDE-ServiceTypes") << QStringLiteral("X-KDE-PluginInfo-Depends"); if (boolkeys.contains(key)) { // should only be lower case, but be tolerant here if (value.toLower() == QLatin1String("true")) { json[key] = true; } else { if (value.toLower() != QLatin1String("false")) { qCWarning(DESKTOPPARSER).nospace() << "Expected boolean value for key \"" << key << "\" at line " << lineNr << "but got \"" << value << "\" instead."; } json[key] = false; } } else if (stringlistkeys.contains(key)) { json[key] = QJsonArray::fromStringList(DesktopFileParser::deserializeList(value)); } else { json[key] = value; } } bool DesktopToJson::convert(const QString &src, const QString &dest, const QStringList& serviceTypes) { QJsonObject json; DesktopFileParser::convert(src, serviceTypes, json, nullptr); if (DesktopFileParser::s_compatibilityMode) { Q_ASSERT(json.value(QStringLiteral("KPlugin")).toObject().isEmpty()); json.remove(QStringLiteral("KPlugin")); } QJsonDocument jdoc; jdoc.setObject(json); QFile file(dest); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { qCCritical(DESKTOPPARSER) << "Failed to open " << dest << endl; return false; } file.write(jdoc.toJson()); qCDebug(DESKTOPPARSER) << "Generated " << dest << endl; return true; } diff --git a/src/desktoptojson/main.cpp b/src/desktoptojson/main.cpp index 2c50a8b..1ddfc3c 100644 --- a/src/desktoptojson/main.cpp +++ b/src/desktoptojson/main.cpp @@ -1,89 +1,88 @@ /****************************************************************************** * Copyright 2013 Sebastian Kügler * * Copyright 2014 Alex Richardson * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * * * License as published by the Free Software Foundation; either * * version 2.1 of the License, or (at your option) version 3, or any * * later version accepted by the membership of KDE e.V. (or its * * successor approved by the membership of KDE e.V.), which shall * * act as a proxy defined in Section 6 of version 3 of the license. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with this library. If not, see * * . * * * ******************************************************************************/ #include "desktoptojson.h" -#include static void messageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { QByteArray localMsg = msg.toLocal8Bit(); switch (type) { case QtDebugMsg: fprintf(stdout, "%s\n", localMsg.constData()); break; case QtInfoMsg: fprintf(stdout, "Info: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtWarningMsg: fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtCriticalMsg: fprintf(stderr, "Error: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); break; case QtFatalMsg: fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), context.file, context.line, context.function); abort(); } } int main(int argc, char** argv) { qInstallMessageHandler(messageOutput); QCoreApplication app(argc, argv); const QString description = QStringLiteral("Converts desktop files to json"); app.setApplicationVersion(QStringLiteral("1.0")); const static auto _i = QStringLiteral("input"); const static auto _o = QStringLiteral("output"); const static auto _n = QStringLiteral("name"); const static auto _c = QStringLiteral("compat"); const static auto _s = QStringLiteral("serviceType"); QCommandLineOption input = QCommandLineOption(QStringList() << QStringLiteral("i") << _i, QStringLiteral("Read input from file"), _n); QCommandLineOption output = QCommandLineOption(QStringList() << QStringLiteral("o") << _o, QStringLiteral("Write output to file"), _n); QCommandLineOption verbose = QCommandLineOption(QStringList() << QStringLiteral("verbose"), QStringLiteral("Enable verbose (debug) output")); QCommandLineOption compat = QCommandLineOption(QStringList() << QStringLiteral("c") << _c, QStringLiteral("Generate JSON that is compatible with KPluginInfo instead of the new KPluginMetaData")); QCommandLineOption serviceTypes = QCommandLineOption(QStringList() << QStringLiteral("s") << _s, QStringLiteral("The name or full path of a KServiceType definition .desktop file. Can be passed multiple times"), _s); QCommandLineParser parser; parser.addVersionOption(); parser.setApplicationDescription(description); parser.addHelpOption(); parser.addOption(input); parser.addOption(output); parser.addOption(verbose); parser.addOption(compat); parser.addOption(serviceTypes); DesktopToJson dtj(&parser, input, output, verbose, compat, serviceTypes); parser.process(app); return dtj.runMain(); } diff --git a/src/lib/caching/kshareddatacache.cpp b/src/lib/caching/kshareddatacache.cpp index 39594db..6ee49ac 100644 --- a/src/lib/caching/kshareddatacache.cpp +++ b/src/lib/caching/kshareddatacache.cpp @@ -1,1720 +1,1717 @@ /* * This file is part of the KDE project. * Copyright © 2010, 2012 Michael Pyne * Copyright © 2012 Ralf Jung * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation. * * This library includes "MurmurHash" code from Austin Appleby, which is * placed in the public domain. See http://sites.google.com/site/murmurhash/ * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kshareddatacache.h" #include "kshareddatacache_p.h" // Various auxiliary support code #include "kcoreaddons_debug.h" #include "qstandardpaths.h" #include #include -#include #include #include #include #include #include -#include #include -#include #include #include #include #include /// The maximum number of probes to make while searching for a bucket in /// the presence of collisions in the cache index table. static const uint MAX_PROBE_COUNT = 6; /** * A very simple class whose only purpose is to be thrown as an exception from * underlying code to indicate that the shared cache is apparently corrupt. * This must be caught by top-level library code and used to unlink the cache * in this circumstance. * * @internal */ class KSDCCorrupted { public: KSDCCorrupted() { qCritical() << "Error detected in cache, re-generating"; } }; //----------------------------------------------------------------------------- // MurmurHashAligned, by Austin Appleby // (Released to the public domain, or licensed under the MIT license where // software may not be released to the public domain. See // http://sites.google.com/site/murmurhash/) // Same algorithm as MurmurHash, but only does aligned reads - should be safer // on certain platforms. static unsigned int MurmurHashAligned(const void *key, int len, unsigned int seed) { const unsigned int m = 0xc6a4a793; const int r = 16; const unsigned char *data = reinterpret_cast(key); unsigned int h = seed ^ (len * m); int align = reinterpret_cast(data) & 3; if (align && len >= 4) { // Pre-load the temp registers unsigned int t = 0, d = 0; switch (align) { case 1: t |= data[2] << 16; case 2: t |= data[1] << 8; case 3: t |= data[0]; } t <<= (8 * align); data += 4 - align; len -= 4 - align; int sl = 8 * (4 - align); int sr = 8 * align; // Mix while (len >= 4) { d = *reinterpret_cast(data); t = (t >> sr) | (d << sl); h += t; h *= m; h ^= h >> r; t = d; data += 4; len -= 4; } // Handle leftover data in temp registers int pack = len < align ? len : align; d = 0; switch (pack) { case 3: d |= data[2] << 16; case 2: d |= data[1] << 8; case 1: d |= data[0]; case 0: h += (t >> sr) | (d << sl); h *= m; h ^= h >> r; } data += pack; len -= pack; } else { while (len >= 4) { h += *reinterpret_cast(data); h *= m; h ^= h >> r; data += 4; len -= 4; } } //---------- // Handle tail bytes switch (len) { case 3: h += data[2] << 16; case 2: h += data[1] << 8; case 1: h += data[0]; h *= m; h ^= h >> r; }; h *= m; h ^= h >> 10; h *= m; h ^= h >> 17; return h; } /** * This is the hash function used for our data to hopefully make the * hashing used to place the QByteArrays as efficient as possible. */ static quint32 generateHash(const QByteArray &buffer) { // The final constant is the "seed" for MurmurHash. Do *not* change it // without incrementing the cache version. return MurmurHashAligned(buffer.data(), buffer.size(), 0xF0F00F0F); } // Alignment concerns become a big deal when we're dealing with shared memory, // since trying to access a structure sized at, say 8 bytes at an address that // is not evenly divisible by 8 is a crash-inducing error on some // architectures. The compiler would normally take care of this, but with // shared memory the compiler will not necessarily know the alignment expected, // so make sure we account for this ourselves. To do so we need a way to find // out the expected alignment. Enter ALIGNOF... #ifndef ALIGNOF #if defined(Q_CC_GNU) || defined(Q_CC_SUN) #define ALIGNOF(x) (__alignof__ (x)) // GCC provides what we want directly #else #include // offsetof template struct __alignmentHack { char firstEntry; T obj; static const size_t size = offsetof(__alignmentHack, obj); }; #define ALIGNOF(x) (__alignmentHack::size) #endif // Non gcc #endif // ALIGNOF undefined // Returns a pointer properly aligned to handle size alignment. // size should be a power of 2. start is assumed to be the lowest // permissible address, therefore the return value will be >= start. template T *alignTo(const void *start, uint size = ALIGNOF(T)) { quintptr mask = size - 1; // Cast to int-type to handle bit-twiddling quintptr basePointer = reinterpret_cast(start); // If (and only if) we are already aligned, adding mask into basePointer // will not increment any of the bits in ~mask and we get the right answer. basePointer = (basePointer + mask) & ~mask; return reinterpret_cast(basePointer); } /** * Returns a pointer to a const object of type T, assumed to be @p offset * *BYTES* greater than the base address. Note that in order to meet alignment * requirements for T, it is possible that the returned pointer points greater * than @p offset into @p base. */ template const T *offsetAs(const void *const base, qint32 offset) { const char *ptr = reinterpret_cast(base); return alignTo(ptr + offset); } // Same as above, but for non-const objects template T *offsetAs(void *const base, qint32 offset) { char *ptr = reinterpret_cast(base); return alignTo(ptr + offset); } /** * @return the smallest integer greater than or equal to (@p a / @p b). * @param a Numerator, should be ≥ 0. * @param b Denominator, should be > 0. */ static unsigned intCeil(unsigned a, unsigned b) { // The overflow check is unsigned and so is actually defined behavior. if (Q_UNLIKELY(b == 0 || ((a + b) < a))) { throw KSDCCorrupted(); } return (a + b - 1) / b; } /** * @return number of set bits in @p value (see also "Hamming weight") */ static unsigned countSetBits(unsigned value) { // K&R / Wegner's algorithm used. GCC supports __builtin_popcount but we // expect there to always be only 1 bit set so this should be perhaps a bit // faster 99.9% of the time. unsigned count = 0; for (count = 0; value != 0; count++) { value &= (value - 1); // Clears least-significant set bit. } return count; } typedef qint32 pageID; // ========================================================================= // Description of the cache: // // The shared memory cache is designed to be handled as two separate objects, // all contained in the same global memory segment. First off, there is the // basic header data, consisting of the global header followed by the // accounting data necessary to hold items (described in more detail // momentarily). Following the accounting data is the start of the "page table" // (essentially just as you'd see it in an Operating Systems text). // // The page table contains shared memory split into fixed-size pages, with a // configurable page size. In the event that the data is too large to fit into // a single logical page, it will need to occupy consecutive pages of memory. // // The accounting data that was referenced earlier is split into two: // // 1. index table, containing a fixed-size list of possible cache entries. // Each index entry is of type IndexTableEntry (below), and holds the various // accounting data and a pointer to the first page. // // 2. page table, which is used to speed up the process of searching for // free pages of memory. There is one entry for every page in the page table, // and it contains the index of the one entry in the index table actually // holding the page (or <0 if the page is free). // // The entire segment looks like so: // ?════════?═════════════?════════════?═══════?═══════?═══════?═══════?═══? // ? Header │ Index Table │ Page Table ? Pages │ │ │ │...? // ?════════?═════════════?════════════?═══════?═══════?═══════?═══════?═══? // ========================================================================= // All elements of this struct must be "plain old data" (POD) types since it // will be in shared memory. In addition, no pointers! To point to something // you must use relative offsets since the pointer start addresses will be // different in each process. struct IndexTableEntry { uint fileNameHash; uint totalItemSize; // in bytes mutable uint useCount; time_t addTime; mutable time_t lastUsedTime; pageID firstPage; }; // Page table entry struct PageTableEntry { // int so we can use values <0 for unassigned pages. qint32 index; }; // Each individual page contains the cached data. The first page starts off with // the utf8-encoded key, a null '\0', and then the data follows immediately // from the next byte, possibly crossing consecutive page boundaries to hold // all of the data. // There is, however, no specific struct for a page, it is simply a location in // memory. // This is effectively the layout of the shared memory segment. The variables // contained within form the header, data contained afterwards is pointed to // by using special accessor functions. struct SharedMemory { /** * Note to downstream packagers: This version flag is intended to be * machine-specific. The KDE-provided source code will not set the lower * two bits to allow for distribution-specific needs, with the exception * of version 1 which was already defined in KDE Platform 4.5. * e.g. the next version bump will be from 4 to 8, then 12, etc. */ enum { PIXMAP_CACHE_VERSION = 12, MINIMUM_CACHE_SIZE = 4096 }; // Note to those who follow me. You should not, under any circumstances, ever // re-arrange the following two fields, even if you change the version number // for later revisions of this code. QAtomicInt ready; ///< DO NOT INITIALIZE quint8 version; // See kshareddatacache_p.h SharedLock shmLock; uint cacheSize; uint cacheAvail; QAtomicInt evictionPolicy; // pageSize and cacheSize determine the number of pages. The number of // pages determine the page table size and (indirectly) the index table // size. QAtomicInt pageSize; // This variable is added to reserve space for later cache timestamping // support. The idea is this variable will be updated when the cache is // written to, to allow clients to detect a changed cache quickly. QAtomicInt cacheTimestamp; /** * Converts the given average item size into an appropriate page size. */ static unsigned equivalentPageSize(unsigned itemSize) { if (itemSize == 0) { return 4096; // Default average item size. } int log2OfSize = 0; while ((itemSize >>= 1) != 0) { log2OfSize++; } // Bound page size between 512 bytes and 256 KiB. // If this is adjusted, also alter validSizeMask in cachePageSize log2OfSize = qBound(9, log2OfSize, 18); return (1 << log2OfSize); } // Returns pageSize in unsigned format. unsigned cachePageSize() const { unsigned _pageSize = static_cast(pageSize.load()); // bits 9-18 may be set. static const unsigned validSizeMask = 0x7FE00u; // Check for page sizes that are not a power-of-2, or are too low/high. if (Q_UNLIKELY(countSetBits(_pageSize) != 1 || (_pageSize & ~validSizeMask))) { throw KSDCCorrupted(); } return _pageSize; } /** * This is effectively the class ctor. But since we're in shared memory, * there's a few rules: * * 1. To allow for some form of locking in the initial-setup case, we * use an atomic int, which will be initialized to 0 by mmap(). Then to * take the lock we atomically increment the 0 to 1. If we end up calling * the QAtomicInt constructor we can mess that up, so we can't use a * constructor for this class either. * 2. Any member variable you add takes up space in shared memory as well, * so make sure you need it. */ bool performInitialSetup(uint _cacheSize, uint _pageSize) { if (_cacheSize < MINIMUM_CACHE_SIZE) { qCritical() << "Internal error: Attempted to create a cache sized < " << MINIMUM_CACHE_SIZE; return false; } if (_pageSize == 0) { qCritical() << "Internal error: Attempted to create a cache with 0-sized pages."; return false; } shmLock.type = findBestSharedLock(); if (shmLock.type == LOCKTYPE_INVALID) { qCritical() << "Unable to find an appropriate lock to guard the shared cache. " << "This *should* be essentially impossible. :("; return false; } bool isProcessShared = false; QSharedPointer tempLock(createLockFromId(shmLock.type, shmLock)); if (!tempLock->initialize(isProcessShared)) { qCritical() << "Unable to initialize the lock for the cache!"; return false; } if (!isProcessShared) { qCWarning(KCOREADDONS_DEBUG) << "Cache initialized, but does not support being" << "shared across processes."; } // These must be updated to make some of our auxiliary functions // work right since their values will be based on the cache size. cacheSize = _cacheSize; pageSize = _pageSize; version = PIXMAP_CACHE_VERSION; cacheTimestamp = static_cast(::time(nullptr)); clearInternalTables(); // Unlock the mini-lock, and introduce a total memory barrier to make // sure all changes have propagated even without a mutex. ready.ref(); return true; } void clearInternalTables() { // Assumes we're already locked somehow. cacheAvail = pageTableSize(); // Setup page tables to point nowhere PageTableEntry *table = pageTable(); for (uint i = 0; i < pageTableSize(); ++i) { table[i].index = -1; } // Setup index tables to be accurate. IndexTableEntry *indices = indexTable(); for (uint i = 0; i < indexTableSize(); ++i) { indices[i].firstPage = -1; indices[i].useCount = 0; indices[i].fileNameHash = 0; indices[i].totalItemSize = 0; indices[i].addTime = 0; indices[i].lastUsedTime = 0; } } const IndexTableEntry *indexTable() const { // Index Table goes immediately after this struct, at the first byte // where alignment constraints are met (accounted for by offsetAs). return offsetAs(this, sizeof(*this)); } const PageTableEntry *pageTable() const { const IndexTableEntry *base = indexTable(); base += indexTableSize(); // Let's call wherever we end up the start of the page table... return alignTo(base); } const void *cachePages() const { const PageTableEntry *tableStart = pageTable(); tableStart += pageTableSize(); // Let's call wherever we end up the start of the data... return alignTo(tableStart, cachePageSize()); } const void *page(pageID at) const { if (static_cast(at) >= pageTableSize()) { return nullptr; } // We must manually calculate this one since pageSize varies. const char *pageStart = reinterpret_cast(cachePages()); pageStart += (at * cachePageSize()); return reinterpret_cast(pageStart); } // The following are non-const versions of some of the methods defined // above. They use const_cast<> because I feel that is better than // duplicating the code. I suppose template member functions (?) // may work, may investigate later. IndexTableEntry *indexTable() { const SharedMemory *that = const_cast(this); return const_cast(that->indexTable()); } PageTableEntry *pageTable() { const SharedMemory *that = const_cast(this); return const_cast(that->pageTable()); } void *cachePages() { const SharedMemory *that = const_cast(this); return const_cast(that->cachePages()); } void *page(pageID at) { const SharedMemory *that = const_cast(this); return const_cast(that->page(at)); } uint pageTableSize() const { return cacheSize / cachePageSize(); } uint indexTableSize() const { // Assume 2 pages on average are needed -> the number of entries // would be half of the number of pages. return pageTableSize() / 2; } /** * @return the index of the first page, for the set of contiguous * pages that can hold @p pagesNeeded PAGES. */ pageID findEmptyPages(uint pagesNeeded) const { if (Q_UNLIKELY(pagesNeeded > pageTableSize())) { return pageTableSize(); } // Loop through the page table, find the first empty page, and just // makes sure that there are enough free pages. const PageTableEntry *table = pageTable(); uint contiguousPagesFound = 0; pageID base = 0; for (pageID i = 0; i < static_cast(pageTableSize()); ++i) { if (table[i].index < 0) { if (contiguousPagesFound == 0) { base = i; } contiguousPagesFound++; } else { contiguousPagesFound = 0; } if (contiguousPagesFound == pagesNeeded) { return base; } } return pageTableSize(); } // left < right? static bool lruCompare(const IndexTableEntry &l, const IndexTableEntry &r) { // Ensure invalid entries migrate to the end if (l.firstPage < 0 && r.firstPage >= 0) { return false; } if (l.firstPage >= 0 && r.firstPage < 0) { return true; } // Most recently used will have the highest absolute time => // least recently used (lowest) should go first => use left < right return l.lastUsedTime < r.lastUsedTime; } // left < right? static bool seldomUsedCompare(const IndexTableEntry &l, const IndexTableEntry &r) { // Ensure invalid entries migrate to the end if (l.firstPage < 0 && r.firstPage >= 0) { return false; } if (l.firstPage >= 0 && r.firstPage < 0) { return true; } // Put lowest use count at start by using left < right return l.useCount < r.useCount; } // left < right? static bool ageCompare(const IndexTableEntry &l, const IndexTableEntry &r) { // Ensure invalid entries migrate to the end if (l.firstPage < 0 && r.firstPage >= 0) { return false; } if (l.firstPage >= 0 && r.firstPage < 0) { return true; } // Oldest entries die first -- they have the lowest absolute add time, // so just like the others use left < right return l.addTime < r.addTime; } void defragment() { if (cacheAvail * cachePageSize() == cacheSize) { return; // That was easy } qCDebug(KCOREADDONS_DEBUG) << "Defragmenting the shared cache"; // Just do a linear scan, and anytime there is free space, swap it // with the pages to its right. In order to meet the precondition // we need to skip any used pages first. pageID currentPage = 0; pageID idLimit = static_cast(pageTableSize()); PageTableEntry *pages = pageTable(); if (Q_UNLIKELY(!pages || idLimit <= 0)) { throw KSDCCorrupted(); } // Skip used pages while (currentPage < idLimit && pages[currentPage].index >= 0) { ++currentPage; } pageID freeSpot = currentPage; // Main loop, starting from a free page, skip to the used pages and // move them back. while (currentPage < idLimit) { // Find the next used page while (currentPage < idLimit && pages[currentPage].index < 0) { ++currentPage; } if (currentPage >= idLimit) { break; } // Found an entry, move it. qint32 affectedIndex = pages[currentPage].index; if (Q_UNLIKELY(affectedIndex < 0 || affectedIndex >= idLimit || indexTable()[affectedIndex].firstPage != currentPage)) { throw KSDCCorrupted(); } indexTable()[affectedIndex].firstPage = freeSpot; // Moving one page at a time guarantees we can use memcpy safely // (in other words, the source and destination will not overlap). while (currentPage < idLimit && pages[currentPage].index >= 0) { const void *const sourcePage = page(currentPage); void *const destinationPage = page(freeSpot); // We always move used pages into previously-found empty spots, // so check ordering as well for logic errors. if (Q_UNLIKELY(!sourcePage || !destinationPage || sourcePage < destinationPage)) { throw KSDCCorrupted(); } ::memcpy(destinationPage, sourcePage, cachePageSize()); pages[freeSpot].index = affectedIndex; pages[currentPage].index = -1; ++currentPage; ++freeSpot; // If we've just moved the very last page and it happened to // be at the very end of the cache then we're done. if (currentPage >= idLimit) { break; } // We're moving consecutive used pages whether they belong to // our affected entry or not, so detect if we've started moving // the data for a different entry and adjust if necessary. if (affectedIndex != pages[currentPage].index) { indexTable()[pages[currentPage].index].firstPage = freeSpot; } affectedIndex = pages[currentPage].index; } // At this point currentPage is on a page that is unused, and the // cycle repeats. However, currentPage is not the first unused // page, freeSpot is, so leave it alone. } } /** * Finds the index entry for a given key. * @param key UTF-8 encoded key to search for. * @return The index of the entry in the cache named by @p key. Returns * <0 if no such entry is present. */ qint32 findNamedEntry(const QByteArray &key) const { uint keyHash = generateHash(key); uint position = keyHash % indexTableSize(); uint probeNumber = 1; // See insert() for description // Imagine 3 entries A, B, C in this logical probing chain. If B is // later removed then we can't find C either. So, we must keep // searching for probeNumber number of tries (or we find the item, // obviously). while (indexTable()[position].fileNameHash != keyHash && probeNumber < MAX_PROBE_COUNT) { position = (keyHash + (probeNumber + probeNumber * probeNumber) / 2) % indexTableSize(); probeNumber++; } if (indexTable()[position].fileNameHash == keyHash) { pageID firstPage = indexTable()[position].firstPage; if (firstPage < 0 || static_cast(firstPage) >= pageTableSize()) { return -1; } const void *resultPage = page(firstPage); if (Q_UNLIKELY(!resultPage)) { throw KSDCCorrupted(); } const char *utf8FileName = reinterpret_cast(resultPage); if (qstrncmp(utf8FileName, key.constData(), cachePageSize()) == 0) { return position; } } return -1; // Not found, or a different one found. } // Function to use with QSharedPointer in removeUsedPages below... static void deleteTable(IndexTableEntry *table) { delete [] table; } /** * Removes the requested number of pages. * * @param numberNeeded the number of pages required to fulfill a current request. * This number should be <0 and <= the number of pages in the cache. * @return The identifier of the beginning of a consecutive block of pages able * to fill the request. Returns a value >= pageTableSize() if no such * request can be filled. * @internal */ uint removeUsedPages(uint numberNeeded) { if (numberNeeded == 0) { qCritical() << "Internal error: Asked to remove exactly 0 pages for some reason."; throw KSDCCorrupted(); } if (numberNeeded > pageTableSize()) { qCritical() << "Internal error: Requested more space than exists in the cache."; qCritical() << numberNeeded << "requested, " << pageTableSize() << "is the total possible."; throw KSDCCorrupted(); } // If the cache free space is large enough we will defragment first // instead since it's likely we're highly fragmented. // Otherwise, we will (eventually) simply remove entries per the // eviction order set for the cache until there is enough room // available to hold the number of pages we need. qCDebug(KCOREADDONS_DEBUG) << "Removing old entries to free up" << numberNeeded << "pages," << cacheAvail << "are already theoretically available."; if (cacheAvail > 3 * numberNeeded) { defragment(); uint result = findEmptyPages(numberNeeded); if (result < pageTableSize()) { return result; } else { qCritical() << "Just defragmented a locked cache, but still there" << "isn't enough room for the current request."; } } // At this point we know we'll have to free some space up, so sort our // list of entries by whatever the current criteria are and start // killing expired entries. QSharedPointer tablePtr(new IndexTableEntry[indexTableSize()], deleteTable); if (!tablePtr) { qCritical() << "Unable to allocate temporary memory for sorting the cache!"; clearInternalTables(); throw KSDCCorrupted(); } // We use tablePtr to ensure the data is destroyed, but do the access // via a helper pointer to allow for array ops. IndexTableEntry *table = tablePtr.data(); ::memcpy(table, indexTable(), sizeof(IndexTableEntry) * indexTableSize()); // Our entry ID is simply its index into the // index table, which qSort will rearrange all willy-nilly, so first // we'll save the *real* entry ID into firstPage (which is useless in // our copy of the index table). On the other hand if the entry is not // used then we note that with -1. for (uint i = 0; i < indexTableSize(); ++i) { table[i].firstPage = table[i].useCount > 0 ? static_cast(i) : -1; } // Declare the comparison function that we'll use to pass to qSort, // based on our cache eviction policy. bool (*compareFunction)(const IndexTableEntry &, const IndexTableEntry &); switch (evictionPolicy.load()) { case KSharedDataCache::EvictLeastOftenUsed: case KSharedDataCache::NoEvictionPreference: default: compareFunction = seldomUsedCompare; break; case KSharedDataCache::EvictLeastRecentlyUsed: compareFunction = lruCompare; break; case KSharedDataCache::EvictOldest: compareFunction = ageCompare; break; } std::sort(table, table + indexTableSize(), compareFunction); // Least recently used entries will be in the front. // Start killing until we have room. // Note on removeEntry: It expects an index into the index table, // but our sorted list is all jumbled. But we stored the real index // in the firstPage member. // Remove entries until we've removed at least the required number // of pages. uint i = 0; while (i < indexTableSize() && numberNeeded > cacheAvail) { int curIndex = table[i++].firstPage; // Really an index, not a page // Removed everything, still no luck (or curIndex is set but too high). if (curIndex < 0 || static_cast(curIndex) >= indexTableSize()) { qCritical() << "Trying to remove index" << curIndex << "out-of-bounds for index table of size" << indexTableSize(); throw KSDCCorrupted(); } qCDebug(KCOREADDONS_DEBUG) << "Removing entry of" << indexTable()[curIndex].totalItemSize << "size"; removeEntry(curIndex); } // At this point let's see if we have freed up enough data by // defragmenting first and seeing if we can find that free space. defragment(); pageID result = pageTableSize(); while (i < indexTableSize() && (static_cast(result = findEmptyPages(numberNeeded))) >= pageTableSize()) { int curIndex = table[i++].firstPage; if (curIndex < 0) { // One last shot. defragment(); return findEmptyPages(numberNeeded); } if (Q_UNLIKELY(static_cast(curIndex) >= indexTableSize())) { throw KSDCCorrupted(); } removeEntry(curIndex); } // Whew. return result; } // Returns the total size required for a given cache size. static uint totalSize(uint cacheSize, uint effectivePageSize) { uint numberPages = intCeil(cacheSize, effectivePageSize); uint indexTableSize = numberPages / 2; // Knowing the number of pages, we can determine what addresses we'd be // using (properly aligned), and from there determine how much memory // we'd use. IndexTableEntry *indexTableStart = offsetAs(static_cast(nullptr), sizeof(SharedMemory)); indexTableStart += indexTableSize; PageTableEntry *pageTableStart = reinterpret_cast(indexTableStart); pageTableStart = alignTo(pageTableStart); pageTableStart += numberPages; // The weird part, we must manually adjust the pointer based on the page size. char *cacheStart = reinterpret_cast(pageTableStart); cacheStart += (numberPages * effectivePageSize); // ALIGNOF gives pointer alignment cacheStart = alignTo(cacheStart, ALIGNOF(void *)); // We've traversed the header, index, page table, and cache. // Wherever we're at now is the size of the enchilada. return static_cast(reinterpret_cast(cacheStart)); } uint fileNameHash(const QByteArray &utf8FileName) const { return generateHash(utf8FileName) % indexTableSize(); } void clear() { clearInternalTables(); } void removeEntry(uint index); }; // The per-instance private data, such as map size, whether // attached or not, pointer to shared memory, etc. class Q_DECL_HIDDEN KSharedDataCache::Private { public: Private(const QString &name, unsigned defaultCacheSize, unsigned expectedItemSize ) : m_cacheName(name) , shm(nullptr) , m_lock() , m_mapSize(0) , m_defaultCacheSize(defaultCacheSize) , m_expectedItemSize(expectedItemSize) , m_expectedType(LOCKTYPE_INVALID) { mapSharedMemory(); } // Put the cache in a condition to be able to call mapSharedMemory() by // completely detaching from shared memory (such as to respond to an // unrecoverable error). // m_mapSize must already be set to the amount of memory mapped to shm. void detachFromSharedMemory() { // The lock holds a reference into shared memory, so this must be // cleared before shm is removed. m_lock.clear(); if (shm && 0 != ::munmap(shm, m_mapSize)) { qCritical() << "Unable to unmap shared memory segment" << static_cast(shm) << ":" << ::strerror(errno); } shm = nullptr; m_mapSize = 0; } // This function does a lot of the important work, attempting to connect to shared // memory, a private anonymous mapping if that fails, and failing that, nothing (but // the cache remains "valid", we just don't actually do anything). void mapSharedMemory() { // 0-sized caches are fairly useless. unsigned cacheSize = qMax(m_defaultCacheSize, uint(SharedMemory::MINIMUM_CACHE_SIZE)); unsigned pageSize = SharedMemory::equivalentPageSize(m_expectedItemSize); // Ensure that the cache is sized such that there is a minimum number of // pages available. (i.e. a cache consisting of only 1 page is fairly // useless and probably crash-prone). cacheSize = qMax(pageSize * 256, cacheSize); // The m_cacheName is used to find the file to store the cache in. const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation); QString cacheName = cacheDir + QLatin1String("/") + m_cacheName + QLatin1String(".kcache"); QFile file(cacheName); QFileInfo fileInfo(file); if (!QDir().mkpath(fileInfo.absolutePath())) { return; } // The basic idea is to open the file that we want to map into shared // memory, and then actually establish the mapping. Once we have mapped the // file into shared memory we can close the file handle, the mapping will // still be maintained (unless the file is resized to be shorter than // expected, which we don't handle yet :-( ) // size accounts for the overhead over the desired cacheSize uint size = SharedMemory::totalSize(cacheSize, pageSize); void *mapAddress = MAP_FAILED; if (size < cacheSize) { qCritical() << "Asked for a cache size less than requested size somehow -- Logic Error :("; return; } // We establish the shared memory mapping here, only if we will have appropriate // mutex support (systemSupportsProcessSharing), then we: // Open the file and resize to some sane value if the file is too small. if (file.open(QIODevice::ReadWrite) && (file.size() >= size || (file.resize(size) && ensureFileAllocated(file.handle(), size)))) { // Use mmap directly instead of QFile::map since the QFile (and its // shared mapping) will disappear unless we hang onto the QFile for no // reason (see the note below, we don't care about the file per se...) mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, file.handle(), 0); // So... it is possible that someone else has mapped this cache already // with a larger size. If that's the case we need to at least match // the size to be able to access every entry, so fixup the mapping. if (mapAddress != MAP_FAILED) { SharedMemory *mapped = reinterpret_cast(mapAddress); // First make sure that the version of the cache on disk is // valid. We also need to check that version != 0 to // disambiguate against an uninitialized cache. if (mapped->version != SharedMemory::PIXMAP_CACHE_VERSION && mapped->version > 0) { qCWarning(KCOREADDONS_DEBUG) << "Deleting wrong version of cache" << cacheName; // CAUTION: Potentially recursive since the recovery // involves calling this function again. m_mapSize = size; shm = mapped; recoverCorruptedCache(); return; } else if (mapped->cacheSize > cacheSize) { // This order is very important. We must save the cache size // before we remove the mapping, but unmap before overwriting // the previous mapping size... cacheSize = mapped->cacheSize; unsigned actualPageSize = mapped->cachePageSize(); ::munmap(mapAddress, size); size = SharedMemory::totalSize(cacheSize, actualPageSize); mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, file.handle(), 0); } } } // We could be here without the mapping established if: // 1) Process-shared synchronization is not supported, either at compile or run time, // 2) Unable to open the required file. // 3) Unable to resize the file to be large enough. // 4) Establishing the mapping failed. // 5) The mapping succeeded, but the size was wrong and we were unable to map when // we tried again. // 6) The incorrect version of the cache was detected. // 7) The file could be created, but posix_fallocate failed to commit it fully to disk. // In any of these cases, attempt to fallback to the // better-supported anonymous private page style of mmap. This memory won't // be shared, but our code will still work the same. // NOTE: We never use the on-disk representation independently of the // shared memory. If we don't get shared memory the disk info is ignored, // if we do get shared memory we never look at disk again. if (mapAddress == MAP_FAILED) { qCWarning(KCOREADDONS_DEBUG) << "Failed to establish shared memory mapping, will fallback" << "to private memory -- memory usage will increase"; mapAddress = QT_MMAP(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); } // Well now we're really hosed. We can still work, but we can't even cache // data. if (mapAddress == MAP_FAILED) { qCritical() << "Unable to allocate shared memory segment for shared data cache" << cacheName << "of size" << cacheSize; return; } m_mapSize = size; // We never actually construct shm, but we assign it the same address as the // shared memory we just mapped, so effectively shm is now a SharedMemory that // happens to be located at mapAddress. shm = reinterpret_cast(mapAddress); // If we were first to create this memory map, all data will be 0. // Therefore if ready == 0 we're not initialized. A fully initialized // header will have ready == 2. Why? // Because 0 means "safe to initialize" // 1 means "in progress of initing" // 2 means "ready" uint usecSleepTime = 8; // Start by sleeping for 8 microseconds while (shm->ready.load() != 2) { if (Q_UNLIKELY(usecSleepTime >= (1 << 21))) { // Didn't acquire within ~8 seconds? Assume an issue exists qCritical() << "Unable to acquire shared lock, is the cache corrupt?"; file.remove(); // Unlink the cache in case it's corrupt. detachFromSharedMemory(); return; // Fallback to QCache (later) } if (shm->ready.testAndSetAcquire(0, 1)) { if (!shm->performInitialSetup(cacheSize, pageSize)) { qCritical() << "Unable to perform initial setup, this system probably " "does not really support process-shared pthreads or " "semaphores, even though it claims otherwise."; file.remove(); detachFromSharedMemory(); return; } } else { usleep(usecSleepTime); // spin // Exponential fallback as in Ethernet and similar collision resolution methods usecSleepTime *= 2; } } m_expectedType = shm->shmLock.type; m_lock = QSharedPointer(createLockFromId(m_expectedType, shm->shmLock)); bool isProcessSharingSupported = false; if (!m_lock->initialize(isProcessSharingSupported)) { qCritical() << "Unable to setup shared cache lock, although it worked when created."; detachFromSharedMemory(); } } // Called whenever the cache is apparently corrupt (for instance, a timeout trying to // lock the cache). In this situation it is safer just to destroy it all and try again. void recoverCorruptedCache() { KSharedDataCache::deleteCache(m_cacheName); detachFromSharedMemory(); // Do this even if we weren't previously cached -- it might work now. mapSharedMemory(); } // This should be called for any memory access to shared memory. This // function will verify that the bytes [base, base+accessLength) are // actually mapped to d->shm. The cache itself may have incorrect cache // page sizes, incorrect cache size, etc. so this function should be called // despite the cache data indicating it should be safe. // // If the access is /not/ safe then a KSDCCorrupted exception will be // thrown, so be ready to catch that. void verifyProposedMemoryAccess(const void *base, unsigned accessLength) const { quintptr startOfAccess = reinterpret_cast(base); quintptr startOfShm = reinterpret_cast(shm); if (Q_UNLIKELY(startOfAccess < startOfShm)) { throw KSDCCorrupted(); } quintptr endOfShm = startOfShm + m_mapSize; quintptr endOfAccess = startOfAccess + accessLength; // Check for unsigned integer wraparound, and then // bounds access if (Q_UNLIKELY((endOfShm < startOfShm) || (endOfAccess < startOfAccess) || (endOfAccess > endOfShm))) { throw KSDCCorrupted(); } } bool lock() const { if (Q_LIKELY(shm && shm->shmLock.type == m_expectedType)) { return m_lock->lock(); } // No shm or wrong type --> corrupt! throw KSDCCorrupted(); } void unlock() const { m_lock->unlock(); } class CacheLocker { mutable Private *d; bool cautiousLock() { int lockCount = 0; // Locking can fail due to a timeout. If it happens too often even though // we're taking corrective action assume there's some disastrous problem // and give up. while (!d->lock() && !isLockedCacheSafe()) { d->recoverCorruptedCache(); if (!d->shm) { qCWarning(KCOREADDONS_DEBUG) << "Lost the connection to shared memory for cache" << d->m_cacheName; return false; } if (lockCount++ > 4) { qCritical() << "There is a very serious problem with the KDE data cache" << d->m_cacheName << "giving up trying to access cache."; d->detachFromSharedMemory(); return false; } } return true; } // Runs a quick battery of tests on an already-locked cache and returns // false as soon as a sanity check fails. The cache remains locked in this // situation. bool isLockedCacheSafe() const { // Note that cachePageSize() itself runs a check that can throw. uint testSize = SharedMemory::totalSize(d->shm->cacheSize, d->shm->cachePageSize()); if (Q_UNLIKELY(d->m_mapSize != testSize)) { return false; } if (Q_UNLIKELY(d->shm->version != SharedMemory::PIXMAP_CACHE_VERSION)) { return false; } switch (d->shm->evictionPolicy.load()) { case NoEvictionPreference: // fallthrough case EvictLeastRecentlyUsed: // fallthrough case EvictLeastOftenUsed: // fallthrough case EvictOldest: break; default: return false; } return true; } public: CacheLocker(const Private *_d) : d(const_cast(_d)) { if (Q_UNLIKELY(!d || !d->shm || !cautiousLock())) { d = nullptr; } } ~CacheLocker() { if (d && d->shm) { d->unlock(); } } CacheLocker(const CacheLocker &) = delete; CacheLocker &operator=(const CacheLocker &) = delete; bool failed() const { return !d || d->shm == nullptr; } }; QString m_cacheName; SharedMemory *shm; QSharedPointer m_lock; uint m_mapSize; uint m_defaultCacheSize; uint m_expectedItemSize; SharedLockId m_expectedType; }; // Must be called while the lock is already held! void SharedMemory::removeEntry(uint index) { if (index >= indexTableSize() || cacheAvail > pageTableSize()) { throw KSDCCorrupted(); } PageTableEntry *pageTableEntries = pageTable(); IndexTableEntry *entriesIndex = indexTable(); // Update page table first pageID firstPage = entriesIndex[index].firstPage; if (firstPage < 0 || static_cast(firstPage) >= pageTableSize()) { qCDebug(KCOREADDONS_DEBUG) << "Trying to remove an entry which is already invalid. This " << "cache is likely corrupt."; throw KSDCCorrupted(); } if (index != static_cast(pageTableEntries[firstPage].index)) { qCritical() << "Removing entry" << index << "but the matching data" << "doesn't link back -- cache is corrupt, clearing."; throw KSDCCorrupted(); } uint entriesToRemove = intCeil(entriesIndex[index].totalItemSize, cachePageSize()); uint savedCacheSize = cacheAvail; for (uint i = firstPage; i < pageTableSize() && static_cast(pageTableEntries[i].index) == index; ++i) { pageTableEntries[i].index = -1; cacheAvail++; } if ((cacheAvail - savedCacheSize) != entriesToRemove) { qCritical() << "We somehow did not remove" << entriesToRemove << "when removing entry" << index << ", instead we removed" << (cacheAvail - savedCacheSize); throw KSDCCorrupted(); } // For debugging #ifdef NDEBUG void *const startOfData = page(firstPage); if (startOfData) { QByteArray str((const char *) startOfData); str.prepend(" REMOVED: "); str.prepend(QByteArray::number(index)); str.prepend("ENTRY "); ::memcpy(startOfData, str.constData(), str.size() + 1); } #endif // Update the index entriesIndex[index].fileNameHash = 0; entriesIndex[index].totalItemSize = 0; entriesIndex[index].useCount = 0; entriesIndex[index].lastUsedTime = 0; entriesIndex[index].addTime = 0; entriesIndex[index].firstPage = -1; } KSharedDataCache::KSharedDataCache(const QString &cacheName, unsigned defaultCacheSize, unsigned expectedItemSize) : d(nullptr) { try { d = new Private(cacheName, defaultCacheSize, expectedItemSize); } catch (KSDCCorrupted) { KSharedDataCache::deleteCache(cacheName); // Try only once more try { d = new Private(cacheName, defaultCacheSize, expectedItemSize); } catch (KSDCCorrupted) { qCritical() << "Even a brand-new cache starts off corrupted, something is" << "seriously wrong. :-("; d = nullptr; // Just in case } } } KSharedDataCache::~KSharedDataCache() { // Note that there is no other actions required to separate from the // shared memory segment, simply unmapping is enough. This makes things // *much* easier so I'd recommend maintaining this ideal. if (!d) { return; } if (d->shm) { #ifdef KSDC_MSYNC_SUPPORTED ::msync(d->shm, d->m_mapSize, MS_INVALIDATE | MS_ASYNC); #endif ::munmap(d->shm, d->m_mapSize); } // Do not delete d->shm, it was never constructed, it's just an alias. d->shm = nullptr; delete d; } bool KSharedDataCache::insert(const QString &key, const QByteArray &data) { try { Private::CacheLocker lock(d); if (lock.failed()) { return false; } QByteArray encodedKey = key.toUtf8(); uint keyHash = generateHash(encodedKey); uint position = keyHash % d->shm->indexTableSize(); // See if we're overwriting an existing entry. IndexTableEntry *indices = d->shm->indexTable(); // In order to avoid the issue of a very long-lived cache having items // with a use count of 1 near-permanently, we attempt to artifically // reduce the use count of long-lived items when there is high load on // the cache. We do this randomly, with a weighting that makes the event // impossible if load < 0.5, and guaranteed if load >= 0.96. const static double startCullPoint = 0.5l; const static double mustCullPoint = 0.96l; // cacheAvail is in pages, cacheSize is in bytes. double loadFactor = 1.0 - (1.0l * d->shm->cacheAvail * d->shm->cachePageSize() / d->shm->cacheSize); bool cullCollisions = false; if (Q_UNLIKELY(loadFactor >= mustCullPoint)) { cullCollisions = true; } else if (loadFactor > startCullPoint) { const int tripWireValue = RAND_MAX * (loadFactor - startCullPoint) / (mustCullPoint - startCullPoint); if (KRandom::random() >= tripWireValue) { cullCollisions = true; } } // In case of collisions in the index table (i.e. identical positions), use // quadratic chaining to attempt to find an empty slot. The equation we use // is: // position = (hash + (i + i*i) / 2) % size, where i is the probe number. uint probeNumber = 1; while (indices[position].useCount > 0 && probeNumber < MAX_PROBE_COUNT) { // If we actually stumbled upon an old version of the key we are // overwriting, then use that position, do not skip over it. if (Q_UNLIKELY(indices[position].fileNameHash == keyHash)) { break; } // If we are "culling" old entries, see if this one is old and if so // reduce its use count. If it reduces to zero then eliminate it and // use its old spot. if (cullCollisions && (::time(nullptr) - indices[position].lastUsedTime) > 60) { indices[position].useCount >>= 1; if (indices[position].useCount == 0) { qCDebug(KCOREADDONS_DEBUG) << "Overwriting existing old cached entry due to collision."; d->shm->removeEntry(position); // Remove it first break; } } position = (keyHash + (probeNumber + probeNumber * probeNumber) / 2) % d->shm->indexTableSize(); probeNumber++; } if (indices[position].useCount > 0 && indices[position].firstPage >= 0) { //qCDebug(KCOREADDONS_DEBUG) << "Overwriting existing cached entry due to collision."; d->shm->removeEntry(position); // Remove it first } // Data will be stored as fileNamefoo\0PNGimagedata..... // So total size required is the length of the encoded file name + 1 // for the trailing null, and then the length of the image data. uint fileNameLength = 1 + encodedKey.length(); uint requiredSize = fileNameLength + data.size(); uint pagesNeeded = intCeil(requiredSize, d->shm->cachePageSize()); uint firstPage(-1); if (pagesNeeded >= d->shm->pageTableSize()) { qCWarning(KCOREADDONS_DEBUG) << key << "is too large to be cached."; return false; } // If the cache has no room, or the fragmentation is too great to find // the required number of consecutive free pages, take action. if (pagesNeeded > d->shm->cacheAvail || (firstPage = d->shm->findEmptyPages(pagesNeeded)) >= d->shm->pageTableSize()) { // If we have enough free space just defragment uint freePagesDesired = 3 * qMax(1u, pagesNeeded / 2); if (d->shm->cacheAvail > freePagesDesired) { // TODO: How the hell long does this actually take on real // caches? d->shm->defragment(); firstPage = d->shm->findEmptyPages(pagesNeeded); } else { // If we already have free pages we don't want to remove a ton // extra. However we can't rely on the return value of // removeUsedPages giving us a good location since we're not // passing in the actual number of pages that we need. d->shm->removeUsedPages(qMin(2 * freePagesDesired, d->shm->pageTableSize()) - d->shm->cacheAvail); firstPage = d->shm->findEmptyPages(pagesNeeded); } if (firstPage >= d->shm->pageTableSize() || d->shm->cacheAvail < pagesNeeded) { qCritical() << "Unable to free up memory for" << key; return false; } } // Update page table PageTableEntry *table = d->shm->pageTable(); for (uint i = 0; i < pagesNeeded; ++i) { table[firstPage + i].index = position; } // Update index indices[position].fileNameHash = keyHash; indices[position].totalItemSize = requiredSize; indices[position].useCount = 1; indices[position].addTime = ::time(nullptr); indices[position].lastUsedTime = indices[position].addTime; indices[position].firstPage = firstPage; // Update cache d->shm->cacheAvail -= pagesNeeded; // Actually move the data in place void *dataPage = d->shm->page(firstPage); if (Q_UNLIKELY(!dataPage)) { throw KSDCCorrupted(); } // Verify it will all fit d->verifyProposedMemoryAccess(dataPage, requiredSize); // Cast for byte-sized pointer arithmetic uchar *startOfPageData = reinterpret_cast(dataPage); ::memcpy(startOfPageData, encodedKey.constData(), fileNameLength); ::memcpy(startOfPageData + fileNameLength, data.constData(), data.size()); return true; } catch (KSDCCorrupted) { d->recoverCorruptedCache(); return false; } } bool KSharedDataCache::find(const QString &key, QByteArray *destination) const { try { Private::CacheLocker lock(d); if (lock.failed()) { return false; } // Search in the index for our data, hashed by key; QByteArray encodedKey = key.toUtf8(); qint32 entry = d->shm->findNamedEntry(encodedKey); if (entry >= 0) { const IndexTableEntry *header = &d->shm->indexTable()[entry]; const void *resultPage = d->shm->page(header->firstPage); if (Q_UNLIKELY(!resultPage)) { throw KSDCCorrupted(); } d->verifyProposedMemoryAccess(resultPage, header->totalItemSize); header->useCount++; header->lastUsedTime = ::time(nullptr); // Our item is the key followed immediately by the data, so skip // past the key. const char *cacheData = reinterpret_cast(resultPage); cacheData += encodedKey.size(); cacheData++; // Skip trailing null -- now we're pointing to start of data if (destination) { *destination = QByteArray(cacheData, header->totalItemSize - encodedKey.size() - 1); } return true; } } catch (KSDCCorrupted) { d->recoverCorruptedCache(); } return false; } void KSharedDataCache::clear() { try { Private::CacheLocker lock(d); if (!lock.failed()) { d->shm->clear(); } } catch (KSDCCorrupted) { d->recoverCorruptedCache(); } } bool KSharedDataCache::contains(const QString &key) const { try { Private::CacheLocker lock(d); if (lock.failed()) { return false; } return d->shm->findNamedEntry(key.toUtf8()) >= 0; } catch (KSDCCorrupted) { d->recoverCorruptedCache(); return false; } } void KSharedDataCache::deleteCache(const QString &cacheName) { QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/") + cacheName + QLatin1String(".kcache"); // Note that it is important to simply unlink the file, and not truncate it // smaller first to avoid SIGBUS errors and similar with shared memory // attached to the underlying inode. qCDebug(KCOREADDONS_DEBUG) << "Removing cache at" << cachePath; QFile::remove(cachePath); } unsigned KSharedDataCache::totalSize() const { try { Private::CacheLocker lock(d); if (lock.failed()) { return 0u; } return d->shm->cacheSize; } catch (KSDCCorrupted) { d->recoverCorruptedCache(); return 0u; } } unsigned KSharedDataCache::freeSize() const { try { Private::CacheLocker lock(d); if (lock.failed()) { return 0u; } return d->shm->cacheAvail * d->shm->cachePageSize(); } catch (KSDCCorrupted) { d->recoverCorruptedCache(); return 0u; } } KSharedDataCache::EvictionPolicy KSharedDataCache::evictionPolicy() const { if (d && d->shm) { return static_cast(d->shm->evictionPolicy.fetchAndAddAcquire(0)); } return NoEvictionPreference; } void KSharedDataCache::setEvictionPolicy(EvictionPolicy newPolicy) { if (d && d->shm) { d->shm->evictionPolicy.fetchAndStoreRelease(static_cast(newPolicy)); } } unsigned KSharedDataCache::timestamp() const { if (d && d->shm) { return static_cast(d->shm->cacheTimestamp.fetchAndAddAcquire(0)); } return 0; } void KSharedDataCache::setTimestamp(unsigned newTimestamp) { if (d && d->shm) { d->shm->cacheTimestamp.fetchAndStoreRelease(static_cast(newTimestamp)); } } diff --git a/src/lib/io/kautosavefile.cpp b/src/lib/io/kautosavefile.cpp index 041d482..6cb15c0 100644 --- a/src/lib/io/kautosavefile.cpp +++ b/src/lib/io/kautosavefile.cpp @@ -1,218 +1,217 @@ /* This file is part of the KDE libraries Copyright (c) 2006 Jacob R Rideout Copyright (c) 2015 Nick Shaforostoff This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kautosavefile.h" #include // for FILENAME_MAX #include #include #include -#include #include #include #include #include "krandom.h" #include "kcoreaddons_debug.h" class KAutoSaveFilePrivate { public: enum {NamePadding=8}; KAutoSaveFilePrivate() : lock(nullptr), managedFileNameChanged(false) {} QString tempFileName(); QUrl managedFile; QLockFile *lock; bool managedFileNameChanged; }; static QStringList findAllStales(const QString &appName) { const QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); QStringList files; for (const QString &dir : dirs) { QDir appDir(dir + QStringLiteral("/stalefiles/") + appName); //qCDebug(KCOREADDONS_DEBUG) << "Looking in" << appDir.absolutePath(); const auto listFiles = appDir.entryList(QDir::Files); for (const QString &file : listFiles) { files << (appDir.absolutePath() + QLatin1Char('/') + file); } } return files; } QString KAutoSaveFilePrivate::tempFileName() { // Note: we drop any query string and user/pass info const QString protocol(managedFile.scheme()); const QString path(managedFile.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path()); QString name(managedFile.fileName()); // Remove any part of the path to the right if it is longer than the max file size and // ensure that the max filesize takes into account the other parts of the tempFileName // Subtract 1 for the _ char, 3 for the padding separator, 5 is for the .lock int pathLengthLimit = FILENAME_MAX - NamePadding - name.size() - protocol.size() - 9; QString junk = KRandom::randomString(NamePadding); // tempName = fileName + junk.truncated + protocol + _ + path.truncated + junk // This is done so that the separation between the filename and path can be determined name += junk.rightRef(3) + protocol + QLatin1Char('_') + path.leftRef(pathLengthLimit) + junk; return QString::fromLatin1(QUrl::toPercentEncoding(name).constData()); } KAutoSaveFile::KAutoSaveFile(const QUrl &filename, QObject *parent) : QFile(parent), d(new KAutoSaveFilePrivate) { setManagedFile(filename); } KAutoSaveFile::KAutoSaveFile(QObject *parent) : QFile(parent), d(new KAutoSaveFilePrivate) { } KAutoSaveFile::~KAutoSaveFile() { releaseLock(); delete d->lock; delete d; } QUrl KAutoSaveFile::managedFile() const { return d->managedFile; } void KAutoSaveFile::setManagedFile(const QUrl &filename) { releaseLock(); d->managedFile = filename; d->managedFileNameChanged = true; } void KAutoSaveFile::releaseLock() { if (d->lock && d->lock->isLocked()) { delete d->lock; d->lock = nullptr; if (!fileName().isEmpty()) { remove(); } } } bool KAutoSaveFile::open(OpenMode openmode) { if (d->managedFile.isEmpty()) { return false; } QString tempFile; if (d->managedFileNameChanged) { QString staleFilesDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/stalefiles/") + QCoreApplication::instance()->applicationName(); if (!QDir().mkpath(staleFilesDir)) { return false; } tempFile = staleFilesDir + QChar::fromLatin1('/') + d->tempFileName(); } else { tempFile = fileName(); } d->managedFileNameChanged = false; setFileName(tempFile); if (QFile::open(openmode)) { if (!d->lock) { d->lock = new QLockFile(tempFile + QStringLiteral(".lock")); d->lock->setStaleLockTime(60 * 1000); // HARDCODE, 1 minute } if (d->lock->isLocked() || d->lock->tryLock()) { return true; } else { qCWarning(KCOREADDONS_DEBUG)<<"Could not lock file:"< KAutoSaveFile::staleFiles(const QUrl &filename, const QString &applicationName) { QString appName(applicationName); if (appName.isEmpty()) { appName = QCoreApplication::instance()->applicationName(); } // get stale files const QStringList files = findAllStales(appName); QList list; // contruct a KAutoSaveFile for stale files corresponding given filename for (const QString &file : files) { if (file.endsWith(QLatin1String(".lock")) || (!filename.isEmpty() && extractManagedFilePath(file).path()!=filename.path())) { continue; } // sets managedFile KAutoSaveFile *asFile = new KAutoSaveFile(filename.isEmpty()?extractManagedFilePath(file):filename); asFile->setFileName(file); asFile->d->managedFileNameChanged = false; // do not regenerate tempfile name list.append(asFile); } return list; } QList KAutoSaveFile::allStaleFiles(const QString &applicationName) { return staleFiles(QUrl(), applicationName); } #include "moc_kautosavefile.cpp" diff --git a/src/lib/io/kmessage.cpp b/src/lib/io/kmessage.cpp index c743935..55b9954 100644 --- a/src/lib/io/kmessage.cpp +++ b/src/lib/io/kmessage.cpp @@ -1,102 +1,101 @@ /* This file is part of the KDE libraries Copyright (C) 2006 Michaël Larouche This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; version 2 of the License. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kmessage.h" -#include #include class StaticMessageHandler { public: StaticMessageHandler() {} ~StaticMessageHandler() { delete m_handler; } StaticMessageHandler(const StaticMessageHandler &) = delete; StaticMessageHandler &operator=(const StaticMessageHandler &) = delete; /* Sets the new message handler and deletes the old one */ void setHandler(KMessageHandler *handler) { delete m_handler; m_handler = handler; } KMessageHandler *handler() const { return m_handler; } protected: KMessageHandler *m_handler = nullptr; }; Q_GLOBAL_STATIC(StaticMessageHandler, s_messageHandler) static void internalMessageFallback(KMessage::MessageType messageType, const QString &text, const QString &caption) { QString prefix; switch (messageType) { case KMessage::Error: prefix = QStringLiteral("ERROR: "); break; case KMessage::Fatal: prefix = QStringLiteral("FATAL: "); break; case KMessage::Information: prefix = QStringLiteral("INFORMATION: "); break; case KMessage::Sorry: prefix = QStringLiteral("SORRY: "); break; case KMessage::Warning: prefix = QStringLiteral("WARNING: "); break; } QString message; if (!caption.isEmpty()) { message += QLatin1Char('(') + caption + QLatin1Char(')'); } message += prefix + text; // Show a message to the developer to setup a KMessageHandler std::cerr << "WARNING: Please setup an KMessageHandler with KMessage::setMessageHandler to display message propertly." << std::endl; // Show message to stdout std::cerr << qPrintable(message) << std::endl; } void KMessage::setMessageHandler(KMessageHandler *handler) { // Delete old message handler. s_messageHandler()->setHandler(handler); } void KMessage::message(KMessage::MessageType messageType, const QString &text, const QString &caption) { // Use current message handler if available, else use stdout if (s_messageHandler()->handler()) { s_messageHandler()->handler()->message(messageType, text, caption); } else { internalMessageFallback(messageType, text, caption); } } diff --git a/src/lib/jobs/kjob.cpp b/src/lib/jobs/kjob.cpp index 086dacc..bff1627 100644 --- a/src/lib/jobs/kjob.cpp +++ b/src/lib/jobs/kjob.cpp @@ -1,342 +1,340 @@ /* This file is part of the KDE project Copyright (C) 2000 Stephan Kulow David Faure Copyright (C) 2006 Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kjob.h" #include "kjob_p.h" #include "kjobuidelegate.h" #include -#include -#include #include KJobPrivate::KJobPrivate() : q_ptr(nullptr), uiDelegate(nullptr), error(KJob::NoError), progressUnit(KJob::Bytes), percentage(0), speedTimer(nullptr), eventLoop(nullptr), capabilities(KJob::NoCapabilities), suspended(false), isAutoDelete(true), isFinished(false) { } KJobPrivate::~KJobPrivate() { } KJob::KJob(QObject *parent) : QObject(parent), d_ptr(new KJobPrivate) { d_ptr->q_ptr = this; } KJob::KJob(KJobPrivate &dd, QObject *parent) : QObject(parent), d_ptr(&dd) { d_ptr->q_ptr = this; } KJob::~KJob() { if (!d_ptr->isFinished) { emit finished(this, QPrivateSignal()); } delete d_ptr->speedTimer; delete d_ptr->uiDelegate; delete d_ptr; } void KJob::setUiDelegate(KJobUiDelegate *delegate) { Q_D(KJob); if (delegate == nullptr || delegate->setJob(this)) { delete d->uiDelegate; d->uiDelegate = delegate; if (d->uiDelegate) { d->uiDelegate->connectJob(this); } } } KJobUiDelegate *KJob::uiDelegate() const { return d_func()->uiDelegate; } KJob::Capabilities KJob::capabilities() const { return d_func()->capabilities; } bool KJob::isSuspended() const { return d_func()->suspended; } void KJob::finishJob(bool emitResult) { Q_D(KJob); d->isFinished = true; if (d->eventLoop) { d->eventLoop->quit(); } // If we are displaying a progress dialog, remove it first. emit finished(this, QPrivateSignal()); if (emitResult) { emit result(this, QPrivateSignal()); } if (isAutoDelete()) { deleteLater(); } } bool KJob::kill(KillVerbosity verbosity) { if (doKill()) { setError(KilledJobError); finishJob(verbosity != Quietly); return true; } else { return false; } } bool KJob::suspend() { Q_D(KJob); if (!d->suspended) { if (doSuspend()) { d->suspended = true; emit suspended(this, QPrivateSignal()); return true; } } return false; } bool KJob::resume() { Q_D(KJob); if (d->suspended) { if (doResume()) { d->suspended = false; emit resumed(this, QPrivateSignal()); return true; } } return false; } bool KJob::doKill() { return false; } bool KJob::doSuspend() { return false; } bool KJob::doResume() { return false; } void KJob::setCapabilities(KJob::Capabilities capabilities) { Q_D(KJob); d->capabilities = capabilities; } bool KJob::exec() { Q_D(KJob); // Usually this job would delete itself, via deleteLater() just after // emitting result() (unless configured otherwise). Since we use an event // loop below, that event loop will process the deletion event and we'll // have been deleted when exec() returns. This crashes, so temporarily // suspend autodeletion and manually do it afterwards. const bool wasAutoDelete = isAutoDelete(); setAutoDelete(false); Q_ASSERT(! d->eventLoop); QEventLoop loop(this); d->eventLoop = &loop; start(); if (!d->isFinished) { d->eventLoop->exec(QEventLoop::ExcludeUserInputEvents); } d->eventLoop = nullptr; if (wasAutoDelete) { deleteLater(); } return (d->error == NoError); } int KJob::error() const { return d_func()->error; } QString KJob::errorText() const { return d_func()->errorText; } QString KJob::errorString() const { return d_func()->errorText; } qulonglong KJob::processedAmount(Unit unit) const { return d_func()->processedAmount[unit]; } qulonglong KJob::totalAmount(Unit unit) const { return d_func()->totalAmount[unit]; } unsigned long KJob::percent() const { return d_func()->percentage; } void KJob::setError(int errorCode) { Q_D(KJob); d->error = errorCode; } void KJob::setErrorText(const QString &errorText) { Q_D(KJob); d->errorText = errorText; } void KJob::setProcessedAmount(Unit unit, qulonglong amount) { Q_D(KJob); bool should_emit = (d->processedAmount[unit] != amount); d->processedAmount[unit] = amount; if (should_emit) { emit processedAmount(this, unit, amount); if (unit == d->progressUnit) { emit processedSize(this, amount); emitPercent(d->processedAmount[unit], d->totalAmount[unit]); } } } void KJob::setTotalAmount(Unit unit, qulonglong amount) { Q_D(KJob); bool should_emit = (d->totalAmount[unit] != amount); d->totalAmount[unit] = amount; if (should_emit) { emit totalAmount(this, unit, amount); if (unit == d->progressUnit) { emit totalSize(this, amount); emitPercent(d->processedAmount[unit], d->totalAmount[unit]); } } } void KJob::setPercent(unsigned long percentage) { Q_D(KJob); if (d->percentage != percentage) { d->percentage = percentage; emit percent(this, percentage); } } void KJob::emitResult() { finishJob(true); } void KJob::emitPercent(qulonglong processedAmount, qulonglong totalAmount) { Q_D(KJob); // calculate percents if (totalAmount) { unsigned long oldPercentage = d->percentage; d->percentage = 100.0 * processedAmount / totalAmount; if (d->percentage != oldPercentage) { emit percent(this, d->percentage); } } } void KJob::emitSpeed(unsigned long value) { Q_D(KJob); if (!d->speedTimer) { d->speedTimer = new QTimer(this); connect(d->speedTimer, SIGNAL(timeout()), SLOT(_k_speedTimeout())); } emit speed(this, value); d->speedTimer->start(5000); // 5 seconds interval should be enough } void KJobPrivate::_k_speedTimeout() { Q_Q(KJob); // send 0 and stop the timer // timer will be restarted only when we receive another speed event emit q->speed(q, 0); speedTimer->stop(); } bool KJob::isAutoDelete() const { Q_D(const KJob); return d->isAutoDelete; } void KJob::setAutoDelete(bool autodelete) { Q_D(KJob); d->isAutoDelete = autodelete; } #include "moc_kjob.cpp" diff --git a/src/lib/kaboutdata.h b/src/lib/kaboutdata.h index 4a822d1..304415d 100644 --- a/src/lib/kaboutdata.h +++ b/src/lib/kaboutdata.h @@ -1,1178 +1,1177 @@ /* * This file is part of the KDE Libraries * Copyright (C) 2000 Espen Sand (espen@kde.org) * Copyright (C) 2008 Friedrich W. H. Kossebau * Copyright (C) 2010 Teo Mrnjavac * Copyright (C) 2013 David Faure * Copyright (C) 2017 Harald Sitter * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #ifndef KABOUTDATA_H #define KABOUTDATA_H #include #include #include -#include #include template class QList; class QCommandLineParser; class QJsonObject; class KAboutData; class KPluginMetaData; namespace KCrash { Q_DECL_IMPORT void defaultCrashHandler(int sig); } /** * This class is used to store information about a person or developer. * It can store the person's name, a task, an email address and a * link to a home page. This class is intended for use in the * KAboutData class, but it can be used elsewhere as well. * Normally you should at least define the person's name. * Creating a KAboutPerson object by yourself is relatively useless, * but the KAboutData methods KAboutData::authors() and KAboutData::credits() * return lists of KAboutPerson data objects which you can examine. * * Example usage within a main(), retrieving the list of people involved * with a program and re-using data from one of them: * * @code * KAboutData about("khello", i18n("KHello"), "0.1", * i18n("A KDE version of Hello, world!"), * KAboutLicense::LGPL, * i18n("Copyright (C) 2014 Developer")); * * about.addAuthor(i18n("Joe Developer"), i18n("developer"), "joe@host.com", 0); * QList people = about.authors(); * about.addCredit(people[0].name(), people[0].task()); * @endcode */ class KCOREADDONS_EXPORT KAboutPerson { Q_GADGET Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString task READ task CONSTANT) Q_PROPERTY(QString emailAddress READ emailAddress CONSTANT) Q_PROPERTY(QString webAddress READ webAddress CONSTANT) Q_PROPERTY(QString ocsUsername READ ocsUsername CONSTANT) friend class KAboutData; public: /** * Convenience constructor * * @param name The name of the person. * * @param task The task of this person. * * @param emailAddress The email address of the person. * * @param webAddress Home page of the person. * * @param ocsUsername Open Collaboration Services username of the person. * * @p name default argument @since 5.53 */ explicit KAboutPerson(const QString &name = QString(), const QString &task = QString(), const QString &emailAddress = QString(), const QString &webAddress = QString(), const QString &ocsUsername = QString()); /** * Copy constructor. Performs a deep copy. * @param other object to copy */ KAboutPerson(const KAboutPerson &other); ~KAboutPerson(); /** * Assignment operator. Performs a deep copy. * @param other object to copy */ KAboutPerson &operator=(const KAboutPerson &other); /** * The person's name * @return the person's name (can be QString(), if it has been * constructed with an empty name) */ QString name() const; /** * The person's task * @return the person's task (can be QString(), if it has been * constructed with an empty task) */ QString task() const; /** * The person's email address * @return the person's email address (can be QString(), if it has been * constructed with an empty email) */ QString emailAddress() const; /** * The home page or a relevant link * @return the persons home page (can be QString(), if it has been * constructed with an empty home page) */ QString webAddress() const; /** * The person's Open Collaboration Services username * @return the persons OCS username (can be QString(), if it has been * constructed with an empty username) */ QString ocsUsername() const; /** * Creates a @c KAboutPerson from a JSON object with the following structure: * * Key | Accessor * -----------| ---------------------------- * Name | name() * Email | emailAddress() * Task | task() * Website | webAddress() * UserName | ocsUsername() * * The @c Name and @c Task key are translatable (by using e.g. a "Task[de_DE]" key) * * @since 5.18 */ static KAboutPerson fromJSON(const QJsonObject &obj); private: /** * @internal Used by KAboutData to construct translator data. */ explicit KAboutPerson(const QString &name, const QString &email, bool disambiguation); class Private; Private *const d; }; /** * This class is used to store information about a license. * The license can be one of some predefined, one given as text or one * that can be loaded from a file. This class is used in the KAboutData class. * Explicitly creating a KAboutLicense object is not possible. * If the license is wanted for a KDE component having KAboutData object, * use KAboutData::licenses() to get the licenses for that component. * If the license is for a non-code resource and given by a keyword * (e.g. in .desktop files), try using KAboutLicense::byKeyword(). */ class KCOREADDONS_EXPORT KAboutLicense { Q_GADGET Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString text READ text CONSTANT) Q_PROPERTY(KAboutLicense::LicenseKey key READ key CONSTANT) Q_PROPERTY(QString spdx READ spdx CONSTANT) friend class KAboutData; public: /** * Describes the license of the software. */ enum LicenseKey { Custom = -2, File = -1, Unknown = 0, GPL = 1, GPL_V2 = 1, LGPL = 2, LGPL_V2 = 2, BSDL = 3, Artistic = 4, QPL = 5, QPL_V1_0 = 5, GPL_V3 = 6, LGPL_V3 = 7, LGPL_V2_1 = 8 ///< @since 5.25 }; Q_ENUM(LicenseKey) /** * Format of the license name. */ enum NameFormat { ShortName, FullName }; Q_ENUM(NameFormat) /** * Whether later versions of the license are allowed. */ enum VersionRestriction { OnlyThisVersion, OrLaterVersions }; Q_ENUM(VersionRestriction) /** * @since 5.53 */ explicit KAboutLicense(); /** * Copy constructor. Performs a deep copy. * @param other object to copy */ KAboutLicense(const KAboutLicense &other); ~KAboutLicense(); /** * Assignment operator. Performs a deep copy. * @param other object to copy */ KAboutLicense &operator=(const KAboutLicense &other); /** * Returns the full license text. If the licenseType argument of the * constructor has been used, any text defined by setLicenseText is ignored, * and the standard text for the chosen license will be returned. * * @return The license text. */ QString text() const; /** * Returns the license name. * * Default argument @since 5.53 * * @return The license name as a string. */ QString name(KAboutLicense::NameFormat formatName = ShortName) const; /** * Returns the license key. * * @return The license key as element of KAboutLicense::LicenseKey enum. */ KAboutLicense::LicenseKey key() const; /** * Returns the SPDX license expression of this license. * If the underlying license cannot be expressed as a SPDX expression a null string is returned. * * @note SPDX expression are expansive constructs. If you parse the return value, do it in a * SPDX specification compliant manner by splitting on whitespaces to discard unwanted * information or by using a complete SPDX license expression parser. * @note SPDX identifiers are case-insensitive. Do not use case-sensitive checks on the return * value. * @see https://spdx.org/licenses * @return SPDX license expression or QString() if the license has no identifier. Compliant * with SPDX 2.1. * * @since 5.37 */ QString spdx() const; /** * Fetch a known license by a keyword/spdx ID * * Frequently the license data is provided by a terse keyword-like string, * e.g. by a field in a .desktop file. Using this method, an application * can get hold of a proper KAboutLicense object, providing that the * license is one of the several known to KDE, and use it to present * more human-readable information to the user. * * Keywords are matched by stripping all whitespace and lowercasing. * The known keywords correspond to the KAboutLicense::LicenseKey enumeration, * e.g. any of "LGPLV3", "LGPLv3", "LGPL v3" would match KAboutLicense::LGPL_V3. * If there is no match for the keyword, a valid license object is still * returned, with its name and text informing about a custom license, * and its key equal to KAboutLicense::Custom. * * @param keyword The license keyword. * @return The license object. * * @see KAboutLicense::LicenseKey */ static KAboutLicense byKeyword(const QString &keyword); private: /** * @internal Used by KAboutData to construct a predefined license. */ explicit KAboutLicense(enum KAboutLicense::LicenseKey licenseType, enum KAboutLicense::VersionRestriction versionRestriction, const KAboutData *aboutData); /** * @internal Used by KAboutData to construct a predefined license. */ explicit KAboutLicense(enum KAboutLicense::LicenseKey licenseType, const KAboutData *aboutData); /** * @internal Used by KAboutData to construct a KAboutLicense */ explicit KAboutLicense(const KAboutData *aboutData); /** * @internal Used by KAboutData to construct license by given text */ void setLicenseFromPath(const QString &pathToFile); /** * @internal Used by KAboutData to construct license by given text */ void setLicenseFromText(const QString &licenseText); class Private; QSharedDataPointer d; }; /** * @class KAboutData kaboutdata.h KAboutData * * This class is used to store information about a program or plugin. * It can store such values as version number, program name, home page, address * for bug reporting, multiple authors and contributors * (using KAboutPerson), license and copyright information. * * Currently, the values set here are shown by the "About" box * (see KAboutDialog), used by the bug report dialog (see KBugReport), * and by the help shown on command line (see KAboutData::setupCommandLine()). * * Porting Notes: Since KDE Frameworks 5.0, the translation catalog mechanism * must provided by your translation framework to load the correct catalog * instead (eg: KLocalizedString::setApplicationDomain() for KI18n, or * QCoreApplication::installTranslator() for Qt's translation system). This * applies to the old setCatalogName() and catalogName() members. But see also * K4AboutData in kde4support as a compatibility class. * * Example: * Setting the metadata of an application using KAboutData in code also relying * on the KDE Framework modules KI18n and KDBusAddons: * @code * // create QApplication instance * QApplication app(argc, argv); * // setup translation string domain for the i18n calls * KLocalizedString::setApplicationDomain("foo"); * // create a KAboutData object to use for setting the application metadata * KAboutData aboutData("foo", i18n("Foo"), "0.1", * i18n("To Foo or not To Foo"), * KAboutLicense::LGPL, * i18n("Copyright 2017 Bar Foundation"), QString(), * "https://www.foo-the-app.net"); * // overwrite default-generated values of organizationDomain & desktopFileName * aboutData.setOrganizationDomain("barfoundation.org"); * aboutData.setDesktopFileName("org.barfoundation.foo"); * * // set the application metadata * KAboutData::setApplicationData(aboutData); * // in GUI apps set the window icon manually, not covered by KAboutData * // needed for environments where the icon name is not extracted from * // the information in the application's desktop file * QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("foo"))); * * // integrate with commandline argument handling * QCommandLineParser parser; * aboutData.setupCommandLine(&parser); * // setup of app specific commandline args * [...] * parser.process(app); * aboutData.processCommandLine(&parser); * * // with the application metadata set, register to the D-Bus session * KDBusService programDBusService(KDBusService::Multiple | KDBusService::NoExitOnFailure); * @endcode * * @short Holds information needed by the "About" box and other * classes. * @author Espen Sand (espen@kde.org), David Faure (faure@kde.org) * */ class KCOREADDONS_EXPORT KAboutData { Q_GADGET Q_PROPERTY(QString displayName READ displayName CONSTANT) Q_PROPERTY(QString productName READ productName CONSTANT) Q_PROPERTY(QString componentName READ componentName CONSTANT) Q_PROPERTY(QVariant programLogo READ programLogo CONSTANT) Q_PROPERTY(QString shortDescription READ shortDescription CONSTANT) Q_PROPERTY(QString homepage READ homepage CONSTANT) Q_PROPERTY(QString bugAddress READ bugAddress CONSTANT) Q_PROPERTY(QString version READ version CONSTANT) Q_PROPERTY(QString otherText READ otherText CONSTANT) Q_PROPERTY(QVariantList authors READ authorsVariant CONSTANT) //constant in practice as addAuthor is not exposed to Q_GADGET Q_PROPERTY(QVariantList credits READ creditsVariant CONSTANT) Q_PROPERTY(QVariantList translators READ translatorsVariant CONSTANT) Q_PROPERTY(QVariantList licenses READ licensesVariant CONSTANT) Q_PROPERTY(QString copyrightStatement READ copyrightStatement CONSTANT) Q_PROPERTY(QString desktopFileName READ desktopFileName CONSTANT) public: /** * Returns the KAboutData for the application. * * This contains information such as authors, license, etc., * provided that setApplicationData has been called before. * If not called before, the returned KAboutData will be initialized from the * equivalent properties of QCoreApplication (and its subclasses), * if an instance of that already exists. * For the list of such properties see setApplicationData * (before 5.22: limited to QCoreApplication::applicationName). * @see setApplicationData */ static KAboutData applicationData(); /** * Sets the application data for this application. * * In addition to changing the result of applicationData(), this initializes * the equivalent properties of QCoreApplication (and its subclasses) with * information from @p aboutData, if an instance of that already exists. * Those properties are:
  • QCoreApplication::applicationName
  • QCoreApplication::applicationVersion
  • QCoreApplication::organizationDomain
  • QGuiApplication::applicationDisplayName
  • QGuiApplication::desktopFileName (since 5.16)
* @see applicationData */ static void setApplicationData(const KAboutData &aboutData); /** * Register the KAboutData information for a plugin. * Call this from the constructor of the plugin. * This will register the plugin's @p aboutData under the component name * that was set in @p aboutData. */ static void registerPluginData(const KAboutData &aboutData); /** * Return the KAboutData for the given plugin identified by @p componentName. */ static KAboutData *pluginData(const QString &componentName); /** * Creates a @c KAboutData from the given @p plugin metadata * * @since 5.18 */ static KAboutData fromPluginMetaData(const KPluginMetaData &plugin); public: /** * Constructor. * * Porting Note: The @p catalogName parameter present in KDE4 was * deprecated and removed. See also K4AboutData * in kde4support if this feature is needed for compatibility purposes, or * consider using componentName() instead. * * @param componentName The program name or plugin name used internally. * Example: QStringLiteral("kwrite"). This should never be translated. * * @param displayName A displayable name for the program or plugin. This string * should be translated. Example: i18n("KWrite") * * @param version The component version string. Example: QStringLiteral("1.0"). * * @param shortDescription A short description of what the component does. * This string should be translated. * Example: i18n("A simple text editor.") * * @param licenseType The license identifier. Use setLicenseText or setLicenseTextFile if you use a license not predefined here. * * @param copyrightStatement A copyright statement, that can look like this: * i18n("Copyright (C) 1999-2000 Name"). The string specified here is * taken verbatim; the author information from addAuthor is not used. * * @param otherText Some free form text, that can contain any kind of * information. The text can contain newlines. This string * should be translated. * * @param homePageAddress The URL to the component's homepage, including * URL scheme. "http://some.domain" is correct, "some.domain" is * not. Since KDE Frameworks 5.17, https and other valid URL schemes * are also valid. See also the note below. * * @param bugAddress The bug report address string, an email address or a URL. * This defaults to the kde.org bug system. * * @note The @p homePageAddress argument is used to derive a default organization * domain for the application (which is used to register on the session D-Bus, * locate the appropriate desktop file, etc.), by taking the host name and dropping * the first component, unless there are less than three (e.g. "www.kde.org" -> "kde.org"). * Use both setOrganizationDomain(const QByteArray&) and setDesktopFileName() if their default values * do not have proper values. * * @see setOrganizationDomain(const QByteArray&), setDesktopFileName(const QString&) */ // KF6: remove constructor that includes catalogName, and put default // values back in for shortDescription and licenseType KAboutData(const QString &componentName, const QString &displayName, const QString &version, const QString &shortDescription, enum KAboutLicense::LicenseKey licenseType, const QString ©rightStatement = QString(), const QString &otherText = QString(), const QString &homePageAddress = QString(), const QString &bugAddress = QStringLiteral("submit@bugs.kde.org") ); /** * Constructor. * * @param componentName The program name or plugin name used internally. * Example: "kwrite". * * @param displayName A displayable name for the program or plugin. This string * should be translated. Example: i18n("KWrite") * * @param version The component version string. * * Sets the property desktopFileName to "org.kde."+componentName and * the property organizationDomain to "kde.org". * * Default arguments @since 5.53 * * @see setOrganizationDomain(const QByteArray&), setDesktopFileName(const QString&) */ explicit KAboutData(const QString &componentName = {}, const QString &displayName = {}, const QString &version = {} ); /** * Copy constructor. Performs a deep copy. * @param other object to copy */ KAboutData(const KAboutData &other); /** * Assignment operator. Performs a deep copy. * @param other object to copy */ KAboutData &operator=(const KAboutData &other); ~KAboutData(); /** * Defines an author. * * You can call this function as many times as you need. Each entry is * appended to a list. The person in the first entry is assumed to be * the leader of the project. * * @param name The developer's name. It should be translated. * * @param task What the person is responsible for. This text can contain * newlines. It should be translated. * Can be left empty. * * @param emailAddress An Email address where the person can be reached. * Can be left empty. * * @param webAddress The person's homepage or a relevant link. * Start the address with "http://". "http://some.domain" is * correct, "some.domain" is not. Can be left empty. * * @param ocsUsername The person's Open Collaboration Services username. * The provider can be optionally specified with @see setOcsProvider. * */ KAboutData &addAuthor(const QString &name, const QString &task = QString(), const QString &emailAddress = QString(), const QString &webAddress = QString(), const QString &ocsUsername = QString()); /** * Defines a person that deserves credit. * * You can call this function as many times as you need. Each entry * is appended to a list. * * @param name The person's name. It should be translated. * * @param task What the person has done to deserve the honor. The * text can contain newlines. It should be translated. * Can be left empty. * * @param emailAddress An email address when the person can be reached. * Can be left empty. * * @param webAddress The person's homepage or a relevant link. * Start the address with "http://". "http://some.domain" is * is correct, "some.domain" is not. Can be left empty. * * @param ocsUsername The person's Open Collaboration Services username. * The provider can be optionally specified with @see setOcsProvider. * */ KAboutData &addCredit(const QString &name, const QString &task = QString(), const QString &emailAddress = QString(), const QString &webAddress = QString(), const QString &ocsUsername = QString()); /** * @brief Sets the name(s) of the translator(s) of the GUI. * * The canonical use with the ki18n framework is: * * \code * setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), * i18nc("EMAIL OF TRANSLATORS", "Your emails")); * \endcode * * If you are using a KMainWindow this is done for you automatically. * * The name and emailAddress are treated as lists separated with ",". * * If the strings are empty or "Your names"/"Your emails" * respectively they will be ignored. * * @param name the name(s) of the translator(s) * @param emailAddress the email address(es) of the translator(s) * @see KAboutTranslator */ KAboutData &setTranslator(const QString &name, const QString &emailAddress); /** * Defines a license text, which is translated. * * Example: * \code * setLicenseText( i18n("This is my license") ); * \endcode * * @param license The license text. */ KAboutData &setLicenseText(const QString &license); /** * Adds a license text, which is translated. * * If there is only one unknown license set, e.g. by using the default * parameter in the constructor, that one is replaced. * * Example: * \code * addLicenseText( i18n("This is my license") ); * \endcode * * @param license The license text. * @see setLicenseText, addLicense, addLicenseTextFile */ KAboutData &addLicenseText(const QString &license); /** * Defines a license text by pointing to a file where it resides. * The file format has to be plain text in an encoding compatible to the locale. * * @param file Path to the file in the local filesystem containing the license text. */ KAboutData &setLicenseTextFile(const QString &file); /** * Adds a license text by pointing to a file where it resides. * The file format has to be plain text in an encoding compatible to the locale. * * If there is only one unknown license set, e.g. by using the default * parameter in the constructor, that one is replaced. * * @param file Path to the file in the local filesystem containing the license text. * @see addLicenseText, addLicense, setLicenseTextFile */ KAboutData &addLicenseTextFile(const QString &file); /** * Defines the component name used internally. * * @param componentName The application or plugin name. Example: "kate". */ KAboutData &setComponentName(const QString &componentName); /** * Defines the displayable component name string. * * @param displayName The display name. This string should be * translated. * Example: i18n("Advanced Text Editor"). */ KAboutData &setDisplayName(const QString &displayName); /** * Obsolete method * * This method used to set the icon name but this is no longer * possible in KDE Frameworks 5 because KCoreAddons does not * depend on QtGui. * * @param iconName name of the icon. Example: "accessories-text-editor" * @see programIconName() * * @deprecated since 5.2, use QApplication::setWindowIcon(QIcon::fromTheme()) instead. */ KCOREADDONS_DEPRECATED KAboutData &setProgramIconName(const QString &iconName); // KF6 remove this /** * Defines the program logo. * * Use this if you need to have an application logo * in AboutData other than the application icon. * * Because KAboutData is a core class it cannot use QImage directly, * so this is a QVariant that should contain a QImage. * * @param image logo image. * @see programLogo() */ KAboutData &setProgramLogo(const QVariant &image); /** * Specifies an Open Collaboration Services provider by URL. * A provider file must be available for the chosen provider. * * Use this if you need to override the default provider. * * If this method is not used, all the KAboutPerson OCS usernames * will be used with the openDesktop.org entry from the default * provider file. * * @param providerUrl The provider URL as defined in the provider file. */ KAboutData &setOcsProvider(const QString &providerUrl); /** * Defines the program version string. * * @param version The program version. */ KAboutData &setVersion(const QByteArray &version); /** * Defines a short description of what the program does. * * @param shortDescription The program description. This string should * be translated. Example: i18n("An advanced text * editor with syntax highlighting support."). */ KAboutData &setShortDescription(const QString &shortDescription); /** * Defines the license identifier. * * @param licenseKey The license identifier. * @see addLicenseText, setLicenseText, setLicenseTextFile */ KAboutData &setLicense(KAboutLicense::LicenseKey licenseKey); /** * Defines the license identifier. * * @param licenseKey The license identifier. * @param versionRestriction Whether later versions of the license are also allowed. * e.g. licensed under "GPL 2.0 or at your option later versions" would be OrLaterVersions. * @see addLicenseText, setLicenseText, setLicenseTextFile * * @since 5.37 */ KAboutData &setLicense(KAboutLicense::LicenseKey licenseKey, KAboutLicense::VersionRestriction versionRestriction); /** * Adds a license identifier. * * If there is only one unknown license set, e.g. by using the default * parameter in the constructor, that one is replaced. * * @param licenseKey The license identifier. * @see setLicenseText, addLicenseText, addLicenseTextFile */ KAboutData &addLicense(KAboutLicense::LicenseKey licenseKey); /** * Adds a license identifier. * * If there is only one unknown license set, e.g. by using the default * parameter in the constructor, that one is replaced. * * @param licenseKey The license identifier. * @param versionRestriction Whether later versions of the license are also allowed. * e.g. licensed under "GPL 2.0 or at your option later versions" would be OrLaterVersions. * @see setLicenseText, addLicenseText, addLicenseTextFile * * @since 5.37 */ KAboutData &addLicense(KAboutLicense::LicenseKey licenseKey, KAboutLicense::VersionRestriction versionRestriction); /** * Defines the copyright statement to show when displaying the license. * * @param copyrightStatement A copyright statement, that can look like * this: i18n("Copyright (C) 1999-2000 Name"). The string specified here is * taken verbatim; the author information from addAuthor is not used. */ KAboutData &setCopyrightStatement(const QString ©rightStatement); /** * Defines the additional text to show in the about dialog. * * @param otherText Some free form text, that can contain any kind of * information. The text can contain newlines. This string * should be translated. */ KAboutData &setOtherText(const QString &otherText); /** * Defines the program homepage. * * @param homepage The program homepage string. * Start the address with "http://". "http://kate.kde.org" * is correct but "kate.kde.org" is not. */ KAboutData &setHomepage(const QString &homepage); /** * Defines the address where bug reports should be sent. * * @param bugAddress The bug report email address or URL. * This defaults to the kde.org bug system. */ KAboutData &setBugAddress(const QByteArray &bugAddress); /** * Defines the domain of the organization that wrote this application. * The domain is set to kde.org by default, or the domain of the homePageAddress constructor argument, * if set. * * Make sure to call setOrganizationDomain(const QByteArray&) if your product * is not developed inside the KDE community. * * Used e.g. for the registration to D-Bus done by KDBusService * from the KDE Frameworks KDBusAddons module. * * Calling this method has no effect on the value of the desktopFileName property. * * @note If your program should work as a D-Bus activatable service, the base name * of the D-Bus service description file or of the desktop file you install must match * the D-Bus "well-known name" for which the program will register. * For example, KDBusService will use a name created from the reversed organization domain * with the component name attached, so for an organization domain "bar.org" and a * component name "foo" the name of an installed D-Bus service file needs to be * "org.bar.foo.service" or the name of the installed desktop file "org.bar.foo.desktop" * (and the desktopFileName property accordingly set to "org.bar.foo"). * For still supporting the deprecated start of services via KToolInvocation, * the desktop file needs to have an entry with the key "X-DBUS-ServiceName" * and a value which matches the used D-Bus "well-known name" as just described, * so with the above used values it needs a line "X-DBUS-ServiceName=org.bar.foo" * * @param domain the domain name, for instance kde.org, koffice.org, etc. * * @see setDesktopFileName(const QString&) */ KAboutData &setOrganizationDomain(const QByteArray &domain); /** * Defines the product name which will be used in the KBugReport dialog. * By default it's the componentName, but you can overwrite it here to provide * support for special components e.g. in the form 'product/component', * such as 'kontact/summary'. * * @param name The name of product */ KAboutData &setProductName(const QByteArray &name); /** * Returns the application's internal name. * @return the internal program name. */ QString componentName() const; /** * Returns the application's product name, which will be used in KBugReport * dialog. By default it returns componentName(), otherwise the one which is set * with setProductName() * * @return the product name. */ QString productName() const; /** * Returns the translated program name. * @return the program name (translated). */ QString displayName() const; /** * Returns the domain name of the organization that wrote this application. * * @see setOrganizationDomain(const QByteArray&) */ QString organizationDomain() const; /** * @internal * Provided for use by KCrash */ const char *internalProgramName() const; /** * Returns the program's icon name. * * The default value is componentName(). * @return the program's icon name. * * This is mostly for compatibility, given that setProgramIconName is deprecated. */ QString programIconName() const; /** * Returns the program logo image. * * Because KAboutData is a core class it cannot use QImage directly, * so this is a QVariant containing a QImage. * * @return the program logo data, or a null image if there is * no custom application logo defined. */ QVariant programLogo() const; /** * Returns the chosen Open Collaboration Services provider URL. * @return the provider URL. */ QString ocsProviderUrl() const; /** * Returns the program's version. * @return the version string. */ QString version() const; /** * @internal * Provided for use by KCrash */ const char *internalVersion() const; /** * Returns a short, translated description. * @return the short description (translated). Can be * QString() if not set. */ QString shortDescription() const; /** * Returns the application homepage. * @return the application homepage URL. Can be QString() if * not set. */ QString homepage() const; /** * Returns the email address or URL for bugs. * @return the address where to report bugs. */ QString bugAddress() const; /** * @internal * Provided for use by KCrash */ const char *internalBugAddress() const; /** * Returns a list of authors. * @return author information (list of persons). */ QList authors() const; /** * Returns a list of persons who contributed. * @return credit information (list of persons). */ QList credits() const; /** * Returns a list of translators. * @return translators information (list of persons) */ QList translators() const; /** * Returns a message about the translation team. * @return a message about the translation team */ static QString aboutTranslationTeam(); /** * Returns a translated, free form text. * @return the free form text (translated). Can be QString() if not set. */ QString otherText() const; /** * Returns a list of licenses. * * @return licenses information (list of licenses) */ QList licenses() const; /** * Returns the copyright statement. * @return the copyright statement. Can be QString() if not set. */ QString copyrightStatement() const; /** * Returns the plain text displayed around the list of authors instead * of the default message telling users to send bug reports to bugAddress(). * * @return the plain text displayed around the list of authors instead * of the default message. Can be QString(). */ QString customAuthorPlainText() const; /** * Returns the rich text displayed around the list of authors instead * of the default message telling users to send bug reports to bugAddress(). * * @return the rich text displayed around the list of authors instead * of the default message. Can be QString(). */ QString customAuthorRichText() const; /** * Returns whether custom text should be displayed around the list of * authors. * * @return whether custom text should be displayed around the list of * authors. */ bool customAuthorTextEnabled() const; /** * Sets the custom text displayed around the list of authors instead * of the default message telling users to send bug reports to bugAddress(). * * @param plainText The plain text. * @param richText The rich text. * * Setting both to parameters to QString() will cause no message to be * displayed at all. Call unsetCustomAuthorText() to revert to the default * message. */ KAboutData &setCustomAuthorText(const QString &plainText, const QString &richText); /** * Clears any custom text displayed around the list of authors and falls * back to the default message telling users to send bug reports to * bugAddress(). */ KAboutData &unsetCustomAuthorText(); /** * Configures the @p parser command line parser to provide an authors entry with * information about the developers of the application and an entry specifying the license. * * Additionally, it will set the description to the command line parser, will add the help * option and if the QApplication has a version set (e.g. via KAboutData::setApplicationData) * it will also add the version option. * * Since 5.16 it also adds an option to set the desktop file name. * * @returns true if adding the options was successful; otherwise returns false. * * @sa processCommandLine() */ bool setupCommandLine(QCommandLineParser *parser); /** * Reads the processed @p parser and sees if any of the arguments are the ones set * up from setupCommandLine(). * * @sa setupCommandLine() */ void processCommandLine(QCommandLineParser *parser); /** * Sets the base name of the desktop entry for this application. * * This is the file name, without the full path and without extension, * of the desktop entry that represents this application according to * the freedesktop desktop entry specification (e.g. "org.kde.foo"). * * A default desktop file name is constructed when the KAboutData * object is created, using the reverse domain name of the * organizationDomain() and the componentName() as they are at the time * of the KAboutData object creation. * Call this method to override that default name. Typically this is * done when also setOrganizationDomain(const QByteArray&) or setComponentName(const QString&) * need to be called to override the initial values. * * The desktop file name can also be passed to the application at runtime through * the @c desktopfile command line option which is added by setupCommandLine(QCommandLineParser*). * This is useful if an application supports multiple desktop files with different runtime * settings. * * @param desktopFileName The desktop file name of this application * * @sa desktopFileName() * @sa organizationDomain() * @sa componentName() * @sa setupCommandLine(QCommandLineParser*) * @since 5.16 **/ KAboutData &setDesktopFileName(const QString &desktopFileName); /** * @returns The desktop file name of this application (e.g. "org.kde.foo") * @sa setDesktopFileName(const QString&) * @since 5.16 **/ QString desktopFileName() const; private: QVariantList licensesVariant() const; QVariantList authorsVariant() const; QVariantList creditsVariant() const; QVariantList translatorsVariant() const; friend void KCrash::defaultCrashHandler(int sig); static const KAboutData *applicationDataPointer(); class Private; Private *const d; }; Q_DECLARE_METATYPE(KAboutData) Q_DECLARE_METATYPE(KAboutLicense) Q_DECLARE_METATYPE(KAboutPerson) #endif diff --git a/src/lib/plugin/kpluginloader.cpp b/src/lib/plugin/kpluginloader.cpp index 1801c7c..8bb5d3f 100644 --- a/src/lib/plugin/kpluginloader.cpp +++ b/src/lib/plugin/kpluginloader.cpp @@ -1,309 +1,308 @@ /* This file is part of the KDE project Copyright (C) 2007 Bernhard Loos This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kpluginloader.h" #include "kpluginfactory.h" #include "kpluginmetadata.h" #include #include #include -#include #include "kcoreaddons_debug.h" #include #include // TODO: Upstream the versioning stuff to Qt // TODO: Patch for Qt to expose plugin-finding code directly // TODO: Add a convenience method to KFactory to replace KPluginLoader::factory() // TODO: (after the above) deprecate this class class KPluginLoaderPrivate { Q_DECLARE_PUBLIC(KPluginLoader) protected: KPluginLoaderPrivate(const QString &libname) : name(libname), loader(nullptr), pluginVersion(~0U), pluginVersionResolved(false) {} ~KPluginLoaderPrivate() {} KPluginLoader *q_ptr; const QString name; QString errorString; QPluginLoader *loader; quint32 pluginVersion; bool pluginVersionResolved; }; QString KPluginLoader::findPlugin(const QString &name) { // We just defer to Qt; unfortunately, QPluginLoader's searching code is not // accessible without creating a QPluginLoader object. // Workaround for QTBUG-39642 static QMutex s_qtWorkaroundMutex; QMutexLocker lock(&s_qtWorkaroundMutex); QPluginLoader loader(name); return loader.fileName(); } KPluginLoader::KPluginLoader(const QString &plugin, QObject *parent) : QObject(parent), d_ptr(new KPluginLoaderPrivate(plugin)) { d_ptr->q_ptr = this; Q_D(KPluginLoader); d->loader = new QPluginLoader(plugin, this); } KPluginLoader::KPluginLoader(const KPluginName &pluginName, QObject *parent) : QObject(parent), d_ptr(new KPluginLoaderPrivate(pluginName.name())) { d_ptr->q_ptr = this; Q_D(KPluginLoader); d->loader = new QPluginLoader(this); if (pluginName.isValid()) { d->loader->setFileName(pluginName.name()); if (d->loader->fileName().isEmpty()) { qCWarning(KCOREADDONS_DEBUG) << "Error loading plugin" << pluginName.name() << d->loader->errorString() << endl << "Plugin search paths are" << QCoreApplication::libraryPaths() << endl << "The environment variable QT_PLUGIN_PATH might be not correctly set"; } } else { d->errorString = pluginName.errorString(); } } KPluginLoader::~KPluginLoader() { delete d_ptr; } KPluginFactory *KPluginLoader::factory() { Q_D(KPluginLoader); QObject *obj = instance(); if (!obj) { return nullptr; } KPluginFactory *factory = qobject_cast(obj); if (factory == nullptr) { qCDebug(KCOREADDONS_DEBUG) << "Expected a KPluginFactory, got a" << obj->metaObject()->className(); delete obj; d->errorString = tr("The library %1 does not offer a KPluginFactory.").arg(d->name); } return factory; } quint32 KPluginLoader::pluginVersion() { Q_D(const KPluginLoader); if (!load()) { return qint32(-1); } return d->pluginVersion; } QString KPluginLoader::pluginName() const { Q_D(const KPluginLoader); return d->name; } QString KPluginLoader::errorString() const { Q_D(const KPluginLoader); if (!d->errorString.isEmpty()) { return d->errorString; } return d->loader->errorString(); } QString KPluginLoader::fileName() const { Q_D(const KPluginLoader); return d->loader->fileName(); } QObject *KPluginLoader::instance() { Q_D(const KPluginLoader); if (!load()) { return nullptr; } return d->loader->instance(); } bool KPluginLoader::isLoaded() const { Q_D(const KPluginLoader); return d->loader->isLoaded() && d->pluginVersionResolved; } bool KPluginLoader::load() { Q_D(KPluginLoader); if (!d->loader->load()) { return false; } if (d->pluginVersionResolved) { return true; } Q_ASSERT(!fileName().isEmpty()); QLibrary lib(fileName()); Q_ASSERT(lib.isLoaded()); // already loaded by QPluginLoader::load() // TODO: this messes up KPluginLoader::errorString(): it will change from unknown error to could not resolve kde_plugin_version quint32 *version = reinterpret_cast(lib.resolve("kde_plugin_version")); if (version) { d->pluginVersion = *version; } else { d->pluginVersion = ~0U; } d->pluginVersionResolved = true; return true; } QLibrary::LoadHints KPluginLoader::loadHints() const { Q_D(const KPluginLoader); return d->loader->loadHints(); } QJsonObject KPluginLoader::metaData() const { Q_D(const KPluginLoader); return d->loader->metaData(); } void KPluginLoader::setLoadHints(QLibrary::LoadHints loadHints) { Q_D(KPluginLoader); d->loader->setLoadHints(loadHints); } bool KPluginLoader::unload() { Q_D(KPluginLoader); // Even if *this* call does not unload it, another might, // so we err on the side of re-resolving the version. d->pluginVersionResolved = false; return d->loader->unload(); } void KPluginLoader::forEachPlugin(const QString &directory, std::function callback) { QStringList dirsToCheck; if (QDir::isAbsolutePath(directory)) { dirsToCheck << directory; } else { const QStringList listPaths = QCoreApplication::libraryPaths(); for (const QString &libDir : listPaths) { dirsToCheck << libDir + QLatin1Char('/') + directory; } } qCDebug(KCOREADDONS_DEBUG) << "Checking for plugins in" << dirsToCheck; for (const QString &dir : qAsConst(dirsToCheck)) { QDirIterator it(dir, QDir::Files); while (it.hasNext()) { it.next(); if (QLibrary::isLibrary(it.fileName())) { callback(it.fileInfo().absoluteFilePath()); } } } } QVector KPluginLoader::findPlugins(const QString &directory, std::function filter) { QVector ret; forEachPlugin(directory, [&](const QString &pluginPath) { KPluginMetaData metadata(pluginPath); if (!metadata.isValid()) { return; } if (filter && !filter(metadata)) { return; } ret.append(metadata); }); return ret; } QVector< KPluginMetaData > KPluginLoader::findPluginsById(const QString& directory, const QString& pluginId) { auto filter = [&pluginId](const KPluginMetaData &md) -> bool { return md.pluginId() == pluginId; }; return KPluginLoader::findPlugins(directory, filter); } QList KPluginLoader::instantiatePlugins(const QString &directory, std::function filter, QObject* parent) { QList ret; QPluginLoader loader; const QVector listMetaData = findPlugins(directory, filter); for (const KPluginMetaData &metadata : listMetaData) { loader.setFileName(metadata.fileName()); QObject* obj = loader.instance(); if (!obj) { qCWarning(KCOREADDONS_DEBUG).nospace() << "Could not instantiate plugin \"" << metadata.fileName() << "\": " << loader.errorString(); continue; } obj->setParent(parent); ret.append(obj); } return ret; } diff --git a/src/lib/plugin/kpluginloader.h b/src/lib/plugin/kpluginloader.h index 70e972e..3b11d49 100644 --- a/src/lib/plugin/kpluginloader.h +++ b/src/lib/plugin/kpluginloader.h @@ -1,476 +1,475 @@ /* This file is part of the KDE project Copyright (C) 2007 Bernhard Loos This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KPLUGINLOADER_H #define KPLUGINLOADER_H #include #include -#include #include class KPluginFactory; class KPluginMetaData; class KPluginLoaderPrivate; class KPluginName; /** * \class KPluginLoader kpluginloader.h * * This class behaves largely like QPluginLoader (and, indeed, uses it * internally), but additionally reads the plugin version, as provided by the * K_EXPORT_PLUGIN_VERSION macro (see pluginVersion()) and provides access to a * KPluginFactory instance if the plugin provides one (see factory()) * * Note that the factory() is a typesafe convenience method that just wraps a * qobject_cast on the result of QPluginLoader::instance(). Therefore, if you do * not need the plugin version feature, you can (and should) just use * QPluginLoader instead. * * Unlike QPluginLoader, it is not possible to re-use KPluginLoader for more * than one plugin (it provides no setFileName method). * * The same notes and caveats that apply to QPluginLoader also apply to * KPluginLoader. * * Sample code: * \code * KPluginLoader loader( ...library or kservice... ); * KPluginFactory* factory = loader.factory(); * if (!factory) { * qWarning() << "Error loading plugin:" << loader.errorString(); * } else { * MyInterface* obj = factory->create(); * if (!obj) { * qWarning() << "Error creating object"; * } * } * \endcode * * \see KPluginFactory * * \author Bernhard Loos */ class KCOREADDONS_EXPORT KPluginLoader : public QObject { Q_OBJECT Q_PROPERTY(QString fileName READ fileName) Q_PROPERTY(QLibrary::LoadHints loadHints READ loadHints WRITE setLoadHints) Q_PROPERTY(QString pluginName READ pluginName) Q_PROPERTY(quint32 pluginVersion READ pluginVersion) public: /** * Load a plugin by name. * * This should be the name of the plugin object file, without any suffix * (like .so or .dll). Plugin object files should not have a 'lib' prefix. * * fileName() will be empty if the plugin could not be found. * * \param plugin The name of the plugin. * \param parent A parent object. */ explicit KPluginLoader(const QString &plugin, QObject *parent = nullptr); /** * Load a plugin by name. * * This constructor behaves exactly the same as * KPluginLoader(const QString&,QObject*). It allows any class that can be * cast to KPluginName (such as KService) to be passed to KPluginLoader. * * \param name The name of the plugin. * \param parent A parent object. */ explicit KPluginLoader(const KPluginName &name, QObject *parent = nullptr); /** * Destroys the plugin loader. * * Unless unload() was called explicitly, the plugin stays in memory until * the application terminates. */ ~KPluginLoader(); /** * Returns the factory object of the plugin. * * This is typically created by one of the KPluginFactory macros. * Internally, this uses QPluginLoader::instance(), and the same * behaviours apply. * * \returns The factory of the plugin or @c nullptr on error. */ KPluginFactory *factory(); /** * Returns the name of this plugin as given to the constructor. * * If the KService constructor was used, this is the name of the library * provided by the service. * * \returns The plugin name. * * \see fileName() */ QString pluginName() const; /** * Returns the plugin version. * * This will load the plugin if it is not already loaded. * * \returns The version given to K_EXPORT_PLUGIN_VERSION, or (quint32) -1 if * the macro was not used or the plugin could not be loaded. */ quint32 pluginVersion(); /** * Locates a plugin. * * Searches for a dynamic object file in the locations KPluginLoader and * QPluginLoader would search (ie: the current directory and * QCoreApplication::libraryPaths()). * * This can be useful if you wish to use a plugin that does not conform to * the Qt plugin scheme of providing a QObject that declares * Q_PLUGIN_METADATA. In this case, you can find the plugin with this * method, and load it with QLibrary. * * Note that the path is not necessarily absolute. In particular, if the * plugin is found in the current directory, it will be a relative path. * * \param name The name of the plugin (can be a relative path; see above). * This should not include a file extension (like .so or .dll). * \returns The path to the plugin if it was found, or QString() if it * could not be found. * * @since 5.0 */ static QString findPlugin(const QString &name); /** * Returns the last error. * * \returns The description of the last error. * * \see QPluginLoader::errorString() */ QString errorString() const; /** * Returns the path of the plugin. * * This will be the full path of the plugin if it was found, and empty if * it could not be found. * * \returns The full path of the plugin, or the null string if it could * not be found. * * \see QPluginLoader::fileName(), pluginName() */ QString fileName() const; /** * Returns the root object of the plugin. * * The plugin will be loaded if necessary. If the plugin used one of the * KPluginFactory macros, you should use factory() instead. * * \returns The plugin's root object. * * \see QPluginLoader::instance() */ QObject *instance(); /** * Determines whether the plugin is loaded. * * \returns @c True if the plugin is loaded, @c false otherwise. * * \see QPluginLoader::isLoaded() */ bool isLoaded() const; /** * Loads the plugin. * * It is safe to call this multiple times; if the plugin was already loaded, * it will just return @c true. * * Methods that require the plugin to be loaded will load it as necessary * anyway, so you do not normally need to call this method. * * \returns @c True if the plugin was loaded successfully, @c false * otherwise. * * \see QPluginLoader::load() */ bool load(); /** * Returns the load hints for the plugin. * * Determines how load() should work. See QLibrary::loadHints for more * information. * * \returns The load hints for the plugin. * * \see QPluginLoader::loadHints(), setLoadHints() */ QLibrary::LoadHints loadHints() const; /** * Returns the meta data for the plugin. * * \returns A JSON object containing the plugin's metadata, if found. * * \see QPluginLoader::metaData() */ QJsonObject metaData() const; /** * Set the load hints for the plugin. * * Determines how load() should work. See QLibrary::loadHints for more * information. * * \param loadHints The load hints for the plugin. * * \see QPluginLoader::setLoadHints(), loadHints() */ void setLoadHints(QLibrary::LoadHints loadHints); /** * Attempts to unload the plugin. * * If other instances of KPluginLoader or QPluginLoader are using the same * plugin, this will fail; unloading will only happen when every instance * has called unload(). * * \returns @c True if the plugin was unloaded, @c false otherwise. * * \see QPluginLoader::unload(), load(), instance(), factory() */ bool unload(); /** * Finds and instantiates (by calling QPluginLoader::instance()) all plugins from a given * directory. Only plugins which have JSON metadata will be considered. A filter can be passed * which determines which of the found plugins should actually be loaded. * * If you use KConfig you could have a group "Plugins" in your configuration file with the * plugin name as the key and true/false as the value to indicate whether the plugin should * be loaded. In order to easily load all the enable plugins you could use the following code: * @code * KConfigGroup pluginGroup = KSharedConfig::openConfig()->group("Plugins"); * auto filter = [&](const KPluginMetaData &md) { * if (!pluginGroup.hasKey(md.pluginName())) { * return md.isEnabledByDefault(); * } else { * return pluginGroup.readEntry(md.pluginName(), false); * } * }; * QList plugins = KPluginLoader::instantiatePlugins("myapp", filter); * @endcode * * @param directory the directory to search for plugins. If a relative path is given for @p directory, * all entries of QCoreApplication::libraryPaths() will be checked with @p directory appended as a * subdirectory. If an absolute path is given only that directory will be searched. * * @param filter a callback function that returns @c true if the found plugin should be loaded * and @c false if it should be skipped. If this argument is omitted all plugins will be loaded. * * @param parent the parent to set for the instantiated plugins, if the * plugins were not already loaded. * * @note If the plugins have been previously loaded (via QPluginLoader, * directly or due to this class) without being deleted in the meantime * then they are not re-created or re-parented and will be returned using * the parent they were originally created with. @sa * QPluginLoader::instance(). * * @return a list containing an instantiation of each plugin that met the filter criteria * * @see KPluginLoader::findPlugins() * * @since 5.1 */ static QList instantiatePlugins(const QString &directory, std::function filter = std::function(), QObject* parent = nullptr); /** * Find all plugins inside @p directory. Only plugins which have JSON metadata will be considered. * * @param directory The directory to search for plugins. If a relative path is given for @p directory, * all entries of QCoreApplication::libraryPaths() will be checked with @p directory appended as a * subdirectory. If an absolute path is given only that directory will be searched. * * @param filter a callback function that returns @c true if the found plugin should be loaded * and @c false if it should be skipped. If this argument is omitted all plugins will be loaded. * * @return all plugins found in @p directory that fulfil the constraints of @p filter * * @see KPluginLoader::instantiatePlugins() * * @since 5.1 */ static QVector findPlugins(const QString &directory, std::function filter = std::function()); /** * Find all plugins inside @p directory with a given pluginId. Only plugins which have JSON metadata will be considered. * * @param directory The directory to search for plugins. If a relative path is given for @p directory, * all entries of QCoreApplication::libraryPaths() will be checked with @p directory appended as a * subdirectory. If an absolute path is given only that directory will be searched. * * @param pluginId The Id of the plugin, for example KPluginMetaData.pluginId(). * * @return all plugins found in @p directory with the given pluginId. * * @see KPluginLoader::instantiatePlugins() * * @since 5.11 */ static QVector findPluginsById(const QString &directory, const QString &pluginId); /** * Invokes @p callback for each valid plugin found inside @p directory. This is useful if * your application needs to customize the behaviour of KPluginLoader::findPlugins() or * KPluginLoader::instantiatePlugins(). * * @note The files found do not necessarily contain JSON metadata and may not be loadable using K/QPluginLoader. * The only guarantee made is that they are valid library file names as determined by QLibrary::isLibrary(). * * @param directory The directory to search for plugins. If a relative path is given for @p directory, * all entries of QCoreApplication::libraryPaths() will be checked with @p directory appended as a * subdirectory. If an absolute path is given only that directory will be searched. * * @param callback This function will be invoked for each valid plugin that is found. It will receive * the absolute path to the plugin as an argument * * @see KPluginLoader::findPlugins(), KPluginLoader::instantiatePlugins() * * @since 5.1 */ static void forEachPlugin(const QString &directory, std::function callback = std::function()); private: Q_DECLARE_PRIVATE(KPluginLoader) Q_DISABLE_COPY(KPluginLoader) KPluginLoaderPrivate *const d_ptr; }; /** * Represents the name of a plugin intended for KPluginLoader. * * This exists only so that classes such as KService can provide a cast * operator to allow them to be used as arguments to KPluginLoader. * Unless you are implementing such a cast operator, you should never * need to use this class directly. */ // NOTE: this class is all inline, as it mainly exists for typing reasons // (ie: to prevent the issues that would be caused by adding an // operator QString() method to KService) class KCOREADDONS_EXPORT KPluginName { public: /** * Construct a (valid) plugin name from a string. * * If there was an error and the name could not be determined, * fromErrorString() should be used instead to construct an * appropriate error message. * * @param name The name of the plugin; this should not be empty. */ inline explicit KPluginName(const QString &name); /** * The name of the plugin. * * @returns The string passed to the constructor if isValid() is * @c true, QString() otherwise. */ inline QString name() const; /** * Whether the name is valid. * * Note that this only determines how the KPluginName was * constructed, not anything about the value of the string. * * @returns @c true if the KPluginName(const QString&) constructor * was used, @c false if fromErrorString() was used. */ inline bool isValid() const; /** * The error string if no name could be determined. * * @returns The string passed to fromErrorString() if isValid() is * @c false, QString() otherwise. */ inline QString errorString() const; /** * Construct an invalid plugin name with an error message. * * When this object is passed to KPluginLoader, @p errorString * will be used for KPluginLoader::errorString(). * * @param errorString The (translated) error string. */ static inline KPluginName fromErrorString(const QString &errorString); private: inline KPluginName(const QString &name, bool isError); QString m_name; bool m_isError; }; inline KPluginName::KPluginName(const QString &name) : m_name(name), m_isError(false) {} inline KPluginName::KPluginName(const QString &name, bool isError) : m_name(name), m_isError(isError) {} inline QString KPluginName::name() const { return m_isError ? QString() : m_name; } inline bool KPluginName::isValid() const { return !m_isError; } inline QString KPluginName::errorString() const { return m_isError ? m_name : QString(); } inline KPluginName KPluginName::fromErrorString(const QString &errorString) { return KPluginName(errorString, true); } #endif diff --git a/src/lib/plugin/kpluginmetadata.cpp b/src/lib/plugin/kpluginmetadata.cpp index 0769f77..0e85567 100644 --- a/src/lib/plugin/kpluginmetadata.cpp +++ b/src/lib/plugin/kpluginmetadata.cpp @@ -1,381 +1,380 @@ /* This file is part of the KDE project Copyright (C) 2014 Alex Richardson This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kpluginmetadata.h" #include "desktopfileparser_p.h" -#include #include #include #include #include #include #include #include "kcoreaddons_debug.h" #include "kpluginloader.h" #include "kaboutdata.h" class KPluginMetaDataPrivate : public QSharedData { public: QString metaDataFileName; }; KPluginMetaData::KPluginMetaData() { } KPluginMetaData::KPluginMetaData(const KPluginMetaData &other) : m_metaData(other.m_metaData), m_fileName(other.fileName()), d(other.d) { } KPluginMetaData &KPluginMetaData::operator=(const KPluginMetaData &other) { m_metaData = other.m_metaData; m_fileName = other.m_fileName; d = other.d; return *this; } KPluginMetaData::~KPluginMetaData() { } KPluginMetaData::KPluginMetaData(const QString &file) { if (file.endsWith(QStringLiteral(".desktop"))) { loadFromDesktopFile(file, QStringList()); } else if (file.endsWith(QStringLiteral(".json"))) { d = new KPluginMetaDataPrivate; QFile f(file); bool b = f.open(QIODevice::ReadOnly); if (!b) { qCWarning(KCOREADDONS_DEBUG) << "Couldn't open" << file; return; } QJsonParseError error; m_metaData = QJsonDocument::fromJson(f.readAll(), &error).object(); if (error.error) { qCWarning(KCOREADDONS_DEBUG) << "error parsing" << file << error.errorString(); } m_fileName = file; d->metaDataFileName = file; } else { QPluginLoader loader(file); m_fileName = QFileInfo(loader.fileName()).absoluteFilePath(); m_metaData = loader.metaData().value(QStringLiteral("MetaData")).toObject(); } } KPluginMetaData::KPluginMetaData(const QPluginLoader &loader) { m_fileName = QFileInfo(loader.fileName()).absoluteFilePath(); m_metaData = loader.metaData().value(QStringLiteral("MetaData")).toObject(); } KPluginMetaData::KPluginMetaData(const KPluginLoader &loader) { m_fileName = QFileInfo(loader.fileName()).absoluteFilePath(); m_metaData = loader.metaData().value(QStringLiteral("MetaData")).toObject(); } KPluginMetaData::KPluginMetaData(const QJsonObject &metaData, const QString &file) { m_fileName = file; m_metaData = metaData; } KPluginMetaData::KPluginMetaData(const QJsonObject &metaData, const QString &pluginFile, const QString &metaDataFile) { m_fileName = pluginFile; m_metaData = metaData; if (!metaDataFile.isEmpty()) { d = new KPluginMetaDataPrivate; d->metaDataFileName = metaDataFile; } } KPluginMetaData KPluginMetaData::fromDesktopFile(const QString &file, const QStringList &serviceTypes) { KPluginMetaData result; result.loadFromDesktopFile(file, serviceTypes); return result; } void KPluginMetaData::loadFromDesktopFile(const QString &file, const QStringList &serviceTypes) { QString libraryPath; if (!DesktopFileParser::convert(file, serviceTypes, m_metaData, &libraryPath)) { Q_ASSERT(!isValid()); return; // file could not be parsed for some reason, leave this object invalid } d = new KPluginMetaDataPrivate; d->metaDataFileName = QFileInfo(file).absoluteFilePath(); if (!libraryPath.isEmpty()) { // this was a plugin with a shared library m_fileName = libraryPath; } else { // no library, make filename point to the .desktop file m_fileName = d->metaDataFileName; } } QJsonObject KPluginMetaData::rawData() const { return m_metaData; } QString KPluginMetaData::fileName() const { return m_fileName; } QString KPluginMetaData::metaDataFileName() const { return d ? d->metaDataFileName : m_fileName; } bool KPluginMetaData::isValid() const { // it can be valid even if m_fileName is empty (as long as the plugin id is set in the metadata) return !pluginId().isEmpty() && !m_metaData.isEmpty(); } bool KPluginMetaData::isHidden() const { return rootObject()[QStringLiteral("Hidden")].toBool(); } QJsonObject KPluginMetaData::rootObject() const { return m_metaData[QStringLiteral("KPlugin")].toObject(); } QStringList KPluginMetaData::readStringList(const QJsonObject &obj, const QString &key) { const QJsonValue value = obj.value(key); if (value.isUndefined() || value.isObject() || value.isNull()) { return QStringList(); } else if (value.isArray()) { return value.toVariant().toStringList(); } else { QString asString = value.isString() ? value.toString() : value.toVariant().toString(); if (asString.isEmpty()) { return QStringList(); } const QString id = obj.value(QStringLiteral("KPlugin")).toObject().value(QStringLiteral("Id")).toString(); qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a string list." " Treating it as a list with a single entry:" << asString << id.toLatin1().constData(); return QStringList(asString); } } QJsonValue KPluginMetaData::readTranslatedValue(const QJsonObject &jo, const QString &key, const QJsonValue &defaultValue) { QString languageWithCountry = QLocale().name(); auto it = jo.constFind(key + QLatin1Char('[') + languageWithCountry + QLatin1Char(']')); if (it != jo.constEnd()) { return it.value(); } const QStringRef language = languageWithCountry.midRef(0, languageWithCountry.indexOf(QLatin1Char('_'))); it = jo.constFind(key + QLatin1Char('[') + language + QLatin1Char(']')); if (it != jo.constEnd()) { return it.value(); } // no translated value found -> check key it = jo.constFind(key); if (it != jo.constEnd()) { return jo.value(key); } return defaultValue; } QString KPluginMetaData::readTranslatedString(const QJsonObject &jo, const QString &key, const QString &defaultValue) { return readTranslatedValue(jo, key, defaultValue).toString(defaultValue); } static inline void addPersonFromJson(const QJsonObject &obj, QList* out) { KAboutPerson person = KAboutPerson::fromJSON(obj); if (person.name().isEmpty()) { qCWarning(KCOREADDONS_DEBUG) << "Invalid plugin metadata: Attempting to create a KAboutPerson from json without 'Name' property:" << obj; return; } out->append(person); } static QList aboutPersonFromJSON(const QJsonValue &people) { QList ret; if (people.isObject()) { // single author addPersonFromJson(people.toObject(), &ret); } else if (people.isArray()) { const QJsonArray peopleArray = people.toArray(); for (const QJsonValue &val : peopleArray) { if (val.isObject()) { addPersonFromJson(val.toObject(), &ret); } } } return ret; } QList KPluginMetaData::authors() const { return aboutPersonFromJSON(rootObject()[QStringLiteral("Authors")]); } QList KPluginMetaData::translators() const { return aboutPersonFromJSON(rootObject()[QStringLiteral("Translators")]); } QList KPluginMetaData::otherContributors() const { return aboutPersonFromJSON(rootObject()[QStringLiteral("OtherContributors")]); } QString KPluginMetaData::category() const { return rootObject()[QStringLiteral("Category")].toString(); } QString KPluginMetaData::description() const { return readTranslatedString(rootObject(), QStringLiteral("Description")); } QString KPluginMetaData::iconName() const { return rootObject()[QStringLiteral("Icon")].toString(); } QString KPluginMetaData::license() const { return rootObject()[QStringLiteral("License")].toString(); } QString KPluginMetaData::name() const { return readTranslatedString(rootObject(), QStringLiteral("Name")); } QString KPluginMetaData::copyrightText() const { return readTranslatedString(rootObject(), QStringLiteral("Copyright")); } QString KPluginMetaData::extraInformation() const { return readTranslatedString(rootObject(), QStringLiteral("ExtraInformation")); } QString KPluginMetaData::pluginId() const { QJsonObject root = rootObject(); auto nameFromMetaData = root.constFind(QStringLiteral("Id")); if (nameFromMetaData != root.constEnd()) { const QString id = nameFromMetaData.value().toString(); if (!id.isEmpty()) { return id; } } // passing QFileInfo an empty string gives the CWD, which is not what we want if (m_fileName.isEmpty()) { return QString(); } return QFileInfo(m_fileName).baseName(); } QString KPluginMetaData::version() const { return rootObject()[QStringLiteral("Version")].toString(); } QString KPluginMetaData::website() const { return rootObject()[QStringLiteral("Website")].toString(); } QStringList KPluginMetaData::dependencies() const { return readStringList(rootObject(), QStringLiteral("Dependencies")); } QStringList KPluginMetaData::serviceTypes() const { return readStringList(rootObject(), QStringLiteral("ServiceTypes")); } QStringList KPluginMetaData::mimeTypes() const { return readStringList(rootObject(), QStringLiteral("MimeTypes")); } QStringList KPluginMetaData::formFactors() const { return readStringList(rootObject(), QStringLiteral("FormFactors")); } bool KPluginMetaData::isEnabledByDefault() const { QJsonValue val = rootObject()[QStringLiteral("EnabledByDefault")]; if (val.isBool()) { return val.toBool(); } else if (val.isString()) { return val.toString() == QLatin1String("true"); } return false; } QString KPluginMetaData::value(const QString &key, const QString &defaultValue) const { const QJsonValue value = m_metaData.value(key); if (value.isString()) { return value.toString(defaultValue); } else if (value.isArray()) { qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a single string." " but it is a stringlist"; const QStringList list = value.toVariant().toStringList(); if (list.isEmpty()) { return defaultValue; } return list.join(QChar::fromLatin1(',')); } else if (value.isBool()) { qCWarning(KCOREADDONS_DEBUG) << "Expected JSON property" << key << "to be a single string." " but it is a bool"; return value.toBool() ? QStringLiteral("true") : QStringLiteral("false"); } return defaultValue; } bool KPluginMetaData::operator==(const KPluginMetaData &other) const { return m_fileName == other.m_fileName && m_metaData == other.m_metaData; } QObject* KPluginMetaData::instantiate() const { return QPluginLoader(m_fileName).instance(); } diff --git a/src/lib/plugin/kpluginmetadata.h b/src/lib/plugin/kpluginmetadata.h index bb1f4ab..aef2a1c 100644 --- a/src/lib/plugin/kpluginmetadata.h +++ b/src/lib/plugin/kpluginmetadata.h @@ -1,407 +1,406 @@ /* This file is part of the KDE project Copyright (C) 2014 Alex Richardson This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KPLUGINMETADATA_H #define KPLUGINMETADATA_H #include "kcoreaddons_export.h" #include #include #include #include -#include #include #include class KPluginLoader; class QPluginLoader; class QStringList; class KPluginMetaDataPrivate; class KAboutPerson; class QObject; /** * @class KPluginMetaData kpluginmetadata.h KPluginMetaData * * This class allows easily accessing some standardized values from the JSON metadata that * can be embedded into Qt plugins. Additional plugin-specific metadata can be retrieved by * directly reading from the QJsonObject returned by KPluginMetaData::rawData(). * * This class can be used instead of KPluginInfo from KService for applications that only load * Qt C++ plugins. * * The following keys will be read from an object "KPlugin" inside the metadata JSON: * * Key | Accessor function | JSON Type * -------------------| -------------------- | --------------------- * Name | name() | string * Description | description() | string * ExtraInformation | extraInformation() | string * Icon | iconName() | string * Authors | authors() | object array (KAboutPerson) * Category | category() | string * License | license() | string * Copyright | copyrightText() | string * Id | pluginId() | string * Version | version() | string * Website | website() | string * EnabledByDefault | isEnabledByDefault() | bool * ServiceTypes | serviceTypes() | string array * MimeTypes | mimeTypes() | string array * FormFactors | formFactors() | string array * Translators | translators() | object array (KAboutPerson) * OtherContributors | otherContributors() | object array (KAboutPerson) * * The Authors, Translators and OtherContributors keys are expected to be * list of objects that match the structure expected by KAboutPerson::fromJSON(). * * An example metadata json file could look like this: * @verbatim { "KPlugin": { "Name": "Date and Time", "Description": "Date and time by timezone", "Icon": "preferences-system-time", "Authors": { "Name": "Aaron Seigo", "Email": "aseigo@kde.org" }, "Category": "Date and Time", "Dependencies": [], "EnabledByDefault": "true", "License": "LGPL", "Id": "time", "Version": "1.0", "Website": "https://plasma.kde.org/", "ServiceTypes": ["Plasma/DataEngine"] } } @endverbatim * * @sa KAboutPerson::fromJSON() * @since 5.1 */ class KCOREADDONS_EXPORT KPluginMetaData { public: /** Creates an invalid KPluginMetaData instance */ KPluginMetaData(); /** * Reads the plugin metadata from a KPluginLoader instance. You must call KPluginLoader::setFileName() * or use the appropriate constructor on @p loader before calling this. */ KPluginMetaData(const KPluginLoader &loader); /** * Reads the plugin metadata from a QPluginLoader instance. You must call QPluginLoader::setFileName() * or use the appropriate constructor on @p loader before calling this. */ KPluginMetaData(const QPluginLoader &loader); /** * Reads the plugin metadata from a plugin or .desktop which can be loaded from @p file. * * For plugins, platform-specific library suffixes may be omitted since @p file will be resolved * using the same logic as QPluginLoader. * * If the file name ends with ".desktop", the .desktop file will be parsed instead of * reading the metadata from the QPluginLoader. This is the same as calling * KPluginMetaData::fromDesktopFile() without the serviceTypes parameter. * * If @p file ends with .json, the file will be loaded as the QJsonObject metadata. * * @see QPluginLoader::setFileName() * @see KPluginMetaData::fromDesktopFile() */ KPluginMetaData(const QString &file); /** * Creates a KPluginMetaData from a QJsonObject holding the metadata and a file name * This can be used if the data is not retrieved from a Qt C++ plugin library but from some * other source. * @see KPluginMetaData(const QJsonObject &, const QString &, const QString &) */ KPluginMetaData(const QJsonObject &metaData, const QString &file); // TODO: KF6: merge with the above and make metaDataFile default to QString() /** * Creates a KPluginMetaData * @param metaData the JSON metadata to use for this object * @param pluginFile the file that the plugin can be loaded from * @param metaDataFile the file that the JSON metadata was read from * * This can be used if the data is not retrieved from a Qt C++ plugin library but from some * other source. * * @since 5.5 */ KPluginMetaData(const QJsonObject &metaData, const QString &pluginFile, const QString &metaDataFile); /** * Copy contructor */ KPluginMetaData(const KPluginMetaData &); /** * Copy assignment */ KPluginMetaData &operator=(const KPluginMetaData &); /** * Destructor */ ~KPluginMetaData(); /** * Load a KPluginMetaData instace from a .desktop file. Unlike the constructor which takes * a single file parameter this method allows you to specify which service type files should * be parsed to determine the correct type for a given .desktop property. * This ensures that a e.g. comma-separated string list field in the .desktop file will correctly * be converted to a JSON string array. * * @note This function mostly exists for backwards-compatibility. It is recommended * that new applications load JSON files directly instead of using .desktop files for plugin metadata. * * @param file the .desktop file to load * @param serviceTypes a list of files to parse If one of these paths is a relative path it * will be resolved relative to the "kservicetypes5" subdirectory in QStandardPaths::GenericDataLocation. * If the list is empty only the default set of properties will be treated specially and all other entries * will be read as the JSON string type. * * @since 5.16 */ static KPluginMetaData fromDesktopFile(const QString &file, const QStringList &serviceTypes = QStringList()); /** * @return whether this object holds valid information about a plugin. * If this is @c true pluginId() will return a non-empty string. */ bool isValid() const; /** * @return whether this object should be hidden, this is usually not used for binary * plugins, when loading a KPluginMetaData from a .desktop file, this will reflect * the value of the "Hidden" key. * * @since 5.8 */ bool isHidden() const; /** * @return the path to the plugin. This string can be passed to the KPluginLoader * or QPluginLoader constructors in order to attempt to load this plugin. * @note It is not guaranteed that this is a valid path to a shared library (i.e. loadable * by QPluginLoader) since the metadata could also refer to a non-C++ plugin. */ QString fileName() const; /** * @return the file that the metadata was read from. This is not necessarily the same as * fileName(), since not all plugins have the metadata embedded. The metadata could also be * stored in a separate .desktop file. * * @since 5.5 */ QString metaDataFileName() const; /** * @return the full metadata stored inside the plugin file. */ QJsonObject rawData() const; /** * Tries to instantiate this plugin using KPluginMetaData::fileName(). * @note The value of KPluginMetaData::dependencies() is not used here, dependencies must be * resolved manually. * * @return The plugin root object or @c nullptr if it could not be loaded * @see QPluginLoader::instance(), KPluginLoader::instance() */ QObject *instantiate() const; /** * @return the user visible name of the plugin. */ QString name() const; /** * @return a short description of the plugin. */ QString description() const; /** * @return additional information about this plugin (e.g. for use in an "about plugin" dialog) * * @since 5.18 */ QString extraInformation() const; /** * @return the author(s) of this plugin. */ QList authors() const; /** * @return the translator(s) of this plugin. * * @since 5.18 */ QList translators() const; /** * @return a list of people that contributed to this plugin (other than the authors and translators). * * @since 5.18 */ QList otherContributors() const; /** * @return the categories of this plugin (e.g. "playlist/skin"). */ QString category() const; /** * @return the icon name for this plugin * @see QIcon::fromTheme() */ QString iconName() const; /** * @return the short license identifier (e.g. LGPL). * @see KAboutLicense::byKeyword() for retrieving the full license information */ QString license() const; /** * @return a short copyright statement * * @since 5.18 */ QString copyrightText() const; /** * @return the internal name of the plugin (for KParts Plugins this is * the same name as set in the .rc file). If the plugin name property is not set in * the metadata this will return the plugin file name without the file extension. */ QString pluginId() const; /** * @return the version of the plugin. */ QString version() const; /** * @return the website of the plugin. */ QString website() const; /** * @return a list of plugins that this plugin depends on so that it can function properly * @see KJsonPluginInfo::pluginId() */ QStringList dependencies() const; /** * @note Unlike KService this does not contain the MIME types. To get the handled MIME types * use the KPluginMetaData::mimeTypes() function. * @return a list of service types this plugin implements (e.g. "Plasma/DataEngine") */ QStringList serviceTypes() const; /** * @return a list of MIME types this plugin can handle (e.g. "application/pdf", "image/png", etc.) * @since 5.16 */ QStringList mimeTypes() const; /** * @return A string list of formfactors this plugin is useful for, e.g. desktop, tablet, * handset, mediacenter, etc. * The keys for this are not formally defined. * * @since 5.12 */ QStringList formFactors() const; /** * @return whether the plugin should be enabled by default. * This is only a recommendation, applications can ignore this value if they want to. */ bool isEnabledByDefault() const; /** * @return the value for @p key from the metadata or @p defaultValue if the key does not exist * or the value for @p key is not of type string * * @see KPluginMetaData::rawData() if QString is not the correct type for @p key */ QString value(const QString &key, const QString &defaultValue = QString()) const; /** @return the value for @p key inside @p jo as a string list. If the type of @p key is string, a list with containing * just that string will be returned, if it is an array the list will contain one entry for each array member. * If the key cannot be found an empty list will be returned. */ static QStringList readStringList(const QJsonObject &jo, const QString &key); /** * Reads a value from @p jo but unlike QJsonObject::value() it allows different entries for each locale * This is done by appending the locale identifier in brackets to the key (e.g. "[de_DE]" or "[es]") * When looking for a key "foo" with German (Germany) locale we will first attempt to read "foo[de_DE]", * if that does not exist "foo[de]", finally falling back to "foo" if that also doesn't exist. * @return the translated value for @p key from @p jo or @p defaultValue if @p key was not found */ static QJsonValue readTranslatedValue(const QJsonObject &jo, const QString &key, const QJsonValue &defaultValue = QJsonValue()); /** * @return the translated value of @p key from @p jo as a string or @p defaultValue if @p key was not found * or the value for @p key is not of type string * @see KPluginMetaData::readTranslatedValue(const QJsonObject &jo, const QString &key) */ static QString readTranslatedString(const QJsonObject &jo, const QString &key, const QString &defaultValue = QString()); /** * @return @c true if this object is equal to @p other, otherwise @c false */ bool operator==(const KPluginMetaData &other) const; /** * @return @c true if this object is not equal to @p other, otherwise @c false. */ inline bool operator!=(const KPluginMetaData &other) const { return !(*this == other); } private: QJsonObject rootObject() const; void loadFromDesktopFile(const QString &file, const QStringList &serviceTypes); private: QJsonObject m_metaData; QString m_fileName; QExplicitlySharedDataPointer d; // for future binary compatible extensions }; inline uint qHash(const KPluginMetaData &md, uint seed) { return qHash(md.pluginId(), seed); } Q_DECLARE_METATYPE(KPluginMetaData); #endif // KPLUGINMETADATA_H diff --git a/src/lib/text/kstringhandler.cpp b/src/lib/text/kstringhandler.cpp index a2002bb..31bd0ce 100644 --- a/src/lib/text/kstringhandler.cpp +++ b/src/lib/text/kstringhandler.cpp @@ -1,361 +1,359 @@ /* This file is part of the KDE libraries Copyright (C) 1999 Ian Zepp (icszepp@islc.net) Copyright (C) 2006 by Dominic Battre Copyright (C) 2006 by Martin Pool This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kstringhandler.h" #include // random() #include // for the word ranges #include -#include -#include - +#include // // Capitalization routines // QString KStringHandler::capwords(const QString &text) { if (text.isEmpty()) { return text; } const QString strippedText = text.trimmed(); const QString space = QString(QLatin1Char(' ')); const QStringList words = capwords(strippedText.split(space)); QString result = text; result.replace(strippedText, words.join(space)); return result; } QStringList KStringHandler::capwords(const QStringList &list) { QStringList tmp = list; for (QStringList::Iterator it = tmp.begin(); it != tmp.end(); ++it) { *it = (*it)[ 0 ].toUpper() + (*it).midRef(1); } return tmp; } QString KStringHandler::lsqueeze(const QString &str, int maxlen) { if (str.length() > maxlen) { int part = maxlen - 3; return QStringLiteral("...") + str.rightRef(part); } else { return str; } } QString KStringHandler::csqueeze(const QString &str, int maxlen) { if (str.length() > maxlen && maxlen > 3) { const int part = (maxlen - 3) / 2; return str.leftRef(part) + QStringLiteral("...") + str.rightRef(part); } else { return str; } } QString KStringHandler::rsqueeze(const QString &str, int maxlen) { if (str.length() > maxlen) { int part = maxlen - 3; return str.leftRef(part) + QStringLiteral("..."); } else { return str; } } QStringList KStringHandler::perlSplit(const QString &sep, const QString &s, int max) { bool ignoreMax = 0 == max; QStringList l; int searchStart = 0; int tokenStart = s.indexOf(sep, searchStart); while (-1 != tokenStart && (ignoreMax || l.count() < max - 1)) { if (!s.midRef(searchStart, tokenStart - searchStart).isEmpty()) { l << s.mid(searchStart, tokenStart - searchStart); } searchStart = tokenStart + sep.length(); tokenStart = s.indexOf(sep, searchStart); } if (!s.midRef(searchStart, s.length() - searchStart).isEmpty()) { l << s.mid(searchStart, s.length() - searchStart); } return l; } QStringList KStringHandler::perlSplit(const QChar &sep, const QString &s, int max) { bool ignoreMax = 0 == max; QStringList l; int searchStart = 0; int tokenStart = s.indexOf(sep, searchStart); while (-1 != tokenStart && (ignoreMax || l.count() < max - 1)) { if (!s.midRef(searchStart, tokenStart - searchStart).isEmpty()) { l << s.mid(searchStart, tokenStart - searchStart); } searchStart = tokenStart + 1; tokenStart = s.indexOf(sep, searchStart); } if (!s.midRef(searchStart, s.length() - searchStart).isEmpty()) { l << s.mid(searchStart, s.length() - searchStart); } return l; } QStringList KStringHandler::perlSplit(const QRegExp &sep, const QString &s, int max) { bool ignoreMax = 0 == max; QStringList l; int searchStart = 0; int tokenStart = sep.indexIn(s, searchStart); int len = sep.matchedLength(); while (-1 != tokenStart && (ignoreMax || l.count() < max - 1)) { if (!s.midRef(searchStart, tokenStart - searchStart).isEmpty()) { l << s.mid(searchStart, tokenStart - searchStart); } searchStart = tokenStart + len; tokenStart = sep.indexIn(s, searchStart); len = sep.matchedLength(); } if (!s.midRef(searchStart, s.length() - searchStart).isEmpty()) { l << s.mid(searchStart, s.length() - searchStart); } return l; } QString KStringHandler::tagUrls(const QString &text) { /*static*/ QRegExp urlEx(QStringLiteral("(www\\.(?!\\.)|(fish|(f|ht)tp(|s))://)[\\d\\w\\./,:_~\\?=&;#@\\-\\+\\%\\$\\(]+[\\d\\w/\\)]")); QString richText(text); int urlPos = 0, urlLen; while ((urlPos = urlEx.indexIn(richText, urlPos)) >= 0) { urlLen = urlEx.matchedLength(); QString href = richText.mid(urlPos, urlLen); // Qt doesn't support (?<=pattern) so we do it here if ((urlPos > 0) && richText[urlPos - 1].isLetterOrNumber()) { urlPos++; continue; } // Don't use QString::arg since %01, %20, etc could be in the string QString anchor = QStringLiteral("") + href + QStringLiteral(""); richText.replace(urlPos, urlLen, anchor); urlPos += anchor.length(); } return richText; } QString KStringHandler::obscure(const QString &str) { QString result; const QChar *unicode = str.unicode(); for (int i = 0; i < str.length(); ++i) // yes, no typo. can't encode ' ' or '!' because // they're the unicode BOM. stupid scrambling. stupid. result += (unicode[ i ].unicode() <= 0x21) ? unicode[ i ] : QChar(0x1001F - unicode[ i ].unicode()); return result; } bool KStringHandler::isUtf8(const char *buf) { int i, n; unsigned char c; bool gotone = false; if (!buf) { return true; // whatever, just don't crash } #define F 0 /* character never appears in text */ #define T 1 /* character appears in plain ASCII text */ #define I 2 /* character appears in ISO-8859 text */ #define X 3 /* character appears in non-ISO extended ASCII (Mac, IBM PC) */ static const unsigned char text_chars[256] = { /* BEL BS HT LF FF CR */ F, F, F, F, F, F, F, T, T, T, T, F, T, T, F, F, /* 0x0X */ /* ESC */ F, F, F, F, F, F, F, F, F, F, F, T, F, F, F, F, /* 0x1X */ T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x2X */ T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x3X */ T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x4X */ T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x5X */ T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, /* 0x6X */ T, T, T, T, T, T, T, T, T, T, T, T, T, T, T, F, /* 0x7X */ /* NEL */ X, X, X, X, X, T, X, X, X, X, X, X, X, X, X, X, /* 0x8X */ X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, X, /* 0x9X */ I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xaX */ I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xbX */ I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xcX */ I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xdX */ I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, /* 0xeX */ I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I /* 0xfX */ }; /* *ulen = 0; */ for (i = 0; (c = buf[i]); ++i) { if ((c & 0x80) == 0) { /* 0xxxxxxx is plain ASCII */ /* * Even if the whole file is valid UTF-8 sequences, * still reject it if it uses weird control characters. */ if (text_chars[c] != T) { return false; } } else if ((c & 0x40) == 0) { /* 10xxxxxx never 1st byte */ return false; } else { /* 11xxxxxx begins UTF-8 */ int following; if ((c & 0x20) == 0) { /* 110xxxxx */ following = 1; } else if ((c & 0x10) == 0) { /* 1110xxxx */ following = 2; } else if ((c & 0x08) == 0) { /* 11110xxx */ following = 3; } else if ((c & 0x04) == 0) { /* 111110xx */ following = 4; } else if ((c & 0x02) == 0) { /* 1111110x */ following = 5; } else { return false; } for (n = 0; n < following; ++n) { i++; if (!(c = buf[i])) { goto done; } if ((c & 0x80) == 0 || (c & 0x40)) { return false; } } gotone = true; } } done: return gotone; /* don't claim it's UTF-8 if it's all 7-bit */ } #undef F #undef T #undef I #undef X QString KStringHandler::from8Bit(const char *str) { if (!str) { return QString(); } if (!*str) { static const QLatin1String emptyString(""); return emptyString; } return KStringHandler::isUtf8(str) ? QString::fromUtf8(str) : QString::fromLocal8Bit(str); } QString KStringHandler::preProcessWrap(const QString &text) { const QChar zwsp(0x200b); QString result; result.reserve(text.length()); for (int i = 0; i < text.length(); i++) { const QChar c = text[i]; bool openingParens = (c == QLatin1Char('(') || c == QLatin1Char('{') || c == QLatin1Char('[')); bool singleQuote = (c == QLatin1Char('\'')); bool closingParens = (c == QLatin1Char(')') || c == QLatin1Char('}') || c == QLatin1Char(']')); bool breakAfter = (closingParens || c.isPunct() || c.isSymbol()); bool nextIsSpace = (i == (text.length() - 1) || text[i + 1].isSpace()); bool prevIsSpace = (i == 0 || text[i - 1].isSpace() || result[result.length() - 1] == zwsp); // Provide a breaking opportunity before opening parenthesis if (openingParens && !prevIsSpace) { result += zwsp; } // Provide a word joiner before the single quote if (singleQuote && !prevIsSpace) { result += QChar(0x2060); } result += c; if (breakAfter && !openingParens && !nextIsSpace && !singleQuote) { result += zwsp; } } return result; } int KStringHandler::logicalLength(const QString& text) { int length = 0; auto chrs = text.toUcs4(); for (auto chr : chrs) { auto script = QChar::script(chr); if (script == QChar::Script_Han || script == QChar::Script_Hangul || script == QChar::Script_Hiragana || script == QChar::Script_Katakana || script == QChar::Script_Yi || QChar::isHighSurrogate(chr)) { length += 2; } else { length += 1; } } return length; } diff --git a/src/lib/text/ktexttohtml.cpp b/src/lib/text/ktexttohtml.cpp index 9e8658d..2e34ef0 100644 --- a/src/lib/text/ktexttohtml.cpp +++ b/src/lib/text/ktexttohtml.cpp @@ -1,608 +1,607 @@ /* Copyright (c) 2002 Dave Corrie Copyright (c) 2014 Daniel Vrátil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "ktexttohtml.h" #include "ktexttohtml_p.h" #include "ktexttohtmlemoticonsinterface.h" #include #include #include #include #include -#include #include #include #include "kcoreaddons_debug.h" static KTextToHTMLEmoticonsInterface *s_emoticonsInterface = nullptr; static void loadEmoticonsPlugin() { static bool triedLoadPlugin = false; if (!triedLoadPlugin) { triedLoadPlugin = true; // Check if QGuiApplication::platformName property exists. This is a // hackish way of determining whether we are running QGuiApplication, // because we cannot load the FrameworkIntegration plugin into a // QCoreApplication, as it would crash immediately if (qApp->metaObject()->indexOfProperty("platformName") > -1) { QPluginLoader lib(QStringLiteral("kf5/KEmoticonsIntegrationPlugin")); QObject *rootObj = lib.instance(); if (rootObj) { s_emoticonsInterface = rootObj->property(KTEXTTOHTMLEMOTICONS_PROPERTY).value(); } } } if (!s_emoticonsInterface) { s_emoticonsInterface = new KTextToHTMLEmoticonsDummy(); } } KTextToHTMLHelper::KTextToHTMLHelper(const QString &plainText, int pos, int maxUrlLen, int maxAddressLen) : mText(plainText) , mMaxUrlLen(maxUrlLen) , mMaxAddressLen(maxAddressLen) , mPos(pos) { } KTextToHTMLEmoticonsInterface* KTextToHTMLHelper::emoticonsInterface() const { if (!s_emoticonsInterface) { loadEmoticonsPlugin(); } return s_emoticonsInterface; } QString KTextToHTMLHelper::getEmailAddress() { QString address; if (mText[mPos] == QLatin1Char('@')) { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ static const QString allowedSpecialChars = QStringLiteral(".!#$%&'*+-/=?^_`{|}~"); // determine the local part of the email address int start = mPos - 1; while (start >= 0 && mText[start].unicode() < 128 && (mText[start].isLetterOrNumber() || mText[start] == QLatin1Char('@') || // allow @ to find invalid email addresses allowedSpecialChars.indexOf(mText[start]) != -1)) { if (mText[start] == QLatin1Char('@')) { return QString(); // local part contains '@' -> no email address } --start; } ++start; // we assume that an email address starts with a letter or a digit while ((start < mPos) && !mText[start].isLetterOrNumber()) { ++start; } if (start == mPos) { return QString(); // local part is empty -> no email address } // determine the domain part of the email address int dotPos = INT_MAX; int end = mPos + 1; while (end < mText.length() && (mText[end].isLetterOrNumber() || mText[end] == QLatin1Char('@') || // allow @ to find invalid email addresses mText[end] == QLatin1Char('.') || mText[end] == QLatin1Char('-'))) { if (mText[end] == QLatin1Char('@')) { return QString(); // domain part contains '@' -> no email address } if (mText[end] == QLatin1Char('.')) { dotPos = qMin(dotPos, end); // remember index of first dot in domain } ++end; } // we assume that an email address ends with a letter or a digit while ((end > mPos) && !mText[end - 1].isLetterOrNumber()) { --end; } if (end == mPos) { return QString(); // domain part is empty -> no email address } if (dotPos >= end) { return QString(); // domain part doesn't contain a dot } if (end - start > mMaxAddressLen) { return QString(); // too long -> most likely no email address } address = mText.mid(start, end - start); mPos = end - 1; } return address; } QString KTextToHTMLHelper::getPhoneNumber() { if (!mText[mPos].isDigit() && mText[mPos] != QLatin1Char('+')) { return {}; } static const QString allowedBeginSeparators = QStringLiteral(" \r\t\n:"); if (mPos > 0 && !allowedBeginSeparators.contains(mText[mPos - 1])) { return {}; } // this isn't 100% accurate, we filter stuff below that is too hard to capture with a regexp static const QRegularExpression telPattern(QStringLiteral(R"([+0](( |( ?[/-] ?)?)\(?\d+\)?+){6,30})")); const auto match = telPattern.match(mText, mPos, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption); if (match.hasMatch()) { auto m = match.captured(); // check for maximum number of digits (15), see https://en.wikipedia.org/wiki/Telephone_numbering_plan if (std::count_if(m.begin(), m.end(), [](const QChar &c) { return c.isDigit(); }) > 15) { return {}; } // only one / is allowed, otherwise we trigger on dates if (std::count(m.begin(), m.end(), QLatin1Char('/')) > 1) { return {}; } // parenthesis need to be balanced, and must not be nested int openIdx = -1; for (int i = 0; i < m.size(); ++i) { if ((m[i] == QLatin1Char('(') && openIdx >= 0) || (m[i] == QLatin1Char(')') && openIdx < 0)) { return {}; } if (m[i] == QLatin1Char('(')) { openIdx = i; } else if (m[i] == QLatin1Char(')')) { openIdx = -1; } } if (openIdx > 0) { m = m.left(openIdx - 1).trimmed(); } // check if there's a plausible separator at the end static const QString allowedEndSeparators = QStringLiteral(" \r\t\n,."); const auto l = m.size(); if (mText.size() > mPos + l && !allowedEndSeparators.contains(mText[mPos + l])) { return {}; } mPos += l - 1; return m; } return {}; } static QString normalizePhoneNumber(const QString &str) { QString res; res.reserve(str.size()); for (const auto c : str) { if (c.isDigit() || c == QLatin1Char('+')) { res.push_back(c); } } return res; } bool KTextToHTMLHelper::atUrl() const { // the following characters are allowed in a dot-atom (RFC 2822): // a-z A-Z 0-9 . ! # $ % & ' * + - / = ? ^ _ ` { | } ~ static const QString allowedSpecialChars = QStringLiteral(".!#$%&'*+-/=?^_`{|}~"); // the character directly before the URL must not be a letter, a number or // any other character allowed in a dot-atom (RFC 2822). if ((mPos > 0) && (mText[mPos - 1].isLetterOrNumber() || (allowedSpecialChars.indexOf(mText[mPos - 1]) != -1))) { return false; } QChar ch = mText[mPos]; return (ch == QLatin1Char('h') && (mText.midRef(mPos, 7) == QLatin1String("http://") || mText.midRef(mPos, 8) == QLatin1String("https://"))) || (ch == QLatin1Char('v') && mText.midRef(mPos, 6) == QLatin1String("vnc://")) || (ch == QLatin1Char('f') && (mText.midRef(mPos, 7) == QLatin1String("fish://") || mText.midRef(mPos, 6) == QLatin1String("ftp://") || mText.midRef(mPos, 7) == QLatin1String("ftps://"))) || (ch == QLatin1Char('s') && (mText.midRef(mPos, 7) == QLatin1String("sftp://") || mText.midRef(mPos, 6) == QLatin1String("smb://"))) || (ch == QLatin1Char('m') && mText.midRef(mPos, 7) == QLatin1String("mailto:")) || (ch == QLatin1Char('w') && mText.midRef(mPos, 4) == QLatin1String("www.")) || (ch == QLatin1Char('f') && (mText.midRef(mPos, 4) == QLatin1String("ftp.") || mText.midRef(mPos, 7) == QLatin1String("file://"))) || (ch == QLatin1Char('n') && mText.midRef(mPos, 5) == QLatin1String("news:")) || (ch == QLatin1Char('t') && mText.midRef(mPos, 4) == QLatin1String("tel:")); } bool KTextToHTMLHelper::isEmptyUrl(const QString &url) const { return url.isEmpty() || url == QLatin1String("http://") || url == QLatin1String("https://") || url == QLatin1String("fish://") || url == QLatin1String("ftp://") || url == QLatin1String("ftps://") || url == QLatin1String("sftp://") || url == QLatin1String("smb://") || url == QLatin1String("vnc://") || url == QLatin1String("mailto") || url == QLatin1String("www") || url == QLatin1String("ftp") || url == QLatin1String("news") || url == QLatin1String("news://") || url == QLatin1String("tel") || url == QLatin1String("tel:"); } QString KTextToHTMLHelper::getUrl(bool *badurl) { QString url; if (atUrl()) { // NOTE: see http://tools.ietf.org/html/rfc3986#appendix-A and especially appendix-C // Appendix-C mainly says, that when extracting URLs from plain text, line breaks shall // be allowed and should be ignored when the URI is extracted. // This implementation follows this recommendation and // allows the URL to be enclosed within different kind of brackets/quotes // If an URL is enclosed, whitespace characters are allowed and removed, otherwise // the URL ends with the first whitespace // Also, if the URL is enclosed in brackets, the URL itself is not allowed // to contain the closing bracket, as this would be detected as the end of the URL QChar beforeUrl, afterUrl; // detect if the url has been surrounded by brackets or quotes if (mPos > 0) { beforeUrl = mText[mPos - 1]; /*if ( beforeUrl == '(' ) { afterUrl = ')'; } else */if (beforeUrl == QLatin1Char('[')) { afterUrl = QLatin1Char(']'); } else if (beforeUrl == QLatin1Char('<')) { afterUrl = QLatin1Char('>'); } else if (beforeUrl == QLatin1Char('>')) { // for e.g. http://..... afterUrl = QLatin1Char('<'); } else if (beforeUrl == QLatin1Char('"')) { afterUrl = QLatin1Char('"'); } } url.reserve(mMaxUrlLen); // avoid allocs int start = mPos; bool previousCharIsSpace = false; bool previousCharIsADoubleQuote = false; bool previousIsAnAnchor = false; while ((mPos < mText.length()) && (mText[mPos].isPrint() || mText[mPos].isSpace()) && ((afterUrl.isNull() && !mText[mPos].isSpace()) || (!afterUrl.isNull() && mText[mPos] != afterUrl))) { if (!previousCharIsSpace && (mText[mPos] == QLatin1Char('<')) && ((mPos + 1) < mText.length())) { // Fix Bug #346132: allow "http://www.foo.bar" // < inside a URL is not allowed, however there is a test which // checks that "http://some/path" should be allowed // Therefore: check if what follows is another URL and if so, stop here mPos++; if (atUrl()) { mPos--; break; } mPos--; } if (!previousCharIsSpace && (mText[mPos] == QLatin1Char(' ')) && ((mPos + 1) < mText.length())) { // Fix kmail bug: allow "http://www.foo.bar http://foo.bar/" // Therefore: check if what follows is another URL and if so, stop here mPos++; if (atUrl()) { mPos--; break; } mPos--; } if (mText[mPos].isSpace()) { previousCharIsSpace = true; } else if (!previousIsAnAnchor && mText[mPos] == QLatin1Char('[')) { break; } else if (!previousIsAnAnchor && mText[mPos] == QLatin1Char(']')) { break; } else { // skip whitespace if (previousCharIsSpace && mText[mPos] == QLatin1Char('<')) { url.append(QLatin1Char(' ')); break; } previousCharIsSpace = false; if (mText[mPos] == QLatin1Char('>') && previousCharIsADoubleQuote) { //it's an invalid url if (badurl) { *badurl = true; } return QString(); } if (mText[mPos] == QLatin1Char('"')) { previousCharIsADoubleQuote = true; } else { previousCharIsADoubleQuote = false; } if (mText[mPos] == QLatin1Char('#')) { previousIsAnAnchor = true; } url.append(mText[mPos]); if (url.length() > mMaxUrlLen) { break; } } ++mPos; } if (isEmptyUrl(url) || (url.length() > mMaxUrlLen)) { mPos = start; url.clear(); return url; } else { --mPos; } } // HACK: This is actually against the RFC. However, most people don't properly escape the URL in // their text with "" or <>. That leads to people writing an url, followed immediately by // a dot to finish the sentence. That would lead the parser to include the dot in the url, // even though that is not wanted. So work around that here. // Most real-life URLs hopefully don't end with dots or commas. static const QString wordBoundaries = QStringLiteral(".,:!?)>"); if (url.length() > 1) { do { if (wordBoundaries.contains(url.at(url.length() - 1))) { url.chop(1); --mPos; } else { break; } } while (url.length() > 1); } return url; } QString KTextToHTMLHelper::highlightedText() { // formating symbols must be prepended with a whitespace if ((mPos > 0) && !mText[mPos - 1].isSpace()) { return QString(); } const QChar ch = mText[mPos]; if (ch != QLatin1Char('/') && ch != QLatin1Char('*') && ch != QLatin1Char('_') && ch != QLatin1Char('-')) { return QString(); } QRegularExpression re(QStringLiteral("\\%1([^\\s|^\\%1].*[^\\s|^\\%1])\\%1").arg(ch)); re.setPatternOptions(QRegularExpression::InvertedGreedinessOption); const auto match = re.match(mText, mPos, QRegularExpression::NormalMatch, QRegularExpression::AnchoredMatchOption); if (match.hasMatch()) { if (match.capturedStart() == mPos) { int length = match.capturedLength(); // there must be a whitespace after the closing formating symbol if (mPos + length < mText.length() && !mText[mPos + length].isSpace()) { return QString(); } mPos += length - 1; switch (ch.toLatin1()) { case '*': return QLatin1String("*") + match.capturedRef(1) + QLatin1String("*"); case '_': return QLatin1String("_") + match.capturedRef(1) + QLatin1String("_"); case '/': return QLatin1String("/") + match.capturedRef(1) + QLatin1String("/"); case '-': return QLatin1String("-") + match.capturedRef(1) + QLatin1String("-"); } } } return QString(); } QString KTextToHTMLHelper::pngToDataUrl(const QString &iconPath) const { if (iconPath.isEmpty()) { return QString(); } QFile pngFile(iconPath); if (!pngFile.open(QIODevice::ReadOnly | QIODevice::Unbuffered)) { return QString(); } QByteArray ba = pngFile.readAll(); pngFile.close(); return QStringLiteral("data:image/png;base64,%1").arg(QLatin1String(ba.toBase64().constData())); } QString KTextToHTML::convertToHtml(const QString &plainText, const KTextToHTML::Options &flags, int maxUrlLen, int maxAddressLen) { KTextToHTMLHelper helper(plainText, maxUrlLen, maxAddressLen); QString str; QString result(static_cast(nullptr), helper.mText.length() * 2); QChar ch; int x; bool startOfLine = true; for (helper.mPos = 0, x = 0; helper.mPos < helper.mText.length(); ++helper.mPos, ++x) { ch = helper.mText[helper.mPos]; if (flags & PreserveSpaces) { if (ch == QLatin1Char(' ')) { if (helper.mPos + 1 < helper.mText.length()) { if (helper.mText[helper.mPos + 1] != QLatin1Char(' ')) { // A single space, make it breaking if not at the start or end of the line const bool endOfLine = helper.mText[helper.mPos + 1] == QLatin1Char('\n'); if (!startOfLine && !endOfLine) { result += QLatin1Char(' '); } else { result += QLatin1String(" "); } } else { // Whitespace of more than one space, make it all non-breaking while (helper.mPos < helper.mText.length() && helper.mText[helper.mPos] == QLatin1Char(' ')) { result += QLatin1String(" "); ++helper.mPos; ++x; } // We incremented once to often, undo that --helper.mPos; --x; } } else { // Last space in the text, it is non-breaking result += QLatin1String(" "); } if (startOfLine) { startOfLine = false; } continue; } else if (ch == QLatin1Char('\t')) { do { result += QLatin1String(" "); ++x; } while ((x & 7) != 0); --x; startOfLine = false; continue; } } if (ch == QLatin1Char('\n')) { result += QLatin1String("
\n"); // Keep the \n, so apps can figure out the quoting levels correctly. startOfLine = true; x = -1; continue; } startOfLine = false; if (ch == QLatin1Char('&')) { result += QLatin1String("&"); } else if (ch == QLatin1Char('"')) { result += QLatin1String("""); } else if (ch == QLatin1Char('<')) { result += QLatin1String("<"); } else if (ch == QLatin1Char('>')) { result += QLatin1String(">"); } else { const int start = helper.mPos; if (!(flags & IgnoreUrls)) { bool badUrl = false; str = helper.getUrl(&badUrl); if (badUrl) { QString resultBadUrl; const int helperTextSize(helper.mText.count()); for (int i = 0; i < helperTextSize; ++i) { const QChar chBadUrl = helper.mText[i]; if (chBadUrl == QLatin1Char('&')) { resultBadUrl += QLatin1String("&"); } else if (chBadUrl == QLatin1Char('"')) { resultBadUrl += QLatin1String("""); } else if (chBadUrl == QLatin1Char('<')) { resultBadUrl += QLatin1String("<"); } else if (chBadUrl == QLatin1Char('>')) { resultBadUrl += QLatin1String(">"); } else { resultBadUrl += chBadUrl; } } return resultBadUrl; } if (!str.isEmpty()) { QString hyperlink; if (str.left(4) == QLatin1String("www.")) { hyperlink = QLatin1String("http://") + str; } else if (str.left(4) == QLatin1String("ftp.")) { hyperlink = QLatin1String("ftp://") + str; } else { hyperlink = str; } result += QLatin1String("") + str.toHtmlEscaped() + QLatin1String(""); x += helper.mPos - start; continue; } str = helper.getEmailAddress(); if (!str.isEmpty()) { // len is the length of the local part int len = str.indexOf(QLatin1Char('@')); QString localPart = str.left(len); // remove the local part from the result (as '&'s have been expanded to // & we have to take care of the 4 additional characters per '&') result.truncate(result.length() - len - (localPart.count(QLatin1Char('&')) * 4)); x -= len; result += QLatin1String("") + str + QLatin1String(""); x += str.length() - 1; continue; } if (flags & ConvertPhoneNumbers) { str = helper.getPhoneNumber(); if (!str.isEmpty()) { result += QLatin1String("") + str + QLatin1String(""); x += str.length() - 1; continue; } } } if (flags & HighlightText) { str = helper.highlightedText(); if (!str.isEmpty()) { result += str; x += helper.mPos - start; continue; } } result += ch; } } if (flags & ReplaceSmileys) { const QStringList exclude = { QStringLiteral("(c)"), QStringLiteral("(C)"), QStringLiteral(">:-("), QStringLiteral(">:("), QStringLiteral("(B)"), QStringLiteral("(b)"), QStringLiteral("(P)"), QStringLiteral("(p)") , QStringLiteral("(O)"), QStringLiteral("(o)"), QStringLiteral("(D)"), QStringLiteral("(d)"), QStringLiteral("(E)"), QStringLiteral("(e)"), QStringLiteral("(K)"), QStringLiteral("(k)") , QStringLiteral("(I)"), QStringLiteral("(i)"), QStringLiteral("(L)"), QStringLiteral("(l)"), QStringLiteral("(8)"), QStringLiteral("(T)"), QStringLiteral("(t)"), QStringLiteral("(G)") , QStringLiteral("(g)"), QStringLiteral("(F)"), QStringLiteral("(f)"), QStringLiteral("(H)") , QStringLiteral("8)"), QStringLiteral("(N)"), QStringLiteral("(n)"), QStringLiteral("(Y)"), QStringLiteral("(y)"), QStringLiteral("(U)"), QStringLiteral("(u)"), QStringLiteral("(W)"), QStringLiteral("(w)") , QStringLiteral("(6)")}; result = helper.emoticonsInterface()->parseEmoticons(result, true, exclude); } return result; } diff --git a/src/lib/text/ktexttohtml.h b/src/lib/text/ktexttohtml.h index 3a343e3..a7f1589 100644 --- a/src/lib/text/ktexttohtml.h +++ b/src/lib/text/ktexttohtml.h @@ -1,101 +1,100 @@ /* Copyright (c) 2002 Dave Corrie Copyright (c) 2014 Daniel Vrátil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KCOREADDONS_KTEXTTOHTML_H #define KCOREADDONS_KTEXTTOHTML_H #include #include -#include /** * @author Dave Corrie \ */ namespace KTextToHTML { /** * @since 5.5.0 */ enum Option { /** * Preserve white-space formatting of the text */ PreserveSpaces = 1 << 1, /** * Replace text emoticons smileys by emoticons images. * * @note * This option works only when KEmoticons framework is available at runtime, * and requires QGuiApplication, otherwise the flag is simply ignored. */ ReplaceSmileys = 1 << 2, /** * Don't parse and replace any URLs. */ IgnoreUrls = 1 << 3, /** * Interpret text highlighting markup, like *bold*, _underline_ and /italic/, * and wrap them in corresponding HTML entities. */ HighlightText = 1 << 4, /** * Replace phone numbers with tel: links. * @since 5.56.0 */ ConvertPhoneNumbers = 1 << 5 }; Q_DECLARE_FLAGS(Options, Option) /** * Converts plaintext into html. The following characters are converted * to HTML entities: & " < >. Newlines are also preserved. * * @param plainText The text to be converted into HTML. * @param options The options to use when processing @p plainText. * @param maxUrlLen The maximum length of permitted URLs. The reason for * this limit is that there may be possible security * implications in handling URLs of unlimited length. * @param maxAddressLen The maximum length of permitted email addresses. * The reason for this limit is that there may be possible * security implications in handling addresses of unlimited * length. * * @return An HTML version of the text supplied in the 'plainText' * parameter, suitable for inclusion in the BODY of an HTML document. * * @since 5.5.0 */ KCOREADDONS_EXPORT QString convertToHtml(const QString &plainText, const KTextToHTML::Options &options, int maxUrlLen = 4096, int maxAddressLen = 255); } Q_DECLARE_OPERATORS_FOR_FLAGS(KTextToHTML::Options) #endif diff --git a/src/lib/text/ktexttohtml_p.h b/src/lib/text/ktexttohtml_p.h index 06d6a5c..fb96f28 100644 --- a/src/lib/text/ktexttohtml_p.h +++ b/src/lib/text/ktexttohtml_p.h @@ -1,63 +1,62 @@ /* Copyright (c) 2002 Dave Corrie Copyright (c) 2014 Daniel Vrátil This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KTEXTTOHTML_P_H #define KTEXTTOHTML_P_H -#include #include "kcoreaddons_export.h" #include "ktexttohtmlemoticonsinterface.h" class KTextToHTMLEmoticonsDummy : public KTextToHTMLEmoticonsInterface { public: QString parseEmoticons(const QString &text, bool strictParse = false, const QStringList &exclude = QStringList()) override { Q_UNUSED(strictParse); Q_UNUSED(exclude); return text; } }; class KTextToHTMLHelper { public: KTextToHTMLHelper(const QString &plainText, int pos = 0, int maxUrlLen = 4096, int maxAddressLen = 255); KTextToHTMLEmoticonsInterface *emoticonsInterface() const; QString getEmailAddress(); QString getPhoneNumber(); bool atUrl() const; bool isEmptyUrl(const QString &url) const; QString getUrl(bool *badurl = nullptr); QString pngToDataUrl(const QString &pngPath) const; QString highlightedText(); QString mText; int mMaxUrlLen; int mMaxAddressLen; int mPos; }; #endif diff --git a/src/lib/util/kosrelease.cpp b/src/lib/util/kosrelease.cpp index ecbaead..0469aba 100644 --- a/src/lib/util/kosrelease.cpp +++ b/src/lib/util/kosrelease.cpp @@ -1,306 +1,305 @@ /* Copyright (C) 2014-2019 Harald Sitter This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "kosrelease.h" -#include #include #include "kcoreaddons_debug.h" #include "kshell.h" // Sets a QString var static void setVar(QString *var, const QString &value) { // Values may contain quotation marks, strip them as we have no use for them. KShell::Errors error; QStringList args = KShell::splitArgs(value, KShell::NoOptions, &error); if (error != KShell::NoError) { // Failed to parse. return; } *var = args.join(QLatin1Char(' ')); } // Sets a QStringList var (i.e. splits a string value) static void setVar(QStringList *var, const QString &value) { // Instead of passing the verbatim value we manually strip any initial quotes // and then run it through KShell. At this point KShell will actually split // by spaces giving us the final QStringList. // NOTE: Splitting like this does not actually allow escaped substrings to // be handled correctly, so "kitteh \"french fries\"" would result in // three list entries. I'd argue that if someone makes an id like that // they are at fault for the bogus parsing here though as id explicitly // is required to not contain spaces even if more advanced shell escaping // is also allowed... QString value_ = value; if (value_.at(0) == QLatin1Char('"') && value_.at(value_.size()-1) == QLatin1Char('"')) { value_.remove(0, 1); value_.remove(-1, 1); } KShell::Errors error; QStringList args = KShell::splitArgs(value_, KShell::NoOptions, &error); if (error != KShell::NoError) { // Failed to parse. return; } *var = args; } static QStringList splitEntry(const QString &line) { QStringList list; const int separatorIndex = line.indexOf(QLatin1Char('=')); list << line.mid(0, separatorIndex); if (separatorIndex != -1) { list << line.mid(separatorIndex + 1, -1); } return list; } static QString defaultFilePath() { if (QFile::exists(QStringLiteral("/etc/os-release"))) { return QStringLiteral("/etc/os-release"); } else if (QFile::exists(QStringLiteral("/usr/lib/os-release"))) { return QStringLiteral("/usr/lib/os-release"); } else { return QString(); } } class Q_DECL_HIDDEN KOSRelease::Private { public: Private(QString filePath) : name(QStringLiteral("Linux")) , id(QStringLiteral("linux")) , prettyName(QStringLiteral("Linux")) { // Default values for non-optional fields set above ^. QHash stringHash = { { QStringLiteral("NAME"), &name }, { QStringLiteral("VERSION"), &version }, { QStringLiteral("ID"), &id }, // idLike is not a QString, special handling below! { QStringLiteral("VERSION_CODENAME"), &versionCodename }, { QStringLiteral("VERSION_ID"), &versionId }, { QStringLiteral("PRETTY_NAME"), &prettyName }, { QStringLiteral("ANSI_COLOR"), &ansiColor }, { QStringLiteral("CPE_NAME"), &cpeName }, { QStringLiteral("HOME_URL"), &homeUrl }, { QStringLiteral("DOCUMENTATION_URL"), &documentationUrl }, { QStringLiteral("SUPPORT_URL"), &supportUrl }, { QStringLiteral("BUG_REPORT_URL"), &bugReportUrl }, { QStringLiteral("PRIVACY_POLICY_URL"), &privacyPolicyUrl }, { QStringLiteral("BUILD_ID"), &buildId }, { QStringLiteral("VARIANT"), &variant }, { QStringLiteral("VARIANT_ID"), &variantId }, { QStringLiteral("LOGO"), &logo } }; if (filePath.isEmpty()) { filePath = defaultFilePath(); } if (filePath.isEmpty()) { qCWarning(KCOREADDONS_DEBUG) << "Failed to find os-release file!"; return; } QFile file(filePath); // NOTE: The os-release specification defines default values for specific // fields which means that even if we can not read the os-release file // we have sort of expected default values to use. // TODO: it might still be handy to indicate to the outside whether // fallback values are being used or not. file.open(QIODevice::ReadOnly | QIODevice::Text); QString line; QStringList parts; while (!file.atEnd()) { // Trimmed to handle indented comment lines properly line = QString::fromLatin1(file.readLine()).trimmed(); if (line.startsWith(QLatin1Char('#'))) { // Comment line // Lines beginning with "#" shall be ignored as comments. continue; } parts = splitEntry(line); if (parts.size() != 2) { // Line has no =, must be invalid. qCDebug(KCOREADDONS_DEBUG) << "Unexpected/invalid os-release line:" << line; continue; } QString key = parts.at(0); QString value = parts.at(1).trimmed(); if (QString *var = stringHash.value(key, nullptr)) { setVar(var, value); continue; } // ID_LIKE is a list and parsed as such (rather than a QString). if (key == QLatin1String("ID_LIKE")) { setVar(&idLike, value); continue; } // os-release explicitly allows for vendor specific additions, we'll // collect them as strings and exposes them as "extras". QString parsedValue; setVar(&parsedValue, value); extras.insert(key, parsedValue); } } QString name; QString version; QString id; QStringList idLike; QString versionCodename; QString versionId; QString prettyName; QString ansiColor; QString cpeName; QString homeUrl; QString documentationUrl; QString supportUrl; QString bugReportUrl; QString privacyPolicyUrl; QString buildId; QString variant; QString variantId; QString logo; QHash extras; }; KOSRelease::KOSRelease(const QString &filePath) : d(new Private(filePath)) { } KOSRelease::~KOSRelease() { delete d; } QString KOSRelease::name() const { return d->name; } QString KOSRelease::version() const { return d->version; } QString KOSRelease::id() const { return d->id; } QStringList KOSRelease::idLike() const { return d->idLike; } QString KOSRelease::versionCodename() const { return d->versionCodename; } QString KOSRelease::versionId() const { return d->versionId; } QString KOSRelease::prettyName() const { return d->prettyName; } QString KOSRelease::ansiColor() const { return d->ansiColor; } QString KOSRelease::cpeName() const { return d->cpeName; } QString KOSRelease::homeUrl() const { return d->homeUrl; } QString KOSRelease::documentationUrl() const { return d->documentationUrl; } QString KOSRelease::supportUrl() const { return d->supportUrl; } QString KOSRelease::bugReportUrl() const { return d->bugReportUrl; } QString KOSRelease::privacyPolicyUrl() const { return d->privacyPolicyUrl; } QString KOSRelease::buildId() const { return d->buildId; } QString KOSRelease::variant() const { return d->variant; } QString KOSRelease::variantId() const { return d->variantId; } QString KOSRelease::logo() const { return d->logo; } QStringList KOSRelease::extraKeys() const { return d->extras.keys(); } QString KOSRelease::extraValue(const QString &key) const { return d->extras.value(key); } diff --git a/src/lib/util/kuser_unix.cpp b/src/lib/util/kuser_unix.cpp index 216dfb7..d37c628 100644 --- a/src/lib/util/kuser_unix.cpp +++ b/src/lib/util/kuser_unix.cpp @@ -1,564 +1,562 @@ /* * KUser - represent a user/account * Copyright (C) 2002 Tim Jansen * Copyright (C) 2014 Alex Richardson * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kuser.h" #include "config-getgrouplist.h" #include "config-accountsservice.h" -#include -#include #include #include #include #include #include #include #include // std::find #include // std::function #if defined(__BIONIC__) && __ANDROID_API__ < 26 static inline struct passwd * getpwent() { return nullptr; } inline void setpwent() { } static inline void setgrent() { } static inline struct group * getgrent() { return nullptr; } inline void endpwent() { } static inline void endgrent() { } #endif class Q_DECL_HIDDEN KUser::Private : public QSharedData { public: uid_t uid; gid_t gid; QString loginName; QString homeDir, shell; QMap properties; Private() : uid(uid_t(-1)), gid(gid_t(-1)) {} Private(const char *name) : uid(uid_t(-1)), gid(gid_t(-1)) { fillPasswd(name ? ::getpwnam(name) : nullptr); } Private(const passwd *p) : uid(uid_t(-1)), gid(gid_t(-1)) { fillPasswd(p); } void fillPasswd(const passwd *p) { if (p) { #ifndef __BIONIC__ QString gecos = QString::fromLocal8Bit(p->pw_gecos); #else QString gecos = QString(); #endif QStringList gecosList = gecos.split(QLatin1Char(',')); // fill up the list, should be at least 4 entries while (gecosList.size() < 4) { gecosList << QString(); } uid = p->pw_uid; gid = p->pw_gid; loginName = QString::fromLocal8Bit(p->pw_name); properties[KUser::FullName] = QVariant(gecosList[0]); properties[KUser::RoomNumber] = QVariant(gecosList[1]); properties[KUser::WorkPhone] = QVariant(gecosList[2]); properties[KUser::HomePhone] = QVariant(gecosList[3]); if (uid == ::getuid() && uid == ::geteuid()) { homeDir = QFile::decodeName(qgetenv("HOME")); } if (homeDir.isEmpty()) { homeDir = QFile::decodeName(p->pw_dir); } shell = QString::fromLocal8Bit(p->pw_shell); } } }; KUser::KUser(UIDMode mode) { uid_t _uid = ::getuid(), _euid; if (mode == UseEffectiveUID && (_euid = ::geteuid()) != _uid) { d = new Private(::getpwuid(_euid)); } else { d = new Private(qgetenv("LOGNAME").constData()); if (d->uid != _uid) { d = new Private(qgetenv("USER").constData()); if (d->uid != _uid) { d = new Private(::getpwuid(_uid)); } } } } KUser::KUser(K_UID _uid) : d(new Private(::getpwuid(_uid))) { } KUser::KUser(KUserId _uid) : d(new Private(::getpwuid(_uid.nativeId()))) { } KUser::KUser(const QString &name) : d(new Private(name.toLocal8Bit().data())) { } KUser::KUser(const char *name) : d(new Private(name)) { } KUser::KUser(const passwd *p) : d(new Private(p)) { } KUser::KUser(const KUser &user) : d(user.d) { } KUser &KUser::operator =(const KUser &user) { d = user.d; return *this; } bool KUser::operator ==(const KUser &user) const { return isValid() && (d->uid == user.d->uid); } bool KUser::isValid() const { return d->uid != uid_t(-1); } KUserId KUser::userId() const { return KUserId(d->uid); } KGroupId KUser::groupId() const { return KGroupId(d->gid); } bool KUser::isSuperUser() const { return d->uid == 0; } QString KUser::loginName() const { return d->loginName; } QString KUser::homeDir() const { return d->homeDir; } QString KUser::faceIconPath() const { QString pathToFaceIcon; if (!d->loginName.isEmpty()) { pathToFaceIcon = QStringLiteral(ACCOUNTS_SERVICE_ICON_DIR) + QLatin1Char('/') + d->loginName; } if (QFile::exists(pathToFaceIcon)) { return pathToFaceIcon; } pathToFaceIcon = QString(homeDir() + QLatin1Char('/') + QStringLiteral(".face.icon")); if (QFileInfo(pathToFaceIcon).isReadable()) { return pathToFaceIcon; } return QString(); } QString KUser::shell() const { return d->shell; } template static void listGroupsForUser(const char *name, gid_t gid, uint maxCount, Func handleNextGroup) { if (Q_UNLIKELY(maxCount == 0)) { return; } uint found = 0; #if HAVE_GETGROUPLIST QVarLengthArray gid_buffer; gid_buffer.resize(100); int numGroups = gid_buffer.size(); int result = getgrouplist(name, gid, gid_buffer.data(), &numGroups); if (result < 0 && uint(numGroups) < maxCount) { // getgrouplist returns -1 if the buffer was too small to store all entries, the required size is in numGroups qDebug("Buffer was too small: %d, need %d", gid_buffer.size(), numGroups); gid_buffer.resize(numGroups); numGroups = gid_buffer.size(); getgrouplist(name, gid, gid_buffer.data(), &numGroups); } for (int i = 0; i < numGroups && found < maxCount; ++i) { struct group *g = getgrgid(gid_buffer[i]); // should never be null, but better be safe than crash if (g) { found++; handleNextGroup(g); } } #else // fall back to getgrent() and reading gr->gr_mem // This is slower than getgrouplist, but works as well // add the current gid, this is often not part of g->gr_mem (e.g. build.kde.org or my openSuSE 13.1 system) struct group *g = getgrgid(gid); if (g) { handleNextGroup(g); found++; if (found >= maxCount) { return; } } static const auto groupContainsUser = [](struct group * g, const char *name) -> bool { for (char **user = g->gr_mem; *user; user++) { if (strcmp(name, *user) == 0) { return true; } } return false; }; setgrent(); while ((g = getgrent())) { // don't add the current gid again if (g->gr_gid != gid && groupContainsUser(g, name)) { handleNextGroup(g); found++; if (found >= maxCount) { break; } } } endgrent(); #endif } QList KUser::groups(uint maxCount) const { QList result; listGroupsForUser( d->loginName.toLocal8Bit().constData(), d->gid, maxCount, [&](const group * g) { result.append(KUserGroup(g)); } ); return result; } QStringList KUser::groupNames(uint maxCount) const { QStringList result; listGroupsForUser( d->loginName.toLocal8Bit().constData(), d->gid, maxCount, [&](const group * g) { result.append(QString::fromLocal8Bit(g->gr_name)); } ); return result; } QVariant KUser::property(UserProperty which) const { return d->properties.value(which); } QList KUser::allUsers(uint maxCount) { QList result; passwd *p; setpwent(); for (uint i = 0; i < maxCount && (p = getpwent()); ++i) { result.append(KUser(p)); } endpwent(); return result; } QStringList KUser::allUserNames(uint maxCount) { QStringList result; passwd *p; setpwent(); for (uint i = 0; i < maxCount && (p = getpwent()); ++i) { result.append(QString::fromLocal8Bit(p->pw_name)); } endpwent(); return result; } KUser::~KUser() { } class Q_DECL_HIDDEN KUserGroup::Private : public QSharedData { public: gid_t gid; QString name; Private() : gid(gid_t(-1)) {} Private(const char *_name) : gid(gid_t(-1)) { fillGroup(_name ? ::getgrnam(_name) : nullptr); } Private(const ::group *p) : gid(gid_t(-1)) { fillGroup(p); } void fillGroup(const ::group *p) { if (p) { gid = p->gr_gid; name = QString::fromLocal8Bit(p->gr_name); } } }; KUserGroup::KUserGroup(KUser::UIDMode mode) { d = new Private(getgrgid(KUser(mode).groupId().nativeId())); } KUserGroup::KUserGroup(K_GID _gid) : d(new Private(getgrgid(_gid))) { } KUserGroup::KUserGroup(KGroupId _gid) : d(new Private(getgrgid(_gid.nativeId()))) { } KUserGroup::KUserGroup(const QString &_name) : d(new Private(_name.toLocal8Bit().data())) { } KUserGroup::KUserGroup(const char *_name) : d(new Private(_name)) { } KUserGroup::KUserGroup(const ::group *g) : d(new Private(g)) { } KUserGroup::KUserGroup(const KUserGroup &group) : d(group.d) { } KUserGroup &KUserGroup::operator =(const KUserGroup &group) { d = group.d; return *this; } bool KUserGroup::operator ==(const KUserGroup &group) const { return isValid() && (d->gid == group.d->gid); } bool KUserGroup::isValid() const { return d->gid != gid_t(-1); } KGroupId KUserGroup::groupId() const { return KGroupId(d->gid); } QString KUserGroup::name() const { return d->name; } static void listGroupMembers(gid_t gid, uint maxCount, std::function handleNextGroupUser) { if (maxCount == 0) { return; } struct group *g = getgrgid(gid); if (!g) { return; } uint found = 0; QVarLengthArray addedUsers; struct passwd *p = nullptr; for (char **user = g->gr_mem; *user; user++) { if ((p = getpwnam(*user))) { addedUsers.append(p->pw_uid); handleNextGroupUser(p); found++; if (found >= maxCount) { break; } } } //gr_mem doesn't contain users where the primary group == gid -> we have to iterate over all users setpwent(); while ((p = getpwent()) && found < maxCount) { if (p->pw_gid != gid) { continue; // only need primary gid since otherwise gr_mem already contains this user } // make sure we don't list a user twice if (std::find(addedUsers.cbegin(), addedUsers.cend(), p->pw_uid) == addedUsers.cend()) { handleNextGroupUser(p); found++; } } endpwent(); } QList KUserGroup::users(uint maxCount) const { QList result; listGroupMembers(d->gid, maxCount, [&](const passwd *p) { result.append(KUser(p)); }); return result; } QStringList KUserGroup::userNames(uint maxCount) const { QStringList result; listGroupMembers(d->gid, maxCount, [&](const passwd *p) { result.append(QString::fromLocal8Bit(p->pw_name)); }); return result; } QList KUserGroup::allGroups(uint maxCount) { QList result; ::group *g; setgrent(); for (uint i = 0; i < maxCount && (g = getgrent()); ++i) { result.append(KUserGroup(g)); } endgrent(); return result; } QStringList KUserGroup::allGroupNames(uint maxCount) { QStringList result; ::group *g; setgrent(); for (uint i = 0; i < maxCount && (g = getgrent()); ++i) { result.append(QString::fromLocal8Bit(g->gr_name)); } endgrent(); return result; } KUserGroup::~KUserGroup() { } KUserId KUserId::fromName(const QString &name) { if (name.isEmpty()) { return KUserId(); } QByteArray name8Bit = name.toLocal8Bit(); struct passwd *p = ::getpwnam(name8Bit.constData()); if (!p) { qWarning("Failed to lookup user %s: %s", name8Bit.constData(), strerror(errno)); return KUserId(); } return KUserId(p->pw_uid); } KGroupId KGroupId::fromName(const QString &name) { if (name.isEmpty()) { return KGroupId(); } QByteArray name8Bit = name.toLocal8Bit(); struct group *g = ::getgrnam(name8Bit.constData()); if (!g) { qWarning("Failed to lookup group %s: %s", name8Bit.constData(), strerror(errno)); return KGroupId(); } return KGroupId(g->gr_gid); } KUserId KUserId::currentUserId() { return KUserId(getuid()); } KUserId KUserId::currentEffectiveUserId() { return KUserId(geteuid()); } KGroupId KGroupId::currentGroupId() { return KGroupId(getgid()); } KGroupId KGroupId::currentEffectiveGroupId() { return KGroupId(getegid()); } diff --git a/tests/faceicontest.cpp b/tests/faceicontest.cpp index e4fb399..7696f42 100644 --- a/tests/faceicontest.cpp +++ b/tests/faceicontest.cpp @@ -1,54 +1,53 @@ /* * Copyright (C) 2014 Nicolás Alvarez * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "faceicontest.h" #include #include #include -#include #include int main(int argc, char **argv) { QApplication app(argc, argv); FaceIconTest *mainWin = new FaceIconTest(); mainWin->show(); return app.exec(); } FaceIconTest::FaceIconTest() { QVBoxLayout *layout = new QVBoxLayout(this); listWidget = new QListWidget(this); layout->addWidget(listWidget); const QList users = KUser::allUsers(); for (const KUser &u : users) { QPixmap pixmap(u.faceIconPath()); if (pixmap.isNull()) { pixmap = QPixmap(QSize(48, 48)); pixmap.fill(); } else { pixmap = pixmap.scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } QListWidgetItem *item = new QListWidgetItem(u.loginName(), listWidget); item->setData(Qt::DecorationRole, pixmap); } } diff --git a/tests/kdirwatchtest.cpp b/tests/kdirwatchtest.cpp index ef958dc..f2b9a62 100644 --- a/tests/kdirwatchtest.cpp +++ b/tests/kdirwatchtest.cpp @@ -1,87 +1,86 @@ /* This file is part of the KDE libraries Copyright 1998 Sven Radej This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kdirwatchtest.h" #include -#include #include #include // TODO debug crash when calling "./kdirwatchtest ./kdirwatchtest" int main(int argc, char **argv) { // TODO port to QCommandLineArguments once it exists //options.add("+[directory ...]", qi18n("Directory(ies) to watch")); QCoreApplication a(argc, argv); myTest testObject; KDirWatch *dirwatch1 = KDirWatch::self(); KDirWatch *dirwatch2 = new KDirWatch; testObject.connect(dirwatch1, SIGNAL(dirty(QString)), SLOT(dirty(QString))); testObject.connect(dirwatch1, SIGNAL(created(QString)), SLOT(created(QString))); testObject.connect(dirwatch1, SIGNAL(deleted(QString)), SLOT(deleted(QString))); // TODO port to QCommandLineArguments once it exists const QStringList args = a.arguments(); for (int i = 1; i < args.count(); ++i) { const QString arg = args.at(i); if (!arg.startsWith("-")) { qDebug() << "Watching: " << arg; dirwatch2->addDir(arg); } } QString home = QString(getenv("HOME")) + '/'; QString desk = home + "Desktop/"; qDebug() << "Watching: " << home; dirwatch1->addDir(home); qDebug() << "Watching file: " << home << "foo "; dirwatch1->addFile(home + "foo"); qDebug() << "Watching: " << desk; dirwatch1->addDir(desk); QString test = home + "test/"; qDebug() << "Watching: (but skipped) " << test; dirwatch1->addDir(test); dirwatch1->startScan(); dirwatch2->startScan(); if (!dirwatch1->stopDirScan(home)) { qDebug() << "stopDirscan: " << home << " error!"; } if (!dirwatch1->restartDirScan(home)) { qDebug() << "restartDirScan: " << home << "error!"; } if (!dirwatch1->stopDirScan(test)) { qDebug() << "stopDirScan: error"; } KDirWatch::statistics(); delete dirwatch2; KDirWatch::statistics(); return a.exec(); } diff --git a/tests/kdirwatchtest_gui.cpp b/tests/kdirwatchtest_gui.cpp index 46b5aa2..deb4d92 100644 --- a/tests/kdirwatchtest_gui.cpp +++ b/tests/kdirwatchtest_gui.cpp @@ -1,140 +1,140 @@ /* * Copyright (C) 2006 Dirk Stoecker * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation; * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kdirwatchtest_gui.h" #include #include #include #include #include -#include #include #include #include +#include int main(int argc, char **argv) { QApplication app(argc, argv); KDirWatchTest_GUI *mainWin = new KDirWatchTest_GUI(); mainWin->show(); return app.exec(); } KDirWatchTest_GUI::KDirWatchTest_GUI() : QWidget() { QPushButton *e, *f; QVBoxLayout *lay = new QVBoxLayout(this); lay->setContentsMargins(0, 0, 0, 0); lay->addWidget(l1 = new QLineEdit(QLatin1String("Test 1"), this)); lay->addWidget(l2 = new QLineEdit(QLatin1String("Test 2"), this)); lay->addWidget(l3 = new QLineEdit(QLatin1String("Test 3"), this)); lay->addWidget(m_eventBrowser = new QTextBrowser(this)); lay->addWidget(d = new QLineEdit(QLatin1String("Status"), this)); lay->addWidget(e = new QPushButton(QLatin1String("new file"), this)); lay->addWidget(f = new QPushButton(QLatin1String("delete file"), this)); dir = QDir::currentPath(); file = dir + QLatin1String("/testfile_kdirwatchtest_gui"); w1 = new KDirWatch(); w1->setObjectName(QLatin1String("w1")); w2 = new KDirWatch(); w2->setObjectName(QLatin1String("w2")); w3 = new KDirWatch(); w3->setObjectName(QLatin1String("w3")); connect(w1, SIGNAL(dirty(QString)), this, SLOT(slotDir1(QString))); connect(w2, SIGNAL(dirty(QString)), this, SLOT(slotDir2(QString))); connect(w3, SIGNAL(dirty(QString)), this, SLOT(slotDir3(QString))); w1->addDir(dir); w2->addDir(dir); w3->addDir(dir); KDirWatch *w4 = new KDirWatch(this); w4->setObjectName(QLatin1String("w4")); w4->addDir(dir, KDirWatch::WatchFiles | KDirWatch::WatchSubDirs); connect(w1, SIGNAL(dirty(QString)), this, SLOT(slotDirty(QString))); connect(w1, SIGNAL(created(QString)), this, SLOT(slotCreated(QString))); connect(w1, SIGNAL(deleted(QString)), this, SLOT(slotDeleted(QString))); KDirWatch *w5 = new KDirWatch(this); w5->setObjectName(QLatin1String(QLatin1String("w5"))); w5->addFile(file); connect(w5, SIGNAL(dirty(QString)), this, SLOT(slotDirty(QString))); connect(w5, SIGNAL(created(QString)), this, SLOT(slotCreated(QString))); connect(w5, SIGNAL(deleted(QString)), this, SLOT(slotDeleted(QString))); lay->addWidget(new QLabel(QLatin1String("Directory = ") + dir, this)); lay->addWidget(new QLabel(QLatin1String("File = ") + file, this)); connect(e, SIGNAL(clicked()), this, SLOT(slotNewClicked())); connect(f, SIGNAL(clicked()), this, SLOT(slotDeleteClicked())); setMinimumWidth(800); setMinimumHeight(400); } void KDirWatchTest_GUI::slotDir1(const QString &a) { l1->setText(QLatin1String("Test 1 changed ") + a + QLatin1String(" at ") + QTime::currentTime().toString()); } void KDirWatchTest_GUI::slotDir2(const QString &a) { // This used to cause bug #119341, fixed now #if 1 w2->stopDirScan(QLatin1String(a.toLatin1().constData())); w2->restartDirScan(QLatin1String(a.toLatin1().constData())); #endif l2->setText(QLatin1String("Test 2 changed ") + a + QLatin1String(" at ") + QTime::currentTime().toString()); } void KDirWatchTest_GUI::slotDir3(const QString &a) { l3->setText(QLatin1String("Test 3 changed ") + a + QLatin1String(" at )") + QTime::currentTime().toString()); } void KDirWatchTest_GUI::slotDeleteClicked() { remove(file.toLatin1().constData()); d->setText(QLatin1String("Delete clicked at ") + QTime::currentTime().toString()); } void KDirWatchTest_GUI::slotNewClicked() { fclose(QT_FOPEN(file.toLatin1().constData(), "wb")); d->setText(QLatin1String("New clicked at ") + QTime::currentTime().toString()); } void KDirWatchTest_GUI::slotDirty(const QString &path) { m_eventBrowser->append(QLatin1String("Dirty(") + sender()->objectName() + QLatin1String("): ") + path + QLatin1Char('\n')); } void KDirWatchTest_GUI::slotCreated(const QString &path) { m_eventBrowser->append(QLatin1String("Created(") + sender()->objectName() + QLatin1String("): ") + path + QLatin1Char('\n')); } void KDirWatchTest_GUI::slotDeleted(const QString &path) { m_eventBrowser->append(QLatin1String("Deleted(") + sender()->objectName() + QLatin1String("): ") + path + QLatin1Char('\n')); }