// Copyright (C) 2018 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant

#include "qqmlpreviewfileengine.h"
#include "qqmlpreviewservice.h"

#include <QtCore/qlibraryinfo.h>
#include <QtCore/qthread.h>
#include <QtCore/qwaitcondition.h>

#include <cstring>

QT_BEGIN_NAMESPACE

static bool isRelative(const QString &path)
{
    if (path.isEmpty())
        return true;
    if (path.at(0) == '/')
        return false;
    if (path.at(0) == ':' && path.size() >= 2 && path.at(1) == '/')
        return false;
#ifdef Q_OS_WIN
    if (path.length() >= 2 && path.at(1) == ':')
        return false;
#endif
    return true;
}

static QString absolutePath(const QString &path)
{
    return QDir::cleanPath(isRelative(path) ? (QDir::currentPath() + '/' + path) : path);
}

bool isRootPath(const QString &path)
{
    return QFileSystemEntry::isRootPath(path);
}

class QQmlPreviewFileEngineIterator : public QAbstractFileEngineIterator
{
public:
    QQmlPreviewFileEngineIterator(const QString &path, QDirListing::IteratorFlags filters,
                                  const QStringList &filterNames, const QStringList &m_entries);
    ~QQmlPreviewFileEngineIterator();

    bool advance() override;
    QString currentFileName() const override;

private:
    const QStringList m_entries;
    int m_index;
};

QQmlPreviewFileEngineIterator::QQmlPreviewFileEngineIterator(const QString &path,
                                                             QDirListing::IteratorFlags filters,
                                                             const QStringList &filterNames,
                                                             const QStringList &entries)
    : QAbstractFileEngineIterator(path, filters, filterNames), m_entries(entries), m_index(0)
{
}

QQmlPreviewFileEngineIterator::~QQmlPreviewFileEngineIterator()
{
}

bool QQmlPreviewFileEngineIterator::advance()
{
    if (m_index >= m_entries.size())
        return false;

    ++m_index;
    return true;
}

QString QQmlPreviewFileEngineIterator::currentFileName() const
{
    if (m_index == 0 || m_index > m_entries.size())
        return QString();
    return m_entries.at(m_index - 1);
}

QQmlPreviewFileEngine::QQmlPreviewFileEngine(const QString &file, const QString &absolute,
                                             QQmlPreviewFileLoader *loader) :
    m_name(file), m_absolute(absolute), m_loader(loader)
{
    load();
}

void QQmlPreviewFileEngine::setFileName(const QString &file)
{
    m_name = file;
    m_absolute = absolutePath(file);
    m_fallback.reset();
    m_contents.close();
    m_contents.setData(QByteArray());
    m_entries.clear();
    load();
}

bool QQmlPreviewFileEngine::open(QIODevice::OpenMode flags,
                                 std::optional<QFile::Permissions> permissions)
{
    switch (m_result) {
    case QQmlPreviewFileLoader::File:
        return m_contents.open(flags);
    case QQmlPreviewFileLoader::Directory:
        return false;
    case QQmlPreviewFileLoader::Fallback:
        return m_fallback->open(flags, permissions);
    default:
        Q_UNREACHABLE_RETURN(false);
    }
}

bool QQmlPreviewFileEngine::close()
{
    switch (m_result) {
    case QQmlPreviewFileLoader::Fallback:
        return m_fallback->close();
    case QQmlPreviewFileLoader::File:
        m_contents.close();
        return true;
    case QQmlPreviewFileLoader::Directory:
        return false;
    default:
        Q_UNREACHABLE_RETURN(false);
    }
}

qint64 QQmlPreviewFileEngine::size() const
{
    return m_fallback ? m_fallback->size() : m_contents.size();
}

qint64 QQmlPreviewFileEngine::pos() const
{
    return m_fallback ? m_fallback->pos() : m_contents.pos();
}

bool QQmlPreviewFileEngine::seek(qint64 newPos)
{
    return m_fallback? m_fallback->seek(newPos) : m_contents.seek(newPos);
}

qint64 QQmlPreviewFileEngine::read(char *data, qint64 maxlen)
{
    return m_fallback ? m_fallback->read(data, maxlen) : m_contents.read(data, maxlen);
}

QAbstractFileEngine::FileFlags QQmlPreviewFileEngine::fileFlags(
        QAbstractFileEngine::FileFlags type) const
{
    if (m_fallback)
        return m_fallback->fileFlags(type);

    QAbstractFileEngine::FileFlags ret;

    if (type & PermsMask) {
        ret |= QAbstractFileEngine::FileFlags(
                    ReadOwnerPerm | ReadUserPerm | ReadGroupPerm | ReadOtherPerm);
    }

    if (type & TypesMask) {
        if (m_result == QQmlPreviewFileLoader::Directory)
            ret |= DirectoryType;
        else
            ret |= FileType;
    }

    if (type & FlagsMask) {
        ret |= ExistsFlag;
        if (isRootPath(m_name))
            ret |= RootFlag;
    }

    return ret;
}

QString QQmlPreviewFileEngine::fileName(QAbstractFileEngine::FileName file) const
{
    if (m_fallback)
        return m_fallback->fileName(file);

    if (file == BaseName) {
        int slashPos = m_name.lastIndexOf('/');
        if (slashPos == -1)
            return m_name;
        return m_name.mid(slashPos + 1);
    } else if (file == PathName || file == AbsolutePathName) {
        const QString path = (file == AbsolutePathName) ? m_absolute : m_name;
        const int slashPos = path.lastIndexOf('/');
        if (slashPos == -1)
            return QString();
        else if (slashPos == 0)
            return "/";
        return path.left(slashPos);
    } else if (file == CanonicalName || file == CanonicalPathName) {
        if (file == CanonicalPathName) {
            const int slashPos = m_absolute.lastIndexOf('/');
            if (slashPos != -1)
                return m_absolute.left(slashPos);
        }
        return m_absolute;
    }
    return m_name;
}

uint QQmlPreviewFileEngine::ownerId(QAbstractFileEngine::FileOwner owner) const
{
    return m_fallback ? m_fallback->ownerId(owner) : static_cast<uint>(-2);
}

QAbstractFileEngine::IteratorUniquePtr QQmlPreviewFileEngine::beginEntryList(
        const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames)
{
    return m_fallback ? m_fallback->beginEntryList(path, filters, filterNames)
                      : std::make_unique<QQmlPreviewFileEngineIterator>(
                              path, filters, filterNames, m_entries);
}

QAbstractFileEngine::IteratorUniquePtr QQmlPreviewFileEngine::endEntryList()
{
    return m_fallback ? m_fallback->endEntryList() : nullptr;
}

bool QQmlPreviewFileEngine::flush()
{
    return m_fallback ? m_fallback->flush() : true;
}

bool QQmlPreviewFileEngine::syncToDisk()
{
    return m_fallback ? m_fallback->syncToDisk() : false;
}

bool QQmlPreviewFileEngine::isSequential() const
{
    return m_fallback ? m_fallback->isSequential() : m_contents.isSequential();
}

bool QQmlPreviewFileEngine::remove()
{
    return m_fallback ? m_fallback->remove() : false;
}

bool QQmlPreviewFileEngine::copy(const QString &newName)
{
    return m_fallback ? m_fallback->copy(newName) : false;
}

bool QQmlPreviewFileEngine::rename(const QString &newName)
{
    return m_fallback ? m_fallback->rename(newName) : false;
}

bool QQmlPreviewFileEngine::renameOverwrite(const QString &newName)
{
    return m_fallback ? m_fallback->renameOverwrite(newName) : false;
}

bool QQmlPreviewFileEngine::link(const QString &newName)
{
    return m_fallback ? m_fallback->link(newName) : false;
}

bool QQmlPreviewFileEngine::mkdir(const QString &dirName, bool createParentDirectories,
                                  std::optional<QFile::Permissions> permissions) const
{
    if (m_fallback)
        return m_fallback->mkdir(dirName, createParentDirectories, permissions);

    if (isRelative(dirName))
        return false;

    // If we're creating a directory from an absolute path, it can be anywhere.
    // The result is unrelated to the currently running preview.
    return QAbstractFileEngine::create(QDir::rootPath())
            ->mkdir(dirName, createParentDirectories, permissions);
}

bool QQmlPreviewFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const
{
    if (m_fallback)
        m_fallback->rmdir(dirName, recurseParentDirectories);

    if (isRelative(dirName))
        return false;

    // If we're removing a directory at an absolute path, it can be anywhere.
    // The result is unrelated to the currently running preview.
    return QAbstractFileEngine::create(QDir::rootPath())
            ->rmdir(dirName, recurseParentDirectories);
}

bool QQmlPreviewFileEngine::setSize(qint64 size)
{
    switch (m_result) {
    case QQmlPreviewFileLoader::Fallback:
        return m_fallback->setSize(size);
    case QQmlPreviewFileLoader::File:
        if (size < 0 || size > std::numeric_limits<int>::max())
            return false;
        m_contents.buffer().resize(static_cast<int>(size));
        return true;
    case QQmlPreviewFileLoader::Directory:
        return false;
    default:
        Q_UNREACHABLE_RETURN(false);
    }
}

bool QQmlPreviewFileEngine::caseSensitive() const
{
    return m_fallback ? m_fallback->caseSensitive() : true;
}

bool QQmlPreviewFileEngine::isRelativePath() const
{
    return m_fallback ? m_fallback->isRelativePath() : isRelative(m_name);
}

bool QQmlPreviewFileEngine::setPermissions(uint perms)
{
    return m_fallback ? m_fallback->setPermissions(perms) : false;
}

QByteArray QQmlPreviewFileEngine::id() const
{
    return m_fallback ? m_fallback->id() : QByteArray();
}

QString QQmlPreviewFileEngine::owner(FileOwner owner) const
{
    return m_fallback ? m_fallback->owner(owner) : QString();
}

QDateTime QQmlPreviewFileEngine::fileTime(QFile::FileTime time) const
{
    // Files we replace are always newer than the ones we had before. This makes the QML engine
    // actually recompile them, rather than pick them from the cache.
    return m_fallback ? m_fallback->fileTime(time) : QDateTime::currentDateTime();
}

int QQmlPreviewFileEngine::handle() const
{
    return m_fallback ? m_fallback->handle() : -1;
}

qint64 QQmlPreviewFileEngine::readLine(char *data, qint64 maxlen)
{
    return m_fallback ? m_fallback->readLine(data, maxlen) : m_contents.readLine(data, maxlen);
}

qint64 QQmlPreviewFileEngine::write(const char *data, qint64 len)
{
    return m_fallback ? m_fallback->write(data, len) : m_contents.write(data, len);
}

bool QQmlPreviewFileEngine::extension(Extension extension, const ExtensionOption *option, ExtensionReturn *output)
{
    return m_fallback ? m_fallback->extension(extension, option, output) : false;
}

bool QQmlPreviewFileEngine::supportsExtension(Extension extension) const
{
    return m_fallback ? m_fallback->supportsExtension(extension) : false;
}

void QQmlPreviewFileEngine::load() const
{
    // We can get here from different threads on different instances of QQmlPreviewFileEngine.
    // However, there is only one loader per QQmlPreviewFileEngineHandler and it is not thread-safe.
    // Its content mutex doesn't help us here because we explicitly wait on it in load(), which
    // causes it to be released. Therefore, lock the load mutex first.
    // This doesn't cause any deadlocks because the only thread that wakes the loader on the content
    // mutex never calls load(). It's the QML debug server thread that handles the debug protocol.
    QMutexLocker loadLocker(m_loader->loadMutex());

    m_result = m_loader->load(m_absolute);
    switch (m_result) {
    case QQmlPreviewFileLoader::File:
        m_contents.setData(m_loader->contents());
        break;
    case QQmlPreviewFileLoader::Directory:
        m_entries = m_loader->entries();
        break;
    case QQmlPreviewFileLoader::Fallback:
        m_fallback = QAbstractFileEngine::create(m_name);
        break;
    case QQmlPreviewFileLoader::Unknown:
        Q_UNREACHABLE();
        break;
    }
}

QQmlPreviewFileEngineHandler::QQmlPreviewFileEngineHandler(QQmlPreviewFileLoader *loader)
    : m_loader(loader)
{
}

std::unique_ptr<QAbstractFileEngine> QQmlPreviewFileEngineHandler::create(
        const QString &fileName) const
{
    using namespace Qt::StringLiterals;
    static QList<QLatin1StringView> prohibitedSuffixes {
        // Don't load compiled QML/JS over the network
        ".qmlc"_L1, ".jsc"_L1, ".mjsc"_L1,

        // Don't load plugins over the network
        ".dll"_L1, ".so"_L1, ".dylib"_L1
    };

    for (QLatin1StringView suffix : prohibitedSuffixes) {
        if (fileName.endsWith(suffix))
            return nullptr;
    }

    if (isRootPath(fileName))
        return nullptr;

    QString relative = fileName;
    while (relative.endsWith('/'))
        relative.chop(1);

    if (relative.isEmpty() || relative == ":")
        return nullptr;

    const QString absolute = relative.startsWith(':') ? relative : absolutePath(relative);

    if (m_loader->isBlacklisted(absolute))
        return {};

    return std::make_unique<QQmlPreviewFileEngine>(relative, absolute, m_loader.data());
}

QT_END_NAMESPACE
