/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2018 Intel Corporation.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QTest>
#include <QSignalSpy>
#include <QJsonArray>
#include <qdir.h>
#include <qpluginloader.h>
#include "theplugin/plugininterface.h"

#if defined(QT_BUILD_INTERNAL) && defined(Q_OF_MACH_O)
#  include <QtCore/private/qmachparser_p.h>
#endif

// Helper macros to let us know if some suffixes are valid
#define bundle_VALID    false
#define dylib_VALID     false
#define sl_VALID        false
#define a_VALID         false
#define so_VALID        false
#define dll_VALID       false

#if defined(Q_OS_DARWIN)
# undef bundle_VALID
# undef dylib_VALID
# undef so_VALID
# define bundle_VALID   true
# define dylib_VALID    true
# define so_VALID       true
# ifdef QT_NO_DEBUG
#  define SUFFIX         ".dylib"
# else
#  define SUFFIX         "_debug.dylib"
# endif
# define PREFIX         "lib"

#elif defined(Q_OS_HPUX) && !defined(__ia64)
# undef sl_VALID
# define sl_VALID       true
# define SUFFIX         ".sl"
# define PREFIX         "lib"

#elif defined(Q_OS_AIX)
# undef a_VALID
# undef so_VALID
# define a_VALID        true
# define so_VALID       true
# define SUFFIX         ".so"
# define PREFIX         "lib"

#elif defined(Q_OS_WIN)
# undef dll_VALID
# define dll_VALID      true
//# ifdef QT_NO_DEBUG
#  define SUFFIX         ".dll"
//# else
//#  define SUFFIX         "d.dll"
//# endif
# define PREFIX         ""

#else  // all other Unix
# undef so_VALID
# define so_VALID       true
# define SUFFIX         ".so"
# define PREFIX         "lib"
#endif

static QString sys_qualifiedLibraryName(const QString &fileName)
{
    QString name = QLatin1String("bin/") + QLatin1String(PREFIX) + fileName + QLatin1String(SUFFIX);
    const QString libname = QFINDTESTDATA(name);
    QFileInfo fi(libname);
    if (fi.exists())
        return fi.canonicalFilePath();
    return libname;
}

QT_FORWARD_DECLARE_CLASS(QPluginLoader)
class tst_QPluginLoader : public QObject
{
    Q_OBJECT
public slots:
    void cleanup();
private slots:
    void errorString();
    void loadHints();
    void deleteinstanceOnUnload();
    void loadDebugObj();
    void loadCorruptElf();
    void loadMachO_data();
    void loadMachO();
#if defined (Q_OS_UNIX)
    void loadGarbage();
#endif
    void relativePath();
    void absolutePath();
    void reloadPlugin();
    void preloadedPlugin_data();
    void preloadedPlugin();
    void staticPlugins();
};

Q_IMPORT_PLUGIN(StaticPlugin)

void tst_QPluginLoader::cleanup()
{
    // check if the library/plugin was leaked
    // we can't use QPluginLoader::isLoaded here because on some platforms the plugin is always loaded by QPluginLoader.
    // Also, if this test fails once, it will keep on failing because we can't force the unload,
    // so we report it only once.
    static bool failedAlready = false;
    if (!failedAlready) {
        QLibrary lib(sys_qualifiedLibraryName("theplugin"));
        failedAlready = true;
        QVERIFY2(!lib.isLoaded(), "Plugin was leaked - will not check again");
        failedAlready = false;
    }
}

void tst_QPluginLoader::errorString()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires Qt to create shared libraries.");
#endif

    const QString unknown(QLatin1String("Unknown error"));

    {
    QPluginLoader loader; // default constructed
    bool loaded = loader.load();
    QCOMPARE(loader.errorString(), unknown);
    QVERIFY(!loaded);

    QObject *obj = loader.instance();
    QCOMPARE(loader.errorString(), unknown);
    QCOMPARE(obj, static_cast<QObject*>(0));

    bool unloaded = loader.unload();
    QCOMPARE(loader.errorString(), unknown);
    QVERIFY(!unloaded);
    }
    {
    QPluginLoader loader( sys_qualifiedLibraryName("tst_qpluginloaderlib"));     //not a plugin
    bool loaded = loader.load();
    QVERIFY(loader.errorString() != unknown);
    QVERIFY(!loaded);

    QObject *obj = loader.instance();
    QVERIFY(loader.errorString() != unknown);
    QCOMPARE(obj, static_cast<QObject*>(0));

    bool unloaded = loader.unload();
    QVERIFY(loader.errorString() != unknown);
    QVERIFY(!unloaded);
    }

    {
    QPluginLoader loader( sys_qualifiedLibraryName("nosuchfile"));     //not a file
    bool loaded = loader.load();
    QVERIFY(loader.errorString() != unknown);
    QVERIFY(!loaded);

    QObject *obj = loader.instance();
    QVERIFY(loader.errorString() != unknown);
    QCOMPARE(obj, static_cast<QObject*>(0));

    bool unloaded = loader.unload();
    QVERIFY(loader.errorString() != unknown);
    QVERIFY(!unloaded);
    }

#if !defined(Q_OS_WIN) && !defined(Q_OS_MAC) && !defined(Q_OS_HPUX)
    {
    QPluginLoader loader( sys_qualifiedLibraryName("almostplugin"));     //a plugin with unresolved symbols
    loader.setLoadHints(QLibrary::ResolveAllSymbolsHint);
    bool loaded = loader.load();
    QVERIFY(loader.errorString() != unknown);
    QVERIFY(!loaded);

    QObject *obj = loader.instance();
    QVERIFY(loader.errorString() != unknown);
    QCOMPARE(obj, static_cast<QObject*>(0));

    bool unloaded = loader.unload();
    QVERIFY(loader.errorString() != unknown);
    QVERIFY(!unloaded);
    }
#endif

    {
    QPluginLoader loader( sys_qualifiedLibraryName("theplugin"));     //a plugin

    // Check metadata
    const QJsonObject metaData = loader.metaData();
    QCOMPARE(metaData.value("IID").toString(), QStringLiteral("org.qt-project.Qt.autotests.plugininterface"));
    const QJsonObject kpluginObject = metaData.value("MetaData").toObject().value("KPlugin").toObject();
    QCOMPARE(kpluginObject.value("Name[mr]").toString(), QString::fromUtf8("चौकट भूमिती"));

    // Load
    QCOMPARE(loader.load(), true);
    QCOMPARE(loader.errorString(), unknown);

    QVERIFY(loader.instance() !=  static_cast<QObject*>(0));
    QCOMPARE(loader.errorString(), unknown);

    // Make sure that plugin really works
    PluginInterface* theplugin = qobject_cast<PluginInterface*>(loader.instance());
    QString pluginName = theplugin->pluginName();
    QCOMPARE(pluginName, QLatin1String("Plugin ok"));

    QCOMPARE(loader.unload(), true);
    QCOMPARE(loader.errorString(), unknown);
    }
}

void tst_QPluginLoader::loadHints()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires Qt to create shared libraries.");
#endif
    QPluginLoader loader;
    QCOMPARE(loader.loadHints(), QLibrary::LoadHints{});   //Do not crash
    loader.setLoadHints(QLibrary::ResolveAllSymbolsHint);
    loader.setFileName( sys_qualifiedLibraryName("theplugin"));     //a plugin
    QCOMPARE(loader.loadHints(), QLibrary::ResolveAllSymbolsHint);
}

void tst_QPluginLoader::deleteinstanceOnUnload()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires Qt to create shared libraries.");
#endif
    for (int pass = 0; pass < 2; ++pass) {
        QPluginLoader loader1;
        loader1.setFileName( sys_qualifiedLibraryName("theplugin"));     //a plugin
        if (pass == 0)
            loader1.load(); // not recommended, instance() should do the job.
        PluginInterface *instance1 = qobject_cast<PluginInterface*>(loader1.instance());
        QVERIFY(instance1);
        QCOMPARE(instance1->pluginName(), QLatin1String("Plugin ok"));

        QPluginLoader loader2;
        loader2.setFileName( sys_qualifiedLibraryName("theplugin"));     //a plugin
        if (pass == 0)
            loader2.load(); // not recommended, instance() should do the job.
        PluginInterface *instance2 = qobject_cast<PluginInterface*>(loader2.instance());
        QCOMPARE(instance2->pluginName(), QLatin1String("Plugin ok"));

        QSignalSpy spy1(loader1.instance(), &QObject::destroyed);
        QSignalSpy spy2(loader2.instance(), &QObject::destroyed);
        QVERIFY(spy1.isValid());
        QVERIFY(spy2.isValid());
        if (pass == 0) {
            QCOMPARE(loader2.unload(), false);  // refcount not reached 0, not really unloaded
            QCOMPARE(spy1.count(), 0);
            QCOMPARE(spy2.count(), 0);
        }
        QCOMPARE(instance1->pluginName(), QLatin1String("Plugin ok"));
        QCOMPARE(instance2->pluginName(), QLatin1String("Plugin ok"));
        QVERIFY(loader1.unload());   // refcount reached 0, did really unload
        QCOMPARE(spy1.count(), 1);
        QCOMPARE(spy2.count(), 1);
    }
}

void tst_QPluginLoader::loadDebugObj()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires a shared build of Qt, as QPluginLoader::setFileName is a no-op in static builds");
#endif
#if defined (__ELF__)
    QVERIFY(QFile::exists(QFINDTESTDATA("elftest/debugobj.so")));
    QPluginLoader lib1(QFINDTESTDATA("elftest/debugobj.so"));
    QCOMPARE(lib1.load(), false);
#endif
}

void tst_QPluginLoader::loadCorruptElf()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires a shared build of Qt, as QPluginLoader::setFileName is a no-op in static builds");
#endif
#if defined (__ELF__)
    if (sizeof(void*) == 8) {
        QVERIFY(QFile::exists(QFINDTESTDATA("elftest/corrupt1.elf64.so")));

        QPluginLoader lib1(QFINDTESTDATA("elftest/corrupt1.elf64.so"));
        QCOMPARE(lib1.load(), false);
        QVERIFY2(lib1.errorString().contains("not an ELF object"), qPrintable(lib1.errorString()));

        QPluginLoader lib2(QFINDTESTDATA("elftest/corrupt2.elf64.so"));
        QCOMPARE(lib2.load(), false);
        QVERIFY2(lib2.errorString().contains("invalid"), qPrintable(lib2.errorString()));

        QPluginLoader lib3(QFINDTESTDATA("elftest/corrupt3.elf64.so"));
        QCOMPARE(lib3.load(), false);
        QVERIFY2(lib3.errorString().contains("invalid"), qPrintable(lib3.errorString()));
    } else if (sizeof(void*) == 4) {
        QPluginLoader libW(QFINDTESTDATA("elftest/corrupt3.elf64.so"));
        QCOMPARE(libW.load(), false);
        QVERIFY2(libW.errorString().contains("architecture"), qPrintable(libW.errorString()));
    } else {
        QFAIL("Please port QElfParser to this platform or blacklist this test.");
    }
#endif
}

void tst_QPluginLoader::loadMachO_data()
{
#if defined(QT_BUILD_INTERNAL) && defined(Q_OF_MACH_O)
    QTest::addColumn<int>("parseResult");

    QTest::newRow("/dev/null") << int(QMachOParser::NotSuitable);
    QTest::newRow("elftest/debugobj.so") << int(QMachOParser::NotSuitable);
    QTest::newRow("tst_qpluginloader.cpp") << int(QMachOParser::NotSuitable);
    QTest::newRow("tst_qpluginloader") << int(QMachOParser::NotSuitable);

#  ifdef Q_PROCESSOR_X86_64
    QTest::newRow("machtest/good.x86_64.dylib") << int(QMachOParser::QtMetaDataSection);
    QTest::newRow("machtest/good.i386.dylib") << int(QMachOParser::NotSuitable);
    QTest::newRow("machtest/good.fat.no-x86_64.dylib") << int(QMachOParser::NotSuitable);
    QTest::newRow("machtest/good.fat.no-i386.dylib") << int(QMachOParser::QtMetaDataSection);
#  elif defined(Q_PROCESSOR_X86_32)
    QTest::newRow("machtest/good.i386.dylib") << int(QMachOParser::QtMetaDataSection);
    QTest::newRow("machtest/good.x86_64.dylib") << int(QMachOParser::NotSuitable);
    QTest::newRow("machtest/good.fat.no-i386.dylib") << int(QMachOParser::NotSuitable);
    QTest::newRow("machtest/good.fat.no-x86_64.dylib") << int(QMachOParser::QtMetaDataSection);
#  endif
#  ifndef Q_PROCESSOR_POWER_64
    QTest::newRow("machtest/good.ppc64.dylib") << int(QMachOParser::NotSuitable);
#  endif

    QTest::newRow("machtest/good.fat.all.dylib") << int(QMachOParser::QtMetaDataSection);
    QTest::newRow("machtest/good.fat.stub-x86_64.dylib") << int(QMachOParser::NotSuitable);
    QTest::newRow("machtest/good.fat.stub-i386.dylib") << int(QMachOParser::NotSuitable);

    QDir d(QFINDTESTDATA("machtest"));
    QStringList badlist = d.entryList(QStringList() << "bad*.dylib");
    foreach (const QString &bad, badlist)
        QTest::newRow(qPrintable("machtest/" + bad)) << int(QMachOParser::NotSuitable);
#endif
}

void tst_QPluginLoader::loadMachO()
{
#if defined(QT_BUILD_INTERNAL) && defined(Q_OF_MACH_O)
    QFile f(QFINDTESTDATA(QTest::currentDataTag()));
    QVERIFY(f.open(QIODevice::ReadOnly));
    QByteArray data = f.readAll();

    qsizetype pos;
    qsizetype len;
    QString errorString;
    int r = QMachOParser::parse(data.constData(), data.size(), f.fileName(), &errorString, &pos, &len);

    QFETCH(int, parseResult);
    QCOMPARE(r, parseResult);

    if (r == QMachOParser::NotSuitable)
        return;

    QVERIFY(pos > 0);
    QVERIFY(size_t(len) >= sizeof(void*));
    QVERIFY(pos + long(len) < data.size());
    QCOMPARE(pos & (sizeof(void*) - 1), 0UL);

    void *value = *(void**)(data.constData() + pos);
    QCOMPARE(value, sizeof(void*) > 4 ? (void*)(0xc0ffeec0ffeeL) : (void*)0xc0ffee);

    // now that we know it's valid, let's try to make it invalid
    ulong offeredlen = pos;
    do {
        --offeredlen;
        r = QMachOParser::parse(data.constData(), offeredlen, f.fileName(), &errorString, &pos, &len);
        QVERIFY2(r == QMachOParser::NotSuitable, qPrintable(QString("Failed at size 0x%1").arg(offeredlen, 0, 16)));
    } while (offeredlen);
#endif
}

#if defined (Q_OS_UNIX)
void tst_QPluginLoader::loadGarbage()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires a shared build of Qt, as QPluginLoader::setFileName is a no-op in static builds");
#endif
    for (int i=0; i<5; i++) {
        const QString name = QLatin1String("elftest/garbage") + QString::number(i + 1) + QLatin1String(".so");
        QPluginLoader lib(QFINDTESTDATA(name));
        QCOMPARE(lib.load(), false);
        QVERIFY(lib.errorString() != QString("Unknown error"));
    }
}
#endif

void tst_QPluginLoader::relativePath()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires Qt to create shared libraries.");
#endif
    // Windows binaries run from release and debug subdirs, so we can't rely on the current dir.
    const QString binDir = QFINDTESTDATA("bin");
    QVERIFY(!binDir.isEmpty());
    QCoreApplication::addLibraryPath(binDir);
    QPluginLoader loader("theplugin" SUFFIX);
    loader.load(); // not recommended, instance() should do the job.
    PluginInterface *instance = qobject_cast<PluginInterface*>(loader.instance());
    QVERIFY(instance);
    QCOMPARE(instance->pluginName(), QLatin1String("Plugin ok"));
    QVERIFY(loader.unload());
}

void tst_QPluginLoader::absolutePath()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires Qt to create shared libraries.");
#endif
    // Windows binaries run from release and debug subdirs, so we can't rely on the current dir.
    const QString binDir = QFINDTESTDATA("bin");
    QVERIFY(!binDir.isEmpty());
    QVERIFY(QDir::isAbsolutePath(binDir));
    QPluginLoader loader(binDir + "/" PREFIX "theplugin" SUFFIX);
    loader.load(); // not recommended, instance() should do the job.
    PluginInterface *instance = qobject_cast<PluginInterface*>(loader.instance());
    QVERIFY(instance);
    QCOMPARE(instance->pluginName(), QLatin1String("Plugin ok"));
    QVERIFY(loader.unload());
}

void tst_QPluginLoader::reloadPlugin()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires Qt to create shared libraries.");
#endif
    QPluginLoader loader;
    loader.setFileName( sys_qualifiedLibraryName("theplugin"));     //a plugin
    loader.load(); // not recommended, instance() should do the job.
    PluginInterface *instance = qobject_cast<PluginInterface*>(loader.instance());
    QVERIFY(instance);
    QCOMPARE(instance->pluginName(), QLatin1String("Plugin ok"));

    QSignalSpy spy(loader.instance(), &QObject::destroyed);
    QVERIFY(spy.isValid());
    QVERIFY(loader.unload());   // refcount reached 0, did really unload
    QCOMPARE(spy.count(), 1);

    // reload plugin
    QVERIFY(loader.load());
    QVERIFY(loader.isLoaded());

    PluginInterface *instance2 = qobject_cast<PluginInterface*>(loader.instance());
    QVERIFY(instance2);
    QCOMPARE(instance2->pluginName(), QLatin1String("Plugin ok"));

    QVERIFY(loader.unload());
}

void tst_QPluginLoader::preloadedPlugin_data()
{
    QTest::addColumn<bool>("doLoad");
    QTest::addColumn<QString>("libname");
    QTest::newRow("create-plugin") << false << sys_qualifiedLibraryName("theplugin");
    QTest::newRow("load-plugin") << true << sys_qualifiedLibraryName("theplugin");
    QTest::newRow("create-non-plugin") << false << sys_qualifiedLibraryName("tst_qpluginloaderlib");
    QTest::newRow("load-non-plugin") << true << sys_qualifiedLibraryName("tst_qpluginloaderlib");
}

void tst_QPluginLoader::preloadedPlugin()
{
#if !defined(QT_SHARED)
    QSKIP("This test requires Qt to create shared libraries.");
#endif
    // check that using QPluginLoader does not interfere with QLibrary
    QFETCH(QString, libname);
    QLibrary lib(libname);
    QVERIFY(lib.load());

    typedef int *(*pf_t)();
    pf_t pf = (pf_t)lib.resolve("pointerAddress");
    QVERIFY(pf);

    int *pluginVariable = pf();
    QVERIFY(pluginVariable);
    QCOMPARE(*pluginVariable, 0xc0ffee);

    {
        // load the plugin
        QPluginLoader loader(libname);
        QFETCH(bool, doLoad);
        if (doLoad && loader.load()) {
            // unload() returns false because QLibrary has it loaded
            QVERIFY(!loader.unload());
        }
    }

    QVERIFY(lib.isLoaded());

    // if the library was unloaded behind our backs, the following will crash:
    QCOMPARE(*pluginVariable, 0xc0ffee);
    QVERIFY(lib.unload());
}

void tst_QPluginLoader::staticPlugins()
{
    const QObjectList instances = QPluginLoader::staticInstances();
    QVERIFY(instances.size());

    bool found = false;
    for (QObject *obj : instances) {
        found = obj->metaObject()->className() == QLatin1String("StaticPlugin");
        if (found)
            break;
    }
    QVERIFY(found);

    const auto plugins = QPluginLoader::staticPlugins();
    QCOMPARE(plugins.size(), instances.size());

    // find the metadata
    QJsonObject metaData;
    for (const auto &p : plugins) {
        metaData = p.metaData();
        found = metaData.value("className").toString() == QLatin1String("StaticPlugin");
        if (found)
            break;
    }
    QVERIFY(found);

    // We don't store the patch release version anymore (since 5.13)
    QCOMPARE(metaData.value("version").toInt() / 0x100, QT_VERSION / 0x100);
    QCOMPARE(metaData.value("IID").toString(), "SomeIID");
    QCOMPARE(metaData.value("ExtraMetaData"), QJsonArray({ "StaticPlugin", "foo" }));
    QCOMPARE(metaData.value("URI").toString(), "qt.test.pluginloader.staticplugin");
}


QTEST_MAIN(tst_QPluginLoader)
#include "tst_qpluginloader.moc"
