// Copyright (C) 2016 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

#include "qqmlfile.h"

#include <QtCore/qurl.h>
#include <QtCore/qobject.h>
#include <QtCore/qmetaobject.h>
#include <QtCore/qfile.h>
#include <private/qqmlengine_p.h>
#include <private/qqmlglobal_p.h>

QT_BEGIN_NAMESPACE

/*!
    \class QQmlFile
    \inmodule QtQml
    \since 5.0
    \brief The QQmlFile class provides static utility methods to categorize URLs.

    QQmlFile provides some static utility methods to categorize URLs
    and file names the way \l{QQmlEngine} does when loading content from them.
*/

/*!
    \internal

    \enum QQmlFile::Status
    \value Null
    \value Ready
    \value Error
    \value Loading
 */

static char qrc_string[] = "qrc";
static char file_string[] = "file";

#if defined(Q_OS_ANDROID)
static char assets_string[] = "assets";
static char content_string[] = "content";
static char authority_externalstorage[] = "com.android.externalstorage.documents";
static char authority_downloads_documents[] = "com.android.providers.downloads.documents";
static char authority_media_documents[] = "com.android.providers.media.documents";
#endif

class QQmlFilePrivate;

#if QT_CONFIG(qml_network)
class QQmlFileNetworkReply : public QObject
{
Q_OBJECT
public:
    QQmlFileNetworkReply(QQmlEngine *, QQmlFilePrivate *, const QUrl &);
    ~QQmlFileNetworkReply();

signals:
    void finished();
    void downloadProgress(qint64, qint64);

public slots:
    void networkFinished();
    void networkDownloadProgress(qint64, qint64);

public:
    static int finishedIndex;
    static int downloadProgressIndex;
    static int networkFinishedIndex;
    static int networkDownloadProgressIndex;
    static int replyFinishedIndex;
    static int replyDownloadProgressIndex;

private:
    QQmlEngine *m_engine;
    QQmlFilePrivate *m_p;

    QNetworkReply *m_reply;
};
#endif

class QQmlFilePrivate
{
public:
    QQmlFilePrivate();

    mutable QUrl url;
    mutable QString urlString;

    QByteArray data;

    enum Error {
        None, NotFound, CaseMismatch, Network
    };

    Error error;
    QString errorString;
#if QT_CONFIG(qml_network)
    QQmlFileNetworkReply *reply;
#endif
};

#if QT_CONFIG(qml_network)
int QQmlFileNetworkReply::finishedIndex = -1;
int QQmlFileNetworkReply::downloadProgressIndex = -1;
int QQmlFileNetworkReply::networkFinishedIndex = -1;
int QQmlFileNetworkReply::networkDownloadProgressIndex = -1;
int QQmlFileNetworkReply::replyFinishedIndex = -1;
int QQmlFileNetworkReply::replyDownloadProgressIndex = -1;

QQmlFileNetworkReply::QQmlFileNetworkReply(QQmlEngine *e, QQmlFilePrivate *p, const QUrl &url)
: m_engine(e), m_p(p), m_reply(nullptr)
{
    if (finishedIndex == -1) {
        finishedIndex = QMetaMethod::fromSignal(&QQmlFileNetworkReply::finished).methodIndex();
        downloadProgressIndex = QMetaMethod::fromSignal(&QQmlFileNetworkReply::downloadProgress).methodIndex();
        const QMetaObject *smo = &staticMetaObject;
        networkFinishedIndex = smo->indexOfMethod("networkFinished()");
        networkDownloadProgressIndex = smo->indexOfMethod("networkDownloadProgress(qint64,qint64)");

        replyFinishedIndex = QMetaMethod::fromSignal(&QNetworkReply::finished).methodIndex();
        replyDownloadProgressIndex = QMetaMethod::fromSignal(&QNetworkReply::downloadProgress).methodIndex();
    }
    Q_ASSERT(finishedIndex != -1 && downloadProgressIndex != -1 &&
             networkFinishedIndex != -1 && networkDownloadProgressIndex != -1 &&
             replyFinishedIndex != -1 && replyDownloadProgressIndex != -1);

    QNetworkRequest req(url);
    req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, true);

    m_reply = m_engine->networkAccessManager()->get(req);
    QMetaObject::connect(m_reply, replyFinishedIndex, this, networkFinishedIndex);
    QMetaObject::connect(m_reply, replyDownloadProgressIndex, this, networkDownloadProgressIndex);
}

QQmlFileNetworkReply::~QQmlFileNetworkReply()
{
    if (m_reply) {
        m_reply->disconnect();
        m_reply->deleteLater();
    }
}

void QQmlFileNetworkReply::networkFinished()
{
    if (m_reply->error()) {
        m_p->errorString = m_reply->errorString();
        m_p->error = QQmlFilePrivate::Network;
    } else {
        m_p->data = m_reply->readAll();
    }

    m_reply->deleteLater();
    m_reply = nullptr;

    m_p->reply = nullptr;
    emit finished();
    delete this;
}

void QQmlFileNetworkReply::networkDownloadProgress(qint64 a, qint64 b)
{
    emit downloadProgress(a, b);
}
#endif // qml_network

QQmlFilePrivate::QQmlFilePrivate()
: error(None)
#if QT_CONFIG(qml_network)
, reply(nullptr)
#endif
{
}

/*!
    \internal
 */
QQmlFile::QQmlFile()
: d(new QQmlFilePrivate)
{
}

/*!
    \internal
    Constructs a QQmlFile for content at \a url, using \a engine to retrieve it.
 */
QQmlFile::QQmlFile(QQmlEngine *engine, const QUrl &url)
: d(new QQmlFilePrivate)
{
    load(engine, url);
}

/*!
    \internal
    Constructs a QQmlFile for content at \a url, using \a engine to retrieve it.
 */
QQmlFile::QQmlFile(QQmlEngine *engine, const QString &url)
    : QQmlFile(engine, QUrl(url))
{
}

/*!
    \internal
 */
QQmlFile::~QQmlFile()
{
#if QT_CONFIG(qml_network)
    delete d->reply;
#endif
    delete d;
    d = nullptr;
}

/*!
    \internal
 */
bool QQmlFile::isNull() const
{
    return status() == Null;
}

/*!
    \internal
 */
bool QQmlFile::isReady() const
{
    return status() == Ready;
}

/*!
    \internal
 */
bool QQmlFile::isError() const
{
    return status() == Error;
}

/*!
    \internal
 */
bool QQmlFile::isLoading() const
{
    return status() == Loading;
}

/*!
    \internal
 */
QUrl QQmlFile::url() const
{
    if (!d->urlString.isEmpty()) {
        d->url = QUrl(d->urlString);
        d->urlString = QString();
    }
    return d->url;
}

/*!
    \internal
 */
QQmlFile::Status QQmlFile::status() const
{
    if (d->url.isEmpty() && d->urlString.isEmpty())
        return Null;
#if QT_CONFIG(qml_network)
    else if (d->reply)
        return Loading;
#endif
    else if (d->error != QQmlFilePrivate::None)
        return Error;
    else
        return Ready;
}

/*!
    \internal
 */
QString QQmlFile::error() const
{
    switch (d->error) {
    default:
    case QQmlFilePrivate::None:
        return QString();
    case QQmlFilePrivate::NotFound:
        return QLatin1String("File not found");
    case QQmlFilePrivate::CaseMismatch:
        return QLatin1String("File name case mismatch");
    }
}

/*!
    \internal
 */
qint64 QQmlFile::size() const
{
    return d->data.size();
}

/*!
    \internal
 */
const char *QQmlFile::data() const
{
    return d->data.constData();
}

/*!
    \internal
 */
QByteArray QQmlFile::dataByteArray() const
{
    return d->data;
}

/*!
    \internal
    Loads content at \a url using \a engine.
 */
void QQmlFile::load(QQmlEngine *engine, const QUrl &url)
{
    Q_ASSERT(engine);

    clear();
    d->url = url;

    if (isLocalFile(url)) {
        QString lf = urlToLocalFileOrQrc(url);

        if (!QQml_isFileCaseCorrect(lf)) {
            d->error = QQmlFilePrivate::CaseMismatch;
            return;
        }

        QFile file(lf);
        if (file.open(QFile::ReadOnly)) {
            d->data = file.readAll();
        } else {
            d->error = QQmlFilePrivate::NotFound;
        }
    } else {
#if QT_CONFIG(qml_network)
        d->reply = new QQmlFileNetworkReply(engine, d, url);
#else
        d->error = QQmlFilePrivate::NotFound;
#endif
    }
}

/*!
    \internal
    Loads content at \a url using \a engine.
 */
void QQmlFile::load(QQmlEngine *engine, const QString &url)
{
    Q_ASSERT(engine);

    clear();

    d->urlString = url;

    if (isLocalFile(url)) {
        QString lf = urlToLocalFileOrQrc(url);

        if (!QQml_isFileCaseCorrect(lf)) {
            d->error = QQmlFilePrivate::CaseMismatch;
            return;
        }

        QFile file(lf);
        if (file.open(QFile::ReadOnly)) {
            d->data = file.readAll();
        } else {
            d->error = QQmlFilePrivate::NotFound;
        }
    } else {
#if QT_CONFIG(qml_network)
        QUrl qurl(url);
        d->url = qurl;
        d->urlString = QString();
        d->reply = new QQmlFileNetworkReply(engine, d, qurl);
#else
        d->error = QQmlFilePrivate::NotFound;
#endif
    }
}

/*!
    \internal
 */
void QQmlFile::clear()
{
    d->url = QUrl();
    d->urlString = QString();
    d->data = QByteArray();
    d->error = QQmlFilePrivate::None;
}

/*!
    \internal
    Redirects to the other clear() overload, ignoring \a object.
 */
void QQmlFile::clear(QObject *object)
{
    Q_UNUSED(object);
    clear();
}

#if QT_CONFIG(qml_network)

/*!
    \internal
    Connects \a method of \a object to the internal \c{finished} signal.
 */
bool QQmlFile::connectFinished(QObject *object, const char *method)
{
    if (!d || !d->reply) {
        qWarning("QQmlFile: connectFinished() called when not loading.");
        return false;
    }

    return QObject::connect(d->reply, SIGNAL(finished()), object, method);
}

/*!
    \internal
    Connects \a method of \a object to the internal \c{finished} signal.
 */
bool QQmlFile::connectFinished(QObject *object, int method)
{
    if (!d || !d->reply) {
        qWarning("QQmlFile: connectFinished() called when not loading.");
        return false;
    }

    return QMetaObject::connect(d->reply, QQmlFileNetworkReply::finishedIndex,
                                object, method);
}

/*!
    \internal
    Connects \a method of \a object to the internal \c{downloadProgress} signal.
 */
bool QQmlFile::connectDownloadProgress(QObject *object, const char *method)
{
    if (!d || !d->reply) {
        qWarning("QQmlFile: connectDownloadProgress() called when not loading.");
        return false;
    }

    return QObject::connect(d->reply, SIGNAL(downloadProgress(qint64,qint64)),
                            object, method);
}

/*!
    \internal
    Connects \a method of \a object to the internal \c{downloadProgress} signal.
 */
bool QQmlFile::connectDownloadProgress(QObject *object, int method)
{
    if (!d || !d->reply) {
        qWarning("QQmlFile: connectDownloadProgress() called when not loading.");
        return false;
    }

    return QMetaObject::connect(d->reply, QQmlFileNetworkReply::downloadProgressIndex,
                                object, method);
}
#endif

/*!
    \internal

    Returns \c true if QQmlFile will open \a url synchronously.
    Otherwise returns \c false. Synchronous urls have a \c{qrc:} or \c{file:}
    scheme.

    \note On Android, urls with \c{assets:} or \c{content:} scheme are also
          considered synchronous.
*/
bool QQmlFile::isSynchronous(const QUrl &url)
{
    QString scheme = url.scheme();

    if ((scheme.size() == 4 && 0 == scheme.compare(QLatin1String(file_string), Qt::CaseInsensitive)) ||
        (scheme.size() == 3 && 0 == scheme.compare(QLatin1String(qrc_string), Qt::CaseInsensitive))) {
        return true;

#if defined(Q_OS_ANDROID)
    } else if (scheme.length() == 6 && 0 == scheme.compare(QLatin1String(assets_string), Qt::CaseInsensitive)) {
        return true;
    } else if (scheme.length() == 7 && 0 == scheme.compare(QLatin1String(content_string), Qt::CaseInsensitive)) {
        return true;
#endif

    } else {
        return false;
    }
}

/*!
    \internal

    Returns \c true if QQmlFile will open \a url synchronously.
    Otherwise returns \c false. Synchronous urls have a \c{qrc:} or \c{file:}
    scheme.

    \note On Android, urls with \c{assets:} or \c{content:} scheme are also
          considered synchronous.
*/
bool QQmlFile::isSynchronous(const QString &url)
{
    if (url.size() < 5 /* qrc:/ */)
        return false;

    QChar f = url[0];

    if (f == QLatin1Char('f') || f == QLatin1Char('F')) {

        return url.size() >= 7 /* file:// */ &&
               url.startsWith(QLatin1String(file_string), Qt::CaseInsensitive) &&
               url[4] == QLatin1Char(':') && url[5] == QLatin1Char('/') && url[6] == QLatin1Char('/');

    } else if (f == QLatin1Char('q') || f == QLatin1Char('Q')) {

        return url.size() >= 5 /* qrc:/ */ &&
               url.startsWith(QLatin1String(qrc_string), Qt::CaseInsensitive) &&
               url[3] == QLatin1Char(':') && url[4] == QLatin1Char('/');

    }

#if defined(Q_OS_ANDROID)
    else if (f == QLatin1Char('a') || f == QLatin1Char('A')) {
        return url.length() >= 8 /* assets:/ */ &&
               url.startsWith(QLatin1String(assets_string), Qt::CaseInsensitive) &&
               url[6] == QLatin1Char(':') && url[7] == QLatin1Char('/');
    } else if (f == QLatin1Char('c') || f == QLatin1Char('C')) {
        return url.length() >= 9 /* content:/ */ &&
               url.startsWith(QLatin1String(content_string), Qt::CaseInsensitive) &&
               url[7] == QLatin1Char(':') && url[8] == QLatin1Char('/');
    }
#endif

    return false;
}

#if defined(Q_OS_ANDROID)
static bool hasLocalContentAuthority(const QUrl &url)
{
    const QString authority = url.authority();
    return authority.isEmpty()
            || authority == QLatin1String(authority_externalstorage)
            || authority == QLatin1String(authority_downloads_documents)
            || authority == QLatin1String(authority_media_documents);
}
#endif

/*!
    Returns \c true if \a url is a local file that can be opened with \l{QFile}.
    Otherwise returns \c false. Local file urls have either a \c{qrc:} or
    \c{file:} scheme.

    \note On Android, urls with \c{assets:} or \c{content:} scheme are also
          considered local files.
*/
bool QQmlFile::isLocalFile(const QUrl &url)
{
    QString scheme = url.scheme();

    // file: URLs with two slashes following the scheme can be interpreted as local files
    // where the slashes are part of the path. Therefore, disregard the authority.
    // See QUrl::toLocalFile().
    if (scheme.size() == 4 && scheme.startsWith(QLatin1String(file_string), Qt::CaseInsensitive))
        return true;

    if (scheme.size() == 3 && scheme.startsWith(QLatin1String(qrc_string), Qt::CaseInsensitive))
        return url.authority().isEmpty();

#if defined(Q_OS_ANDROID)
    if (scheme.length() == 6
         && scheme.startsWith(QLatin1String(assets_string), Qt::CaseInsensitive))
        return url.authority().isEmpty();
    if (scheme.length() == 7
         && scheme.startsWith(QLatin1String(content_string), Qt::CaseInsensitive))
        return hasLocalContentAuthority(url);
#endif

    return false;
}

static bool hasScheme(const QString &url, const char *scheme, qsizetype schemeLength)
{
    const qsizetype urlLength = url.size();

    if (urlLength < schemeLength + 1)
        return false;

    if (!url.startsWith(QLatin1String(scheme, scheme + schemeLength), Qt::CaseInsensitive))
        return false;

    if (url[schemeLength] != QLatin1Char(':'))
        return false;

    return true;
}

static qsizetype authorityOffset(const QString &url, qsizetype schemeLength)
{
    const qsizetype urlLength = url.size();

    if (urlLength < schemeLength + 3)
        return -1;

    const QLatin1Char slash('/');
    if (url[schemeLength + 1] == slash && url[schemeLength + 2] == slash) {
        // Exactly two slashes denote an authority.
        if (urlLength < schemeLength + 4 || url[schemeLength + 3] != slash)
            return schemeLength + 3;
    }

    return -1;
}

#if defined(Q_OS_ANDROID)
static bool hasLocalContentAuthority(const QString &url, qsizetype schemeLength)
{
    const qsizetype offset = authorityOffset(url, schemeLength);
    if (offset == -1)
        return true; // no authority is a local authority.

    const QString authorityAndPath = url.sliced(offset);
    return authorityAndPath.startsWith(QLatin1String(authority_externalstorage))
         || authorityAndPath.startsWith(QLatin1String(authority_downloads_documents))
         || authorityAndPath.startsWith(QLatin1String(authority_media_documents));
}

#endif

/*!
    Returns \c true if \a url is a local file that can be opened with \l{QFile}.
    Otherwise returns \c false. Local file urls have either a \c{qrc:} or
    \c{file:} scheme.

    \note On Android, urls with \c{assets:} or \c{content:} scheme are also considered
          local files.
*/
bool QQmlFile::isLocalFile(const QString &url)
{
    if (url.size() < 4 /* qrc: */)
        return false;

    switch (url[0].toLatin1()) {
    case 'f':
    case 'F': {
        // file: URLs with two slashes following the scheme can be interpreted as local files
        // where the slashes are part of the path. Therefore, disregard the authority.
        // See QUrl::toLocalFile().
        const qsizetype fileLength = strlen(file_string);
        return url.startsWith(QLatin1String(file_string, file_string + fileLength),
                              Qt::CaseInsensitive)
                && url.size() > fileLength
                && url[fileLength] == QLatin1Char(':');
    }
    case 'q':
    case 'Q':
        return hasScheme(url, qrc_string, strlen(qrc_string))
            && authorityOffset(url, strlen(qrc_string)) == -1;
#if defined(Q_OS_ANDROID)
    case 'a':
    case 'A':
        return hasScheme(url, assets_string, strlen(assets_string))
            && authorityOffset(url, strlen(assets_string)) == -1;
    case 'c':
    case 'C':
        return hasScheme(url, content_string, strlen(content_string))
            && hasLocalContentAuthority(url, strlen(content_string));
#endif
    default:
        break;
    }

    return false;
}

/*!
    If \a url is a local file returns a path suitable for passing to \l{QFile}.
    Otherwise returns an empty string.

    \sa isLocalFile
*/
QString QQmlFile::urlToLocalFileOrQrc(const QUrl& url)
{
    if (url.scheme().compare(QLatin1String("qrc"), Qt::CaseInsensitive) == 0) {
        if (url.authority().isEmpty())
            return QLatin1Char(':') + url.path();
        return QString();
    }

#if defined(Q_OS_ANDROID)
    if (url.scheme().compare(QLatin1String("assets"), Qt::CaseInsensitive) == 0)
        return url.authority().isEmpty() ? url.toString() : QString();
    if (url.scheme().compare(QLatin1String("content"), Qt::CaseInsensitive) == 0) {
        if (hasLocalContentAuthority(url))
            return url.toString();
        return QString();
    }
#endif
    return url.toLocalFile();
}

static QString toLocalFile(const QString &url)
{
    const QUrl file(url);
    if (!file.isLocalFile())
        return QString();

    // QUrl::toLocalFile() interprets two slashes as part of the path.
    // Therefore windows hostnames like "//servername/path/to/file.txt" are preserved.

    return file.toLocalFile();
}

static bool isDoubleSlashed(const QString &url, qsizetype offset)
{
    const qsizetype urlLength = url.size();
    if (urlLength < offset + 2)
        return false;

    const QLatin1Char slash('/');
    if (url[offset] != slash || url[offset + 1] != slash)
        return false;

    if (urlLength < offset + 3)
        return true;

    return url[offset + 2] != slash;
}

/*!
    If \a url is a local file returns a path suitable for passing to \l{QFile}.
    Otherwise returns an empty string.

    \sa isLocalFile
*/
QString QQmlFile::urlToLocalFileOrQrc(const QString& url)
{
    if (url.startsWith(QLatin1String("qrc://"), Qt::CaseInsensitive)) {
        // Exactly two slashes are bad because that's a URL authority.
        // One slash is fine and >= 3 slashes are file.
        if (url.size() == 6 || url[6] != QLatin1Char('/')) {
            Q_ASSERT(isDoubleSlashed(url, strlen("qrc:")));
            return QString();
        }
        Q_ASSERT(!isDoubleSlashed(url, strlen("qrc:")));
        return QLatin1Char(':') + QStringView{url}.mid(6);
    }

    if (url.startsWith(QLatin1String("qrc:"), Qt::CaseInsensitive)) {
        Q_ASSERT(!isDoubleSlashed(url, strlen("qrc:")));
        if (url.size() > 4)
            return QLatin1Char(':') + QStringView{url}.mid(4);
        return QStringLiteral(":");
    }

#if defined(Q_OS_ANDROID)
    if (url.startsWith(QLatin1String("assets:"), Qt::CaseInsensitive))
        return isDoubleSlashed(url, strlen("assets:")) ? QString() : url;
    if (hasScheme(url, content_string, strlen(content_string)))
        return hasLocalContentAuthority(url, strlen(content_string)) ? url : QString();
#endif

    return toLocalFile(url);
}

QT_END_NAMESPACE

#include "qqmlfile.moc"
