1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmlfile.h"
5
6#include <QtCore/qurl.h>
7#include <QtCore/qobject.h>
8#include <QtCore/qmetaobject.h>
9#include <QtCore/qfile.h>
10#include <private/qqmlengine_p.h>
11#include <private/qqmlglobal_p.h>
12
13/*!
14\class QQmlFile
15\brief The QQmlFile class gives access to local and remote files.
16
17\internal
18
19Supports file:// and qrc:/ uris and whatever QNetworkAccessManager supports.
20*/
21
22#define QQMLFILE_MAX_REDIRECT_RECURSION 16
23
24QT_BEGIN_NAMESPACE
25
26static char qrc_string[] = "qrc";
27static char file_string[] = "file";
28
29#if defined(Q_OS_ANDROID)
30static char assets_string[] = "assets";
31static char content_string[] = "content";
32static char authority_externalstorage[] = "com.android.externalstorage.documents";
33static char authority_downloads_documents[] = "com.android.providers.downloads.documents";
34static char authority_media_documents[] = "com.android.providers.media.documents";
35#endif
36
37class QQmlFilePrivate;
38
39#if QT_CONFIG(qml_network)
40class QQmlFileNetworkReply : public QObject
41{
42Q_OBJECT
43public:
44 QQmlFileNetworkReply(QQmlEngine *, QQmlFilePrivate *, const QUrl &);
45 ~QQmlFileNetworkReply();
46
47signals:
48 void finished();
49 void downloadProgress(qint64, qint64);
50
51public slots:
52 void networkFinished();
53 void networkDownloadProgress(qint64, qint64);
54
55public:
56 static int finishedIndex;
57 static int downloadProgressIndex;
58 static int networkFinishedIndex;
59 static int networkDownloadProgressIndex;
60 static int replyFinishedIndex;
61 static int replyDownloadProgressIndex;
62
63private:
64 QQmlEngine *m_engine;
65 QQmlFilePrivate *m_p;
66
67 int m_redirectCount;
68 QNetworkReply *m_reply;
69};
70#endif
71
72class QQmlFilePrivate
73{
74public:
75 QQmlFilePrivate();
76
77 mutable QUrl url;
78 mutable QString urlString;
79
80 QByteArray data;
81
82 enum Error {
83 None, NotFound, CaseMismatch, Network
84 };
85
86 Error error;
87 QString errorString;
88#if QT_CONFIG(qml_network)
89 QQmlFileNetworkReply *reply;
90#endif
91};
92
93#if QT_CONFIG(qml_network)
94int QQmlFileNetworkReply::finishedIndex = -1;
95int QQmlFileNetworkReply::downloadProgressIndex = -1;
96int QQmlFileNetworkReply::networkFinishedIndex = -1;
97int QQmlFileNetworkReply::networkDownloadProgressIndex = -1;
98int QQmlFileNetworkReply::replyFinishedIndex = -1;
99int QQmlFileNetworkReply::replyDownloadProgressIndex = -1;
100
101QQmlFileNetworkReply::QQmlFileNetworkReply(QQmlEngine *e, QQmlFilePrivate *p, const QUrl &url)
102: m_engine(e), m_p(p), m_redirectCount(0), m_reply(nullptr)
103{
104 if (finishedIndex == -1) {
105 finishedIndex = QMetaMethod::fromSignal(signal: &QQmlFileNetworkReply::finished).methodIndex();
106 downloadProgressIndex = QMetaMethod::fromSignal(signal: &QQmlFileNetworkReply::downloadProgress).methodIndex();
107 const QMetaObject *smo = &staticMetaObject;
108 networkFinishedIndex = smo->indexOfMethod(method: "networkFinished()");
109 networkDownloadProgressIndex = smo->indexOfMethod(method: "networkDownloadProgress(qint64,qint64)");
110
111 replyFinishedIndex = QMetaMethod::fromSignal(signal: &QNetworkReply::finished).methodIndex();
112 replyDownloadProgressIndex = QMetaMethod::fromSignal(signal: &QNetworkReply::downloadProgress).methodIndex();
113 }
114 Q_ASSERT(finishedIndex != -1 && downloadProgressIndex != -1 &&
115 networkFinishedIndex != -1 && networkDownloadProgressIndex != -1 &&
116 replyFinishedIndex != -1 && replyDownloadProgressIndex != -1);
117
118 QNetworkRequest req(url);
119 req.setAttribute(code: QNetworkRequest::HttpPipeliningAllowedAttribute, value: true);
120
121 m_reply = m_engine->networkAccessManager()->get(request: req);
122 QMetaObject::connect(sender: m_reply, signal_index: replyFinishedIndex, receiver: this, method_index: networkFinishedIndex);
123 QMetaObject::connect(sender: m_reply, signal_index: replyDownloadProgressIndex, receiver: this, method_index: networkDownloadProgressIndex);
124}
125
126QQmlFileNetworkReply::~QQmlFileNetworkReply()
127{
128 if (m_reply) {
129 m_reply->disconnect();
130 m_reply->deleteLater();
131 }
132}
133
134void QQmlFileNetworkReply::networkFinished()
135{
136 ++m_redirectCount;
137 if (m_redirectCount < QQMLFILE_MAX_REDIRECT_RECURSION) {
138 QVariant redirect = m_reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute);
139 if (redirect.isValid()) {
140 QUrl url = m_reply->url().resolved(relative: redirect.toUrl());
141
142 QNetworkRequest req(url);
143 req.setAttribute(code: QNetworkRequest::HttpPipeliningAllowedAttribute, value: true);
144
145 m_reply->deleteLater();
146 m_reply = m_engine->networkAccessManager()->get(request: req);
147
148 QMetaObject::connect(sender: m_reply, signal_index: replyFinishedIndex,
149 receiver: this, method_index: networkFinishedIndex);
150 QMetaObject::connect(sender: m_reply, signal_index: replyDownloadProgressIndex,
151 receiver: this, method_index: networkDownloadProgressIndex);
152
153 return;
154 }
155 }
156
157 if (m_reply->error()) {
158 m_p->errorString = m_reply->errorString();
159 m_p->error = QQmlFilePrivate::Network;
160 } else {
161 m_p->data = m_reply->readAll();
162 }
163
164 m_reply->deleteLater();
165 m_reply = nullptr;
166
167 m_p->reply = nullptr;
168 emit finished();
169 delete this;
170}
171
172void QQmlFileNetworkReply::networkDownloadProgress(qint64 a, qint64 b)
173{
174 emit downloadProgress(a, b);
175}
176#endif // qml_network
177
178QQmlFilePrivate::QQmlFilePrivate()
179: error(None)
180#if QT_CONFIG(qml_network)
181, reply(nullptr)
182#endif
183{
184}
185
186QQmlFile::QQmlFile()
187: d(new QQmlFilePrivate)
188{
189}
190
191QQmlFile::QQmlFile(QQmlEngine *e, const QUrl &url)
192: d(new QQmlFilePrivate)
193{
194 load(e, url);
195}
196
197QQmlFile::QQmlFile(QQmlEngine *e, const QString &url)
198 : QQmlFile(e, QUrl(url))
199{
200}
201
202QQmlFile::~QQmlFile()
203{
204#if QT_CONFIG(qml_network)
205 delete d->reply;
206#endif
207 delete d;
208 d = nullptr;
209}
210
211bool QQmlFile::isNull() const
212{
213 return status() == Null;
214}
215
216bool QQmlFile::isReady() const
217{
218 return status() == Ready;
219}
220
221bool QQmlFile::isError() const
222{
223 return status() == Error;
224}
225
226bool QQmlFile::isLoading() const
227{
228 return status() == Loading;
229}
230
231QUrl QQmlFile::url() const
232{
233 if (!d->urlString.isEmpty()) {
234 d->url = QUrl(d->urlString);
235 d->urlString = QString();
236 }
237 return d->url;
238}
239
240QQmlFile::Status QQmlFile::status() const
241{
242 if (d->url.isEmpty() && d->urlString.isEmpty())
243 return Null;
244#if QT_CONFIG(qml_network)
245 else if (d->reply)
246 return Loading;
247#endif
248 else if (d->error != QQmlFilePrivate::None)
249 return Error;
250 else
251 return Ready;
252}
253
254QString QQmlFile::error() const
255{
256 switch (d->error) {
257 default:
258 case QQmlFilePrivate::None:
259 return QString();
260 case QQmlFilePrivate::NotFound:
261 return QLatin1String("File not found");
262 case QQmlFilePrivate::CaseMismatch:
263 return QLatin1String("File name case mismatch");
264 }
265}
266
267qint64 QQmlFile::size() const
268{
269 return d->data.size();
270}
271
272const char *QQmlFile::data() const
273{
274 return d->data.constData();
275}
276
277QByteArray QQmlFile::dataByteArray() const
278{
279 return d->data;
280}
281
282void QQmlFile::load(QQmlEngine *engine, const QUrl &url)
283{
284 Q_ASSERT(engine);
285
286 clear();
287 d->url = url;
288
289 if (isLocalFile(url)) {
290 QString lf = urlToLocalFileOrQrc(url);
291
292 if (!QQml_isFileCaseCorrect(fileName: lf)) {
293 d->error = QQmlFilePrivate::CaseMismatch;
294 return;
295 }
296
297 QFile file(lf);
298 if (file.open(flags: QFile::ReadOnly)) {
299 d->data = file.readAll();
300 } else {
301 d->error = QQmlFilePrivate::NotFound;
302 }
303 } else {
304#if QT_CONFIG(qml_network)
305 d->reply = new QQmlFileNetworkReply(engine, d, url);
306#else
307 d->error = QQmlFilePrivate::NotFound;
308#endif
309 }
310}
311
312void QQmlFile::load(QQmlEngine *engine, const QString &url)
313{
314 Q_ASSERT(engine);
315
316 clear();
317
318 d->urlString = url;
319
320 if (isLocalFile(url)) {
321 QString lf = urlToLocalFileOrQrc(url);
322
323 if (!QQml_isFileCaseCorrect(fileName: lf)) {
324 d->error = QQmlFilePrivate::CaseMismatch;
325 return;
326 }
327
328 QFile file(lf);
329 if (file.open(flags: QFile::ReadOnly)) {
330 d->data = file.readAll();
331 } else {
332 d->error = QQmlFilePrivate::NotFound;
333 }
334 } else {
335#if QT_CONFIG(qml_network)
336 QUrl qurl(url);
337 d->url = qurl;
338 d->urlString = QString();
339 d->reply = new QQmlFileNetworkReply(engine, d, qurl);
340#else
341 d->error = QQmlFilePrivate::NotFound;
342#endif
343 }
344}
345
346void QQmlFile::clear()
347{
348 d->url = QUrl();
349 d->urlString = QString();
350 d->data = QByteArray();
351 d->error = QQmlFilePrivate::None;
352}
353
354void QQmlFile::clear(QObject *)
355{
356 clear();
357}
358
359#if QT_CONFIG(qml_network)
360bool QQmlFile::connectFinished(QObject *object, const char *method)
361{
362 if (!d || !d->reply) {
363 qWarning(msg: "QQmlFile: connectFinished() called when not loading.");
364 return false;
365 }
366
367 return QObject::connect(sender: d->reply, SIGNAL(finished()),
368 receiver: object, member: method);
369}
370
371bool QQmlFile::connectFinished(QObject *object, int method)
372{
373 if (!d || !d->reply) {
374 qWarning(msg: "QQmlFile: connectFinished() called when not loading.");
375 return false;
376 }
377
378 return QMetaObject::connect(sender: d->reply, signal_index: QQmlFileNetworkReply::finishedIndex,
379 receiver: object, method_index: method);
380}
381
382bool QQmlFile::connectDownloadProgress(QObject *object, const char *method)
383{
384 if (!d || !d->reply) {
385 qWarning(msg: "QQmlFile: connectDownloadProgress() called when not loading.");
386 return false;
387 }
388
389 return QObject::connect(sender: d->reply, SIGNAL(downloadProgress(qint64,qint64)),
390 receiver: object, member: method);
391}
392
393bool QQmlFile::connectDownloadProgress(QObject *object, int method)
394{
395 if (!d || !d->reply) {
396 qWarning(msg: "QQmlFile: connectDownloadProgress() called when not loading.");
397 return false;
398 }
399
400 return QMetaObject::connect(sender: d->reply, signal_index: QQmlFileNetworkReply::downloadProgressIndex,
401 receiver: object, method_index: method);
402}
403#endif
404
405/*!
406Returns true if QQmlFile will open \a url synchronously.
407
408Synchronous urls have a qrc:/ or file:// scheme.
409
410\note On Android, urls with assets:/ scheme are also considered synchronous.
411*/
412bool QQmlFile::isSynchronous(const QUrl &url)
413{
414 QString scheme = url.scheme();
415
416 if ((scheme.size() == 4 && 0 == scheme.compare(other: QLatin1String(file_string), cs: Qt::CaseInsensitive)) ||
417 (scheme.size() == 3 && 0 == scheme.compare(other: QLatin1String(qrc_string), cs: Qt::CaseInsensitive))) {
418 return true;
419
420#if defined(Q_OS_ANDROID)
421 } else if (scheme.length() == 6 && 0 == scheme.compare(QLatin1String(assets_string), Qt::CaseInsensitive)) {
422 return true;
423 } else if (scheme.length() == 7 && 0 == scheme.compare(QLatin1String(content_string), Qt::CaseInsensitive)) {
424 return true;
425#endif
426
427 } else {
428 return false;
429 }
430}
431
432/*!
433Returns true if QQmlFile will open \a url synchronously.
434
435Synchronous urls have a qrc:/ or file:// scheme.
436
437\note On Android, urls with assets:/ scheme are also considered synchronous.
438*/
439bool QQmlFile::isSynchronous(const QString &url)
440{
441 if (url.size() < 5 /* qrc:/ */)
442 return false;
443
444 QChar f = url[0];
445
446 if (f == QLatin1Char('f') || f == QLatin1Char('F')) {
447
448 return url.size() >= 7 /* file:// */ &&
449 url.startsWith(s: QLatin1String(file_string), cs: Qt::CaseInsensitive) &&
450 url[4] == QLatin1Char(':') && url[5] == QLatin1Char('/') && url[6] == QLatin1Char('/');
451
452 } else if (f == QLatin1Char('q') || f == QLatin1Char('Q')) {
453
454 return url.size() >= 5 /* qrc:/ */ &&
455 url.startsWith(s: QLatin1String(qrc_string), cs: Qt::CaseInsensitive) &&
456 url[3] == QLatin1Char(':') && url[4] == QLatin1Char('/');
457
458 }
459
460#if defined(Q_OS_ANDROID)
461 else if (f == QLatin1Char('a') || f == QLatin1Char('A')) {
462 return url.length() >= 8 /* assets:/ */ &&
463 url.startsWith(QLatin1String(assets_string), Qt::CaseInsensitive) &&
464 url[6] == QLatin1Char(':') && url[7] == QLatin1Char('/');
465 } else if (f == QLatin1Char('c') || f == QLatin1Char('C')) {
466 return url.length() >= 9 /* content:/ */ &&
467 url.startsWith(QLatin1String(content_string), Qt::CaseInsensitive) &&
468 url[7] == QLatin1Char(':') && url[8] == QLatin1Char('/');
469 }
470#endif
471
472 return false;
473}
474
475#if defined(Q_OS_ANDROID)
476static bool hasLocalContentAuthority(const QUrl &url)
477{
478 const QString authority = url.authority();
479 return authority.isEmpty()
480 || authority == QLatin1String(authority_externalstorage)
481 || authority == QLatin1String(authority_downloads_documents)
482 || authority == QLatin1String(authority_media_documents);
483}
484#endif
485
486/*!
487Returns true if \a url is a local file that can be opened with QFile.
488
489Local file urls have either a qrc:/ or file:// scheme.
490
491\note On Android, urls with assets:/ scheme are also considered local files.
492*/
493bool QQmlFile::isLocalFile(const QUrl &url)
494{
495 QString scheme = url.scheme();
496
497 // file: URLs with two slashes following the scheme can be interpreted as local files
498 // where the slashes are part of the path. Therefore, disregard the authority.
499 // See QUrl::toLocalFile().
500 if (scheme.size() == 4 && scheme.startsWith(s: QLatin1String(file_string), cs: Qt::CaseInsensitive))
501 return true;
502
503 if (scheme.size() == 3 && scheme.startsWith(s: QLatin1String(qrc_string), cs: Qt::CaseInsensitive))
504 return url.authority().isEmpty();
505
506#if defined(Q_OS_ANDROID)
507 if (scheme.length() == 6
508 && scheme.startsWith(QLatin1String(assets_string), Qt::CaseInsensitive))
509 return url.authority().isEmpty();
510 if (scheme.length() == 7
511 && scheme.startsWith(QLatin1String(content_string), Qt::CaseInsensitive))
512 return hasLocalContentAuthority(url);
513#endif
514
515 return false;
516}
517
518static bool hasScheme(const QString &url, const char *scheme, qsizetype schemeLength)
519{
520 const qsizetype urlLength = url.size();
521
522 if (urlLength < schemeLength + 1)
523 return false;
524
525 if (!url.startsWith(s: QLatin1String(scheme, scheme + schemeLength), cs: Qt::CaseInsensitive))
526 return false;
527
528 if (url[schemeLength] != QLatin1Char(':'))
529 return false;
530
531 return true;
532}
533
534static qsizetype authorityOffset(const QString &url, qsizetype schemeLength)
535{
536 const qsizetype urlLength = url.size();
537
538 if (urlLength < schemeLength + 3)
539 return -1;
540
541 const QLatin1Char slash('/');
542 if (url[schemeLength + 1] == slash && url[schemeLength + 2] == slash) {
543 // Exactly two slashes denote an authority.
544 if (urlLength < schemeLength + 4 || url[schemeLength + 3] != slash)
545 return schemeLength + 3;
546 }
547
548 return -1;
549}
550
551#if defined(Q_OS_ANDROID)
552static bool hasLocalContentAuthority(const QString &url, qsizetype schemeLength)
553{
554 const qsizetype offset = authorityOffset(url, schemeLength);
555 if (offset == -1)
556 return true; // no authority is a local authority.
557
558 const QString authorityAndPath = url.sliced(offset);
559 return authorityAndPath.startsWith(QLatin1String(authority_externalstorage))
560 || authorityAndPath.startsWith(QLatin1String(authority_downloads_documents))
561 || authorityAndPath.startsWith(QLatin1String(authority_media_documents));
562}
563
564#endif
565
566/*!
567Returns true if \a url is a local file that can be opened with QFile.
568
569Local file urls have either a qrc: or file: scheme.
570
571\note On Android, urls with assets: or content: scheme are also considered local files.
572*/
573bool QQmlFile::isLocalFile(const QString &url)
574{
575 if (url.size() < 4 /* qrc: */)
576 return false;
577
578 switch (url[0].toLatin1()) {
579 case 'f':
580 case 'F': {
581 // file: URLs with two slashes following the scheme can be interpreted as local files
582 // where the slashes are part of the path. Therefore, disregard the authority.
583 // See QUrl::toLocalFile().
584 const qsizetype fileLength = strlen(s: file_string);
585 return url.startsWith(s: QLatin1String(file_string, file_string + fileLength),
586 cs: Qt::CaseInsensitive)
587 && url.size() > fileLength
588 && url[fileLength] == QLatin1Char(':');
589 }
590 case 'q':
591 case 'Q':
592 return hasScheme(url, scheme: qrc_string, schemeLength: strlen(s: qrc_string))
593 && authorityOffset(url, schemeLength: strlen(s: qrc_string)) == -1;
594#if defined(Q_OS_ANDROID)
595 case 'a':
596 case 'A':
597 return hasScheme(url, assets_string, strlen(assets_string))
598 && authorityOffset(url, strlen(assets_string)) == -1;
599 case 'c':
600 case 'C':
601 return hasScheme(url, content_string, strlen(content_string))
602 && hasLocalContentAuthority(url, strlen(content_string));
603#endif
604 default:
605 break;
606 }
607
608 return false;
609}
610
611/*!
612If \a url is a local file returns a path suitable for passing to QFile. Otherwise returns an
613empty string.
614*/
615QString QQmlFile::urlToLocalFileOrQrc(const QUrl& url)
616{
617 if (url.scheme().compare(other: QLatin1String("qrc"), cs: Qt::CaseInsensitive) == 0) {
618 if (url.authority().isEmpty())
619 return QLatin1Char(':') + url.path();
620 return QString();
621 }
622
623#if defined(Q_OS_ANDROID)
624 if (url.scheme().compare(QLatin1String("assets"), Qt::CaseInsensitive) == 0)
625 return url.authority().isEmpty() ? url.toString() : QString();
626 if (url.scheme().compare(QLatin1String("content"), Qt::CaseInsensitive) == 0) {
627 if (hasLocalContentAuthority(url))
628 return url.toString();
629 return QString();
630 }
631#endif
632 return url.toLocalFile();
633}
634
635static QString toLocalFile(const QString &url)
636{
637 const QUrl file(url);
638 if (!file.isLocalFile())
639 return QString();
640
641 // QUrl::toLocalFile() interprets two slashes as part of the path.
642 // Therefore windows hostnames like "//servername/path/to/file.txt" are preserved.
643
644 return file.toLocalFile();
645}
646
647static bool isDoubleSlashed(const QString &url, qsizetype offset)
648{
649 const qsizetype urlLength = url.size();
650 if (urlLength < offset + 2)
651 return false;
652
653 const QLatin1Char slash('/');
654 if (url[offset] != slash || url[offset + 1] != slash)
655 return false;
656
657 if (urlLength < offset + 3)
658 return true;
659
660 return url[offset + 2] != slash;
661}
662
663/*!
664If \a url is a local file returns a path suitable for passing to QFile. Otherwise returns an
665empty string.
666*/
667QString QQmlFile::urlToLocalFileOrQrc(const QString& url)
668{
669 if (url.startsWith(s: QLatin1String("qrc://"), cs: Qt::CaseInsensitive)) {
670 // Exactly two slashes are bad because that's a URL authority.
671 // One slash is fine and >= 3 slashes are file.
672 if (url.size() == 6 || url[6] != QLatin1Char('/')) {
673 Q_ASSERT(isDoubleSlashed(url, strlen("qrc:")));
674 return QString();
675 }
676 Q_ASSERT(!isDoubleSlashed(url, strlen("qrc:")));
677 return QLatin1Char(':') + QStringView{url}.mid(pos: 6);
678 }
679
680 if (url.startsWith(s: QLatin1String("qrc:"), cs: Qt::CaseInsensitive)) {
681 Q_ASSERT(!isDoubleSlashed(url, strlen("qrc:")));
682 if (url.size() > 4)
683 return QLatin1Char(':') + QStringView{url}.mid(pos: 4);
684 return QStringLiteral(":");
685 }
686
687#if defined(Q_OS_ANDROID)
688 if (url.startsWith(QLatin1String("assets:"), Qt::CaseInsensitive))
689 return isDoubleSlashed(url, strlen("assets:")) ? QString() : url;
690 if (hasScheme(url, content_string, strlen(content_string)))
691 return hasLocalContentAuthority(url, strlen(content_string)) ? url : QString();
692#endif
693
694 return toLocalFile(url);
695}
696
697QT_END_NAMESPACE
698
699#include "qqmlfile.moc"
700

source code of qtdeclarative/src/qml/qml/qqmlfile.cpp