// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
// Qt-Security score:significant

#include "qqmljsresourcefilemapper_p.h"

#include <QFileInfo>
#include <QDir>
#include <QXmlStreamReader>

QT_BEGIN_NAMESPACE

QQmlJSResourceFileMapper::Filter QQmlJSResourceFileMapper::allQmlJSFilter() {
    return Filter {
        QString(),
        QStringList { QStringLiteral("qml"), QStringLiteral("js"), QStringLiteral("mjs") },
        Directory | Recurse
    };
}

QQmlJSResourceFileMapper::Filter QQmlJSResourceFileMapper::localFileFilter(const QString &file)
{
    return Filter {
        QDir::cleanPath(QFileInfo(file).absoluteFilePath()),
        QStringList(),
        File
    };
}

QQmlJSResourceFileMapper::Filter QQmlJSResourceFileMapper::resourceFileFilter(const QString &file)
{
    return Filter {
        file,
        QStringList(),
        Resource
    };
}

QQmlJSResourceFileMapper::Filter QQmlJSResourceFileMapper::resourceQmlDirectoryFilter(
        const QString &directory)
{
    return Filter {
        directory,
        QStringList { QStringLiteral("qml") },
        Directory | Resource
    };
}

QQmlJSResourceFileMapper::QQmlJSResourceFileMapper(const QStringList &resourceFiles)
{
    for (const QString &fileName: resourceFiles) {
        QFile f(fileName);
        if (!f.open(QIODevice::ReadOnly))
            continue;
        populateFromQrcFile(f);
    }
}

bool QQmlJSResourceFileMapper::isEmpty() const
{
    return qrcPathToFileSystemPath.isEmpty();
}

bool QQmlJSResourceFileMapper::isFile(QStringView resourcePath) const
{
    for (const auto &entry : qrcPathToFileSystemPath) {
        if (entry.resourcePath == resourcePath)
            return true;
    }
    return false;
}

static bool hasSuffix(const QString &qrcPath, const QStringList &suffixes)
{
    if (suffixes.isEmpty())
        return true;
    const QString suffix = QFileInfo(qrcPath).suffix();
    return suffixes.contains(suffix);
}

template<typename HandleMatch>
void doFilter(const QList<QQmlJSResourceFileMapper::Entry> &qrcPathToFileSystemPath,
              const QQmlJSResourceFileMapper::Filter &filter,
              const HandleMatch &handler)
{
    if (filter.flags & QQmlJSResourceFileMapper::Directory) {
        const QString terminatedDirectory = filter.path.endsWith(u'/')
                ? filter.path : (filter.path + u'/');

        for (auto it = qrcPathToFileSystemPath.constBegin(),
             end = qrcPathToFileSystemPath.constEnd(); it != end; ++it) {

            const QString candidate = (filter.flags & QQmlJSResourceFileMapper::Resource)
                    ? it->resourcePath
                    : it->filePath;

            if (!filter.path.isEmpty() && !candidate.startsWith(terminatedDirectory))
                continue;

            if (!hasSuffix(candidate, filter.suffixes))
                continue;

            if ((filter.flags & QQmlJSResourceFileMapper::Recurse)
                    // Crude. But shall we really allow slashes in QRC file names?
                    || !candidate.mid(terminatedDirectory.size()).contains(u'/')) {
                if (handler(*it))
                    return;
            }
        }
        return;
    }

    if (!hasSuffix(filter.path, filter.suffixes))
        return;

    for (auto it = qrcPathToFileSystemPath.constBegin(),
         end = qrcPathToFileSystemPath.constEnd(); it != end; ++it) {
        if (filter.flags & QQmlJSResourceFileMapper::Resource) {
            if (it->resourcePath == filter.path && handler(*it))
                return;
        } else if (it->filePath == filter.path && handler(*it)) {
            return;
        }
    }
}

QList<QQmlJSResourceFileMapper::Entry> QQmlJSResourceFileMapper::filter(
        const QQmlJSResourceFileMapper::Filter &filter) const
{
    QList<Entry> result;
    doFilter(qrcPathToFileSystemPath, filter, [&](const Entry &entry) {
        result.append(entry);
        return false;
    });
    return result;
}

QStringList QQmlJSResourceFileMapper::filePaths(
        const QQmlJSResourceFileMapper::Filter &filter) const
{
    QStringList result;
    doFilter(qrcPathToFileSystemPath, filter, [&](const Entry &entry) {
        result.append(entry.filePath);
        return false;
    });
    return result;
}

QStringList QQmlJSResourceFileMapper::resourcePaths(
        const QQmlJSResourceFileMapper::Filter &filter) const
{
    QStringList result;
    doFilter(qrcPathToFileSystemPath, filter, [&](const Entry &entry) {
        result.append(entry.resourcePath);
        return false;
    });
    return result;
}

QQmlJSResourceFileMapper::Entry QQmlJSResourceFileMapper::entry(
        const QQmlJSResourceFileMapper::Filter &filter) const
{
    Entry result;
    doFilter(qrcPathToFileSystemPath, filter, [&](const Entry &entry) {
        result = entry;
        return true;
    });
    return result;
}

void QQmlJSResourceFileMapper::populateFromQrcFile(QFile &file)
{
    enum State {
        InitialState,
        InRCC,
        InResource,
        InFile
    };
    State state = InitialState;

    QDir qrcDir = QFileInfo(file).absoluteDir();

    QString prefix;
    QString currentFileName;
    QXmlStreamAttributes currentFileAttributes;

    QXmlStreamReader reader(&file);
    while (!reader.atEnd()) {
        switch (reader.readNext()) {
        case QXmlStreamReader::StartElement:
            if (reader.name() == QStringLiteral("RCC")) {
                if (state != InitialState)
                    return;
                state = InRCC;
                continue;
            } else if (reader.name() == QStringLiteral("qresource")) {
                if (state != InRCC)
                    return;
                state = InResource;
                QXmlStreamAttributes attributes = reader.attributes();
                if (attributes.hasAttribute(QStringLiteral("prefix")))
                    prefix = attributes.value(QStringLiteral("prefix")).toString();
                if (!prefix.startsWith(QLatin1Char('/')))
                    prefix.prepend(QLatin1Char('/'));
                if (!prefix.endsWith(QLatin1Char('/')))
                    prefix.append(QLatin1Char('/'));
                continue;
            } else if (reader.name() == QStringLiteral("file")) {
                if (state != InResource)
                    return;
                state = InFile;
                currentFileAttributes = reader.attributes();
                continue;
            }
            return;

        case QXmlStreamReader::EndElement:
            if (reader.name() == QStringLiteral("file")) {
                if (state != InFile)
                    return;
                state = InResource;
                continue;
            } else if (reader.name() == QStringLiteral("qresource")) {
                if (state != InResource)
                    return;
                state = InRCC;
                continue;
            } else if (reader.name() == QStringLiteral("RCC")) {
                if (state != InRCC)
                    return;
                state = InitialState;
                continue;
            }
            return;

        case QXmlStreamReader::Characters: {
            if (reader.isWhitespace())
                break;
            if (state != InFile)
                return;
            currentFileName = reader.text().toString();
            if (currentFileName.isEmpty())
                continue;

            const QString fsPath = QDir::cleanPath(qrcDir.absoluteFilePath(currentFileName));

            if (currentFileAttributes.hasAttribute(QStringLiteral("alias")))
                currentFileName = currentFileAttributes.value(QStringLiteral("alias")).toString();

            currentFileName = QDir::cleanPath(currentFileName);
            while (currentFileName.startsWith(QLatin1String("../")))
                currentFileName.remove(0, 3);

            const QString qrcPath = prefix + currentFileName;
            if (QFile::exists(fsPath))
                qrcPathToFileSystemPath.append({qrcPath, fsPath});
            continue;
        }

        default: break;
    }
    }
}

QT_END_NAMESPACE
