| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2016 The Qt Company Ltd. |
| 4 | ** Copyright (C) 2018 Intel Corporation. |
| 5 | ** Contact: https://www.qt.io/licensing/ |
| 6 | ** |
| 7 | ** This file is part of the test suite of the Qt Toolkit. |
| 8 | ** |
| 9 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 10 | ** Commercial License Usage |
| 11 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 12 | ** accordance with the commercial license agreement provided with the |
| 13 | ** Software or, alternatively, in accordance with the terms contained in |
| 14 | ** a written agreement between you and The Qt Company. For licensing terms |
| 15 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 16 | ** information use the contact form at https://www.qt.io/contact-us. |
| 17 | ** |
| 18 | ** GNU General Public License Usage |
| 19 | ** Alternatively, this file may be used under the terms of the GNU |
| 20 | ** General Public License version 3 as published by the Free Software |
| 21 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 22 | ** included in the packaging of this file. Please review the following |
| 23 | ** information to ensure the GNU General Public License requirements will |
| 24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 25 | ** |
| 26 | ** $QT_END_LICENSE$ |
| 27 | ** |
| 28 | ****************************************************************************/ |
| 29 | #include <QtTest/QtTest> |
| 30 | |
| 31 | #include <QCoreApplication> |
| 32 | #include <QDebug> |
| 33 | #include <QDir> |
| 34 | #include <qplugin.h> |
| 35 | #include <QPluginLoader> |
| 36 | |
| 37 | #include <private/qplugin_p.h> |
| 38 | |
| 39 | class tst_QPlugin : public QObject |
| 40 | { |
| 41 | Q_OBJECT |
| 42 | |
| 43 | QDir dir; |
| 44 | |
| 45 | public: |
| 46 | tst_QPlugin(); |
| 47 | |
| 48 | private slots: |
| 49 | void initTestCase(); |
| 50 | void loadDebugPlugin(); |
| 51 | void loadReleasePlugin(); |
| 52 | void scanInvalidPlugin_data(); |
| 53 | void scanInvalidPlugin(); |
| 54 | }; |
| 55 | |
| 56 | tst_QPlugin::tst_QPlugin() |
| 57 | : dir(QFINDTESTDATA("plugins" )) |
| 58 | { |
| 59 | } |
| 60 | |
| 61 | void tst_QPlugin::initTestCase() |
| 62 | { |
| 63 | QVERIFY2(dir.exists(), |
| 64 | qPrintable(QString::fromLatin1("Cannot find the 'plugins' directory starting from '%1'" ). |
| 65 | arg(QDir::toNativeSeparators(QDir::currentPath())))); |
| 66 | } |
| 67 | |
| 68 | void tst_QPlugin::loadDebugPlugin() |
| 69 | { |
| 70 | const auto fileNames = dir.entryList(nameFilters: QStringList() << "*debug*" , filters: QDir::Files); |
| 71 | if (fileNames.isEmpty()) |
| 72 | QSKIP("No debug plugins found - skipping test" ); |
| 73 | |
| 74 | for (const QString &fileName : fileNames) { |
| 75 | if (!QLibrary::isLibrary(fileName)) |
| 76 | continue; |
| 77 | QPluginLoader loader(dir.filePath(fileName)); |
| 78 | #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) |
| 79 | // we can always load a plugin on unix |
| 80 | QVERIFY(loader.load()); |
| 81 | QObject *object = loader.instance(); |
| 82 | QVERIFY(object != 0); |
| 83 | #else |
| 84 | // loading a plugin is dependent on which lib we are running against |
| 85 | # if defined(QT_NO_DEBUG) |
| 86 | // release build, we cannot load debug plugins |
| 87 | QVERIFY(!loader.load()); |
| 88 | # else |
| 89 | // debug build, we can load debug plugins |
| 90 | QVERIFY(loader.load()); |
| 91 | QObject *object = loader.instance(); |
| 92 | QVERIFY(object != 0); |
| 93 | # endif |
| 94 | #endif |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | void tst_QPlugin::loadReleasePlugin() |
| 99 | { |
| 100 | const auto fileNames = dir.entryList(nameFilters: QStringList() << "*release*" , filters: QDir::Files); |
| 101 | if (fileNames.isEmpty()) |
| 102 | QSKIP("No release plugins found - skipping test" ); |
| 103 | |
| 104 | for (const QString &fileName : fileNames) { |
| 105 | if (!QLibrary::isLibrary(fileName)) |
| 106 | continue; |
| 107 | QPluginLoader loader(dir.filePath(fileName)); |
| 108 | #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) |
| 109 | // we can always load a plugin on unix |
| 110 | QVERIFY(loader.load()); |
| 111 | QObject *object = loader.instance(); |
| 112 | QVERIFY(object != 0); |
| 113 | #else |
| 114 | // loading a plugin is dependent on which lib we are running against |
| 115 | # if defined(QT_NO_DEBUG) |
| 116 | // release build, we can load debug plugins |
| 117 | QVERIFY(loader.load()); |
| 118 | QObject *object = loader.instance(); |
| 119 | QVERIFY(object != 0); |
| 120 | # else |
| 121 | // debug build, we cannot load debug plugins |
| 122 | QVERIFY(!loader.load()); |
| 123 | # endif |
| 124 | #endif |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | void tst_QPlugin::scanInvalidPlugin_data() |
| 129 | { |
| 130 | QTest::addColumn<QByteArray>(name: "metadata" ); |
| 131 | QTest::addColumn<bool>(name: "loads" ); |
| 132 | QTest::addColumn<QString>(name: "errMsg" ); |
| 133 | |
| 134 | #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) |
| 135 | // Binary JSON metadata |
| 136 | QByteArray prefix = "QTMETADATA " ; |
| 137 | |
| 138 | { |
| 139 | QJsonObject obj; |
| 140 | obj.insert(key: "IID" , value: "org.qt-project.tst_qplugin" ); |
| 141 | obj.insert(key: "className" , value: "tst" ); |
| 142 | obj.insert(key: "version" , value: int(QT_VERSION)); |
| 143 | #ifdef QT_NO_DEBUG |
| 144 | obj.insert("debug" , false); |
| 145 | #else |
| 146 | obj.insert(key: "debug" , value: true); |
| 147 | #endif |
| 148 | obj.insert(key: "MetaData" , value: QJsonObject()); |
| 149 | QTest::newRow(dataTag: "json-control" ) << (prefix + QJsonDocument(obj).toBinaryData()) << true << "" ; |
| 150 | } |
| 151 | |
| 152 | QTest::newRow(dataTag: "json-zeroes" ) << prefix << false << " " ; |
| 153 | |
| 154 | prefix += "qbjs" ; |
| 155 | QTest::newRow(dataTag: "bad-json-version0" ) << prefix << false << " " ; |
| 156 | QTest::newRow(dataTag: "bad-json-version2" ) << (prefix + QByteArray("\2\0\0\0" , 4)) << false << " " ; |
| 157 | |
| 158 | // valid qbjs version 1 |
| 159 | prefix += QByteArray("\1\0\0\0" ); |
| 160 | |
| 161 | // too large for the file (100 MB) |
| 162 | QTest::newRow(dataTag: "bad-json-size-large1" ) << (prefix + QByteArray("\0\0\x40\x06" )) << false << " " ; |
| 163 | |
| 164 | // too large for binary JSON (512 MB) |
| 165 | QTest::newRow(dataTag: "bad-json-size-large2" ) << (prefix + QByteArray("\0\0\0\x20" )) << false << " " ; |
| 166 | |
| 167 | // could overflow |
| 168 | QTest::newRow(dataTag: "bad-json-size-large3" ) << (prefix + "\xff\xff\xff\x7f" ) << false << " " ; |
| 169 | #endif |
| 170 | |
| 171 | // CBOR metadata |
| 172 | QByteArray cprefix = "QTMETADATA !1234" ; |
| 173 | cprefix[12] = 0; // current version |
| 174 | cprefix[13] = QT_VERSION_MAJOR; |
| 175 | cprefix[14] = QT_VERSION_MINOR; |
| 176 | cprefix[15] = qPluginArchRequirements(); |
| 177 | |
| 178 | QByteArray cborValid = [] { |
| 179 | QCborMap m; |
| 180 | m.insert(key: int(QtPluginMetaDataKeys::IID), value_: QLatin1String("org.qt-project.tst_qplugin" )); |
| 181 | m.insert(key: int(QtPluginMetaDataKeys::ClassName), value_: QLatin1String("tst" )); |
| 182 | m.insert(key: int(QtPluginMetaDataKeys::MetaData), value_: QCborMap()); |
| 183 | return QCborValue(m).toCbor(); |
| 184 | }(); |
| 185 | QTest::newRow(dataTag: "cbor-control" ) << (cprefix + cborValid) << true << "" ; |
| 186 | |
| 187 | cprefix[12] = 1; |
| 188 | QTest::newRow(dataTag: "cbor-major-too-new" ) << (cprefix + cborValid) << false |
| 189 | << " Invalid metadata version" ; |
| 190 | |
| 191 | cprefix[12] = 0; |
| 192 | cprefix[13] = QT_VERSION_MAJOR + 1; |
| 193 | QTest::newRow(dataTag: "cbor-major-too-new" ) << (cprefix + cborValid) << false << "" ; |
| 194 | |
| 195 | cprefix[13] = QT_VERSION_MAJOR - 1; |
| 196 | QTest::newRow(dataTag: "cbor-major-too-old" ) << (cprefix + cborValid) << false << "" ; |
| 197 | |
| 198 | cprefix[13] = QT_VERSION_MAJOR; |
| 199 | cprefix[14] = QT_VERSION_MINOR + 1; |
| 200 | QTest::newRow(dataTag: "cbor-minor-too-new" ) << (cprefix + cborValid) << false << "" ; |
| 201 | |
| 202 | QTest::newRow(dataTag: "cbor-invalid" ) << (cprefix + "\xff" ) << false |
| 203 | << " Metadata parsing error: Invalid CBOR stream: unexpected 'break' byte" ; |
| 204 | QTest::newRow(dataTag: "cbor-not-map1" ) << (cprefix + "\x01" ) << false |
| 205 | << " Unexpected metadata contents" ; |
| 206 | QTest::newRow(dataTag: "cbor-not-map2" ) << (cprefix + "\x81\x01" ) << false |
| 207 | << " Unexpected metadata contents" ; |
| 208 | } |
| 209 | |
| 210 | static const char invalidPluginSignature[] = "qplugin testfile" ; |
| 211 | static qsizetype locateMetadata(const uchar *data, qsizetype len) |
| 212 | { |
| 213 | const uchar *dataend = data + len - strlen(s: invalidPluginSignature); |
| 214 | |
| 215 | for (const uchar *ptr = data; ptr < dataend; ++ptr) { |
| 216 | if (*ptr != invalidPluginSignature[0]) |
| 217 | continue; |
| 218 | |
| 219 | int r = memcmp(s1: ptr, s2: invalidPluginSignature, n: strlen(s: invalidPluginSignature)); |
| 220 | if (r) |
| 221 | continue; |
| 222 | |
| 223 | return ptr - data; |
| 224 | } |
| 225 | |
| 226 | return -1; |
| 227 | } |
| 228 | |
| 229 | void tst_QPlugin::scanInvalidPlugin() |
| 230 | { |
| 231 | #if defined(Q_OS_MACOS) && defined(Q_PROCESSOR_ARM) |
| 232 | QSKIP("This test crashes on ARM macOS" ); |
| 233 | #endif |
| 234 | const auto fileNames = dir.entryList(nameFilters: {"*invalid*" }, filters: QDir::Files); |
| 235 | QString invalidPluginName; |
| 236 | if (fileNames.isEmpty()) |
| 237 | QSKIP("No invalid plugin found - skipping test" ); |
| 238 | else |
| 239 | invalidPluginName = dir.absoluteFilePath(fileName: fileNames.first()); |
| 240 | |
| 241 | |
| 242 | // copy the file |
| 243 | QFileInfo fn(invalidPluginName); |
| 244 | QTemporaryDir tmpdir; |
| 245 | QVERIFY(tmpdir.isValid()); |
| 246 | |
| 247 | QString newName = tmpdir.path() + '/' + fn.fileName(); |
| 248 | QVERIFY(QFile::copy(invalidPluginName, newName)); |
| 249 | |
| 250 | { |
| 251 | QFile f(newName); |
| 252 | QVERIFY(f.open(QIODevice::ReadWrite | QIODevice::Unbuffered)); |
| 253 | QVERIFY(f.size() > qint64(strlen(invalidPluginSignature))); |
| 254 | uchar *data = f.map(offset: 0, size: f.size()); |
| 255 | QVERIFY(data); |
| 256 | |
| 257 | static const qsizetype offset = locateMetadata(data, len: f.size()); |
| 258 | QVERIFY(offset > 0); |
| 259 | |
| 260 | QFETCH(QByteArray, metadata); |
| 261 | |
| 262 | // sanity check |
| 263 | QVERIFY(metadata.size() < 512); |
| 264 | |
| 265 | // replace the data |
| 266 | memcpy(dest: data + offset, src: metadata.constData(), n: metadata.size()); |
| 267 | memset(s: data + offset + metadata.size(), c: 0, n: 512 - metadata.size()); |
| 268 | } |
| 269 | |
| 270 | // now try to load this |
| 271 | QFETCH(bool, loads); |
| 272 | QFETCH(QString, errMsg); |
| 273 | if (!errMsg.isEmpty()) |
| 274 | QTest::ignoreMessage(type: QtWarningMsg, |
| 275 | message: "Found invalid metadata in lib " + QFile::encodeName(fileName: newName) + |
| 276 | ":" + errMsg.toUtf8()); |
| 277 | QPluginLoader loader(newName); |
| 278 | QCOMPARE(loader.load(), loads); |
| 279 | if (loads) |
| 280 | loader.unload(); |
| 281 | } |
| 282 | |
| 283 | QTEST_MAIN(tst_QPlugin) |
| 284 | #include "tst_qplugin.moc" |
| 285 | |