diff options
7 files changed, 358 insertions, 51 deletions
diff --git a/src/qml/qml/qqmlfile.cpp b/src/qml/qml/qqmlfile.cpp index 4db8981975..dc2734d7d1 100644 --- a/src/qml/qml/qqmlfile.cpp +++ b/src/qml/qml/qqmlfile.cpp @@ -516,58 +516,91 @@ bool QQmlFile::isLocalFile(const QUrl &url) { QString scheme = url.scheme(); - if ((scheme.length() == 4 && 0 == scheme.compare(QLatin1String(file_string), Qt::CaseInsensitive)) || - (scheme.length() == 3 && 0 == scheme.compare(QLatin1String(qrc_string), Qt::CaseInsensitive))) { + // 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.length() == 4 && scheme.startsWith(QLatin1String(file_string), Qt::CaseInsensitive)) return true; + if (scheme.length() == 3 && scheme.startsWith(QLatin1String(qrc_string), Qt::CaseInsensitive)) + return url.authority().isEmpty(); + #if defined(Q_OS_ANDROID) - } else if (scheme.length() == 6 && 0 == scheme.compare(QLatin1String(assets_string), Qt::CaseInsensitive)) { - return true; + if ((scheme.length() == 6 + && scheme.startsWith(QLatin1String(assets_string), Qt::CaseInsensitive)) + || (scheme.length() == 7 + && scheme.startsWith(QLatin1String(content_string), Qt::CaseInsensitive))) { + return url.authority().isEmpty(); + } #endif - } else { + return false; +} + +static bool hasSchemeAndNoAuthority(const QString &url, const char *scheme, qsizetype schemeLength) +{ + const qsizetype urlLength = url.length(); + + if (urlLength < schemeLength + 1) + return false; + + if (!url.startsWith(QLatin1String(scheme, scheme + schemeLength), Qt::CaseInsensitive)) + return false; + + if (url[schemeLength] != QLatin1Char(':')) return false; + + if (urlLength < schemeLength + 3) + return true; + + const QLatin1Char slash('/'); + if (url[schemeLength + 1] == slash && url[schemeLength + 2] == slash) { + // Exactly two slashes denote an authority. We don't want that. + if (urlLength < schemeLength + 4 || url[schemeLength + 3] != slash) + return false; } + + return true; } /*! Returns true if \a url is a local file that can be opened with QFile. -Local file urls have either a qrc:/ or file:// scheme. +Local file urls have either a qrc: or file: scheme. -\note On Android, urls with assets:/ scheme are also considered local files. +\note On Android, urls with assets: or content: scheme are also considered local files. */ bool QQmlFile::isLocalFile(const QString &url) { - if (url.length() < 5 /* qrc:/ */) + if (url.length() < 4 /* qrc: */) return false; - QChar f = url[0]; - - if (f == QLatin1Char('f') || f == QLatin1Char('F')) { - - return url.length() >= 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.length() >= 5 /* qrc:/ */ && - url.startsWith(QLatin1String(qrc_string), Qt::CaseInsensitive) && - url[3] == QLatin1Char(':') && url[4] == QLatin1Char('/'); - + 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.length() > fileLength + && url[fileLength] == QLatin1Char(':'); } + case 'q': + case 'Q': + return hasSchemeAndNoAuthority(url, qrc_string, strlen(qrc_string)); #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('/'); - } + case 'a': + case 'A': + return hasSchemeAndNoAuthority(url, assets_string, strlen(assets_string)); + case 'c': + case 'C': + return hasSchemeAndNoAuthority(url, content_string, strlen(content_string)); #endif + default: + break; + } return false; } @@ -585,13 +618,10 @@ QString QQmlFile::urlToLocalFileOrQrc(const QUrl& url) } #if defined(Q_OS_ANDROID) - else if (url.scheme().compare(QLatin1String("assets"), Qt::CaseInsensitive) == 0) { - if (url.authority().isEmpty()) - return url.toString(); - return QString(); - } else if (url.scheme().compare(QLatin1String("content"), Qt::CaseInsensitive) == 0) { - return url.toString(); - } + 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) + return url.authority().isEmpty() ? url.toString() : QString(); #endif return url.toLocalFile(); @@ -603,11 +633,28 @@ static QString toLocalFile(const QString &url) if (!file.isLocalFile()) return QString(); - //XXX TODO: handle windows hostnames: "//servername/path/to/file.txt" + // 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.length(); + 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 QFile. Otherwise returns an empty string. @@ -615,23 +662,28 @@ empty string. QString QQmlFile::urlToLocalFileOrQrc(const QString& url) { if (url.startsWith(QLatin1String("qrc://"), Qt::CaseInsensitive)) { - if (url.length() > 6) - return QLatin1Char(':') + QStringView{url}.mid(6); - return QString(); + // Exactly two slashes are bad because that's a URL authority. + // One slash is fine and >= 3 slashes are file. + if (url.length() == 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.length() > 4) return QLatin1Char(':') + QStringView{url}.mid(4); - return QString(); + return QStringLiteral(":"); } #if defined(Q_OS_ANDROID) - else if (url.startsWith(QLatin1String("assets:"), Qt::CaseInsensitive)) { - return url; - } else if (url.startsWith(QLatin1String("content:"), Qt::CaseInsensitive)) { - return url; - } + if (url.startsWith(QLatin1String("assets:"), Qt::CaseInsensitive)) + return isDoubleSlashed(url, strlen("assets:")) ? QString() : url; + if (url.startsWith(QLatin1String("content:"), Qt::CaseInsensitive)) + return isDoubleSlashed(url, strlen("content:")) ? QString() : url; #endif return toLocalFile(url); diff --git a/tests/auto/qml/qqmlapplicationengine/CMakeLists.txt b/tests/auto/qml/qqmlapplicationengine/CMakeLists.txt index d9fbf6517d..caf1e2edaa 100644 --- a/tests/auto/qml/qqmlapplicationengine/CMakeLists.txt +++ b/tests/auto/qml/qqmlapplicationengine/CMakeLists.txt @@ -55,3 +55,4 @@ qt_internal_extend_target(tst_qqmlapplicationengine CONDITION NOT ANDROID AND NO QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/data\\\" ) add_subdirectory(testapp) +add_subdirectory(androidassets) diff --git a/tests/auto/qml/qqmlapplicationengine/androidassets/CMakeLists.txt b/tests/auto/qml/qqmlapplicationengine/androidassets/CMakeLists.txt new file mode 100644 index 0000000000..1c0d305311 --- /dev/null +++ b/tests/auto/qml/qqmlapplicationengine/androidassets/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_internal_add_test(tst_androidassets + SOURCES + tst_androidassets.cpp + PUBLIC_LIBRARIES + Qt::Gui + Qt::Qml + Qt::Quick +) + +# add qml/*.qml files as assets instead of resources + +file( + COPY qml/main.qml + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/android-build/assets/qml/") + +file( + COPY qml/pages/MainPage.qml + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/android-build/assets/qml/pages/") diff --git a/tests/auto/qml/qqmlapplicationengine/androidassets/qml/main.qml b/tests/auto/qml/qqmlapplicationengine/androidassets/qml/main.qml new file mode 100644 index 0000000000..6901572b00 --- /dev/null +++ b/tests/auto/qml/qqmlapplicationengine/androidassets/qml/main.qml @@ -0,0 +1,13 @@ +import QtQuick + +// relative import: has to work when loading from android assets by path or by URL +import "pages" + +Window { + width: 640 + height: 480 + visible: true + title: qsTr("Hello World") + + MainPage { } +} diff --git a/tests/auto/qml/qqmlapplicationengine/androidassets/qml/pages/MainPage.qml b/tests/auto/qml/qqmlapplicationengine/androidassets/qml/pages/MainPage.qml new file mode 100644 index 0000000000..c9549a928a --- /dev/null +++ b/tests/auto/qml/qqmlapplicationengine/androidassets/qml/pages/MainPage.qml @@ -0,0 +1,11 @@ +import QtQuick + +Rectangle { + anchors.fill: parent + color: "#ddd" + + Text { + anchors.centerIn: parent + text: "Qt 6" + } +} diff --git a/tests/auto/qml/qqmlapplicationengine/androidassets/tst_androidassets.cpp b/tests/auto/qml/qqmlapplicationengine/androidassets/tst_androidassets.cpp new file mode 100644 index 0000000000..1926cf4fab --- /dev/null +++ b/tests/auto/qml/qqmlapplicationengine/androidassets/tst_androidassets.cpp @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** Copyright (C) 2022 The Qt Company Ltd. +** 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 <QtQml/qqmlapplicationengine.h> +#include <QtTest/qsignalspy.h> +#include <QtTest/qtest.h> + +class tst_AndroidAssets : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void loadsFromAssetsPath(); + void loadsFromAssetsUrl(); + +private: + + static QString pathPrefix() + { +#ifdef Q_OS_ANDROID + return QStringLiteral("assets:"); +#else + // Even when not running on android we can check that the copying to build dir worked. + return QCoreApplication::applicationDirPath() + QStringLiteral("/android-build/assets"); +#endif + } + + static QString urlPrefix() { +#ifdef Q_OS_ANDROID + return pathPrefix(); +#else + return QStringLiteral("file:") + pathPrefix(); +#endif + } +}; + + +void tst_AndroidAssets::loadsFromAssetsPath() +{ + QQmlApplicationEngine engine; + QSignalSpy failureSpy(&engine, &QQmlApplicationEngine::objectCreationFailed); + + // load QML file from assets, by path: + engine.load(pathPrefix() + QStringLiteral("/qml/main.qml")); + QTRY_VERIFY(engine.rootObjects().length() == 1); + QVERIFY(failureSpy.isEmpty()); +} + +void tst_AndroidAssets::loadsFromAssetsUrl() +{ + QQmlApplicationEngine engine; + QSignalSpy failureSpy(&engine, &QQmlApplicationEngine::objectCreationFailed); + + // load QML file from assets, by URL: + engine.load(QUrl(urlPrefix() + QStringLiteral("/qml/main.qml"))); + QTRY_VERIFY(engine.rootObjects().length() == 1); + QVERIFY(failureSpy.isEmpty()); +} + +QTEST_MAIN(tst_AndroidAssets) + +#include "tst_androidassets.moc" diff --git a/tests/auto/qml/qqmlfile/tst_qqmlfile.cpp b/tests/auto/qml/qqmlfile/tst_qqmlfile.cpp index a1c8daddcf..77909d8576 100644 --- a/tests/auto/qml/qqmlfile/tst_qqmlfile.cpp +++ b/tests/auto/qml/qqmlfile/tst_qqmlfile.cpp @@ -38,19 +38,145 @@ public: tst_qqmlfile() {} private Q_SLOTS: + void isLocalFile_data(); + void isLocalFile(); + + void urlToLocalFileOrQrcOverloads_data(); void urlToLocalFileOrQrcOverloads(); + +private: + void urlData(); }; +void tst_qqmlfile::urlData() +{ + QTest::addColumn<QString>("urlString"); + QTest::addColumn<bool>("isLocal"); + QTest::addColumn<QString>("localPath"); + + const QString invalid; + const QString relative = QStringLiteral("foo/bar"); + const QString absolute = QStringLiteral("/foo/bar"); + + QTest::addRow("plain empty") << QStringLiteral("") << false << invalid; + QTest::addRow("plain no slash") << QStringLiteral("foo/bar") << false << invalid; + QTest::addRow("plain 1 slash") << QStringLiteral("/foo/bar") << false << invalid; + QTest::addRow("plain 2 slashes") << QStringLiteral("//foo/bar") << false << invalid; + QTest::addRow("plain 3 slashes") << QStringLiteral("///foo/bar") << false << invalid; + + QTest::addRow(": empty") << QStringLiteral(":") << false << invalid; + QTest::addRow(": no slash") << QStringLiteral(":foo/bar") << false << invalid; + QTest::addRow(": 1 slash") << QStringLiteral(":/foo/bar") << false << invalid; + QTest::addRow(": 2 slashes") << QStringLiteral("://foo/bar") << false << invalid; + QTest::addRow(": 3 slashes") << QStringLiteral(":///foo/bar") << false << invalid; + + QTest::addRow("C empty") << QStringLiteral("C:") << false << invalid; + QTest::addRow("C no slash") << QStringLiteral("C:foo/bar") << false << invalid; + QTest::addRow("C 1 slash") << QStringLiteral("C:/foo/bar") << false << invalid; + QTest::addRow("C 2 slashes") << QStringLiteral("C://foo/bar") << false << invalid; + QTest::addRow("C 3 slashes") << QStringLiteral("C:///foo/bar") << false << invalid; + + QTest::addRow("file empty") << QStringLiteral("file:") << true << QString(); + QTest::addRow("file no slash") << QStringLiteral("file:foo/bar") << true << relative; + QTest::addRow("file 1 slash") << QStringLiteral("file:/foo/bar") << true << absolute; + QTest::addRow("file 2 slashes") << QStringLiteral("file://foo/bar") << true << QStringLiteral("//foo/bar"); + QTest::addRow("file 3 slashes") << QStringLiteral("file:///foo/bar") << true << absolute; + + QTest::addRow("qrc empty") << QStringLiteral("qrc:") << true << QStringLiteral(":"); + QTest::addRow("qrc no slash") << QStringLiteral("qrc:foo/bar") << true << u':' + relative; + QTest::addRow("qrc 1 slash") << QStringLiteral("qrc:/foo/bar") << true << u':' + absolute; + QTest::addRow("qrc 2 slashes") << QStringLiteral("qrc://foo/bar") << false << invalid; + QTest::addRow("qrc 3 slashes") << QStringLiteral("qrc:///foo/bar") << true << u':' + absolute; + + QTest::addRow("file+stuff empty") << QStringLiteral("file+stuff:") << false << invalid; + QTest::addRow("file+stuff no slash") << QStringLiteral("file+stuff:foo/bar") << false << invalid; + QTest::addRow("file+stuff 1 slash") << QStringLiteral("file+stuff:/foo/bar") << false << invalid; + QTest::addRow("file+stuff 2 slashes") << QStringLiteral("file+stuff://foo/bar") << false << invalid; + QTest::addRow("file+stuff 3 slashes") << QStringLiteral("file+stuff:///foo/bar") << false << invalid; + + // "assets:" and "content:" URLs are only treated as local files on android. In contrast to + // "qrc:" and "file:" we're not trying to be clever about multiple slashes. Two slashes are + // prohibited as that says part of what we would recognize as path is actually a URL authority. + // Everything else is android's problem. + +#ifdef Q_OS_ANDROID + const bool hasAssetsAndContent = true; +#else + const bool hasAssetsAndContent = false; +#endif + + const QString assetsEmpty = hasAssetsAndContent ? QStringLiteral("assets:") : invalid; + const QString assetsRelative = hasAssetsAndContent ? (QStringLiteral("assets:") + relative) : invalid; + const QString assetsAbsolute = hasAssetsAndContent ? (QStringLiteral("assets:") + absolute) : invalid; + const QString assetsThreeSlashes = hasAssetsAndContent ? (QStringLiteral("assets://") + absolute) : invalid; + + QTest::addRow("assets empty") << QStringLiteral("assets:") << hasAssetsAndContent << assetsEmpty; + QTest::addRow("assets no slash") << QStringLiteral("assets:foo/bar") << hasAssetsAndContent << assetsRelative; + QTest::addRow("assets 1 slash") << QStringLiteral("assets:/foo/bar") << hasAssetsAndContent << assetsAbsolute; + QTest::addRow("assets 2 slashes") << QStringLiteral("assets://foo/bar") << false << invalid; + QTest::addRow("assets 3 slashes") << QStringLiteral("assets:///foo/bar") << hasAssetsAndContent << assetsThreeSlashes; + + const QString contentEmpty = hasAssetsAndContent ? QStringLiteral("content:") : invalid; + const QString contentRelative = hasAssetsAndContent ? (QStringLiteral("content:") + relative) : invalid; + const QString contentAbsolute = hasAssetsAndContent ? (QStringLiteral("content:") + absolute) : invalid; + const QString contentThreeSlashes = hasAssetsAndContent ? (QStringLiteral("content://") + absolute) : invalid; + + QTest::addRow("content empty") << QStringLiteral("content:") << hasAssetsAndContent << contentEmpty; + QTest::addRow("content no slash") << QStringLiteral("content:foo/bar") << hasAssetsAndContent << contentRelative; + QTest::addRow("content 1 slash") << QStringLiteral("content:/foo/bar") << hasAssetsAndContent << contentAbsolute; + QTest::addRow("content 2 slashes") << QStringLiteral("content://foo/bar") << false << invalid; + QTest::addRow("content 3 slashes") << QStringLiteral("content:///foo/bar") << hasAssetsAndContent << contentThreeSlashes; + + + // These are local files everywhere. Their paths are only meaningful on android, though. + // The inner slashes of the path do not influence the URL parsing. + + QTest::addRow("file:assets empty") << QStringLiteral("file:assets:") << true << QStringLiteral("assets:"); + QTest::addRow("file:assets no slash") << QStringLiteral("file:assets:foo/bar") << true << QStringLiteral("assets:foo/bar"); + QTest::addRow("file:assets 1 slash") << QStringLiteral("file:assets:/foo/bar") << true << QStringLiteral("assets:/foo/bar"); + QTest::addRow("file:assets 2 slashes") << QStringLiteral("file:assets://foo/bar") << true << QStringLiteral("assets://foo/bar"); + QTest::addRow("file:assets 3 slashes") << QStringLiteral("file:assets:///foo/bar") << true << QStringLiteral("assets:///foo/bar"); + + QTest::addRow("file:content empty") << QStringLiteral("file:content:") << true << QStringLiteral("content:"); + QTest::addRow("file:content no slash") << QStringLiteral("file:content:foo/bar") << true << QStringLiteral("content:foo/bar"); + QTest::addRow("file:content 1 slash") << QStringLiteral("file:content:/foo/bar") << true << QStringLiteral("content:/foo/bar"); + QTest::addRow("file:content 2 slashes") << QStringLiteral("file:content://foo/bar") << true << QStringLiteral("content://foo/bar"); + QTest::addRow("file:content 3 slashes") << QStringLiteral("file:content:///foo/bar") << true << QStringLiteral("content:///foo/bar"); +} + +void tst_qqmlfile::isLocalFile_data() +{ + urlData(); +} + +void tst_qqmlfile::isLocalFile() +{ + QFETCH(QString, urlString); + QFETCH(bool, isLocal); + + const QUrl url(urlString); + + QCOMPARE(QQmlFile::isLocalFile(urlString), isLocal); + QCOMPARE(QQmlFile::isLocalFile(url), isLocal); +} + +void tst_qqmlfile::urlToLocalFileOrQrcOverloads_data() +{ + urlData(); +} + void tst_qqmlfile::urlToLocalFileOrQrcOverloads() { - const QString urlString = QStringLiteral("qrc:///example.qml"); + QFETCH(QString, urlString); + QFETCH(QString, localPath); + const QUrl url(urlString); const QString pathForUrlString = QQmlFile::urlToLocalFileOrQrc(urlString); const QString pathForUrl = QQmlFile::urlToLocalFileOrQrc(url); - QCOMPARE(pathForUrlString, pathForUrl); - QCOMPARE(pathForUrlString, QStringLiteral(":/example.qml")); + QCOMPARE(pathForUrlString, localPath); + QCOMPARE(pathForUrl, localPath); } QTEST_GUILESS_MAIN(tst_qqmlfile) |