1 | // Copyright (C) 2019 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 <private/qqmldatablob_p.h> |
5 | #include <private/qqmlglobal_p.h> |
6 | #include <private/qqmlprofiler_p.h> |
7 | #include <private/qqmltypeloader_p.h> |
8 | #include <private/qqmltypeloaderthread_p.h> |
9 | #include <private/qqmlsourcecoordinate_p.h> |
10 | |
11 | #include <QtQml/qqmlengine.h> |
12 | |
13 | #include <qtqml_tracepoints_p.h> |
14 | |
15 | #ifdef DATABLOB_DEBUG |
16 | #define ASSERT_CALLBACK() do { if (!m_typeLoader || !m_typeLoader->m_thread->isThisThread()) qFatal("QQmlDataBlob: An API call was made outside a callback"); } while (false) |
17 | #else |
18 | #define ASSERT_CALLBACK() |
19 | #endif |
20 | |
21 | DEFINE_BOOL_CONFIG_OPTION(dumpErrors, QML_DUMP_ERRORS); |
22 | |
23 | Q_DECLARE_LOGGING_CATEGORY(lcCycle) |
24 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | /*! |
28 | \class QQmlDataBlob |
29 | \brief The QQmlDataBlob encapsulates a data request that can be issued to a QQmlTypeLoader. |
30 | \internal |
31 | |
32 | QQmlDataBlob's are loaded by a QQmlTypeLoader. The user creates the QQmlDataBlob |
33 | and then calls QQmlTypeLoader::load() or QQmlTypeLoader::loadWithStaticData() to load it. |
34 | The QQmlTypeLoader invokes callbacks on the QQmlDataBlob as data becomes available. |
35 | */ |
36 | |
37 | /*! |
38 | \enum QQmlDataBlob::Status |
39 | |
40 | This enum describes the status of the data blob. |
41 | |
42 | \value Null The blob has not yet been loaded by a QQmlTypeLoader |
43 | \value Loading The blob is loading network data. The QQmlDataBlob::setData() callback has |
44 | not yet been invoked or has not yet returned. |
45 | \value WaitingForDependencies The blob is waiting for dependencies to be done before continuing. |
46 | This status only occurs after the QQmlDataBlob::setData() callback has been made, |
47 | and when the blob has outstanding dependencies. |
48 | \value Complete The blob's data has been loaded and all dependencies are done. |
49 | \value Error An error has been set on this blob. |
50 | */ |
51 | |
52 | /*! |
53 | \enum QQmlDataBlob::Type |
54 | |
55 | This enum describes the type of the data blob. |
56 | |
57 | \value QmlFile This is a QQmlTypeData |
58 | \value JavaScriptFile This is a QQmlScriptData |
59 | \value QmldirFile This is a QQmlQmldirData |
60 | */ |
61 | |
62 | /*! |
63 | Create a new QQmlDataBlob for \a url and of the provided \a type. |
64 | */ |
65 | QQmlDataBlob::QQmlDataBlob(const QUrl &url, Type type, QQmlTypeLoader *manager) |
66 | : m_typeLoader(manager), m_type(type), m_url(url), m_finalUrl(url), m_redirectCount(0), |
67 | m_inCallback(false), m_isDone(false) |
68 | { |
69 | //Set here because we need to get the engine from the manager |
70 | if (const QQmlEngine *qmlEngine = m_typeLoader->engine()) |
71 | m_url = qmlEngine->interceptUrl(url: m_url, type: (QQmlAbstractUrlInterceptor::DataType)m_type); |
72 | } |
73 | |
74 | /*! \internal */ |
75 | QQmlDataBlob::~QQmlDataBlob() |
76 | { |
77 | Q_ASSERT(m_waitingOnMe.isEmpty()); |
78 | |
79 | cancelAllWaitingFor(); |
80 | } |
81 | |
82 | /*! |
83 | Must be called before loading can occur. |
84 | */ |
85 | void QQmlDataBlob::startLoading() |
86 | { |
87 | Q_ASSERT(status() == QQmlDataBlob::Null); |
88 | m_data.setStatus(QQmlDataBlob::Loading); |
89 | } |
90 | |
91 | /*! |
92 | Returns the type provided to the constructor. |
93 | */ |
94 | QQmlDataBlob::Type QQmlDataBlob::type() const |
95 | { |
96 | return m_type; |
97 | } |
98 | |
99 | /*! |
100 | Returns the blob's status. |
101 | */ |
102 | QQmlDataBlob::Status QQmlDataBlob::status() const |
103 | { |
104 | return m_data.status(); |
105 | } |
106 | |
107 | /*! |
108 | Returns true if the status is Null. |
109 | */ |
110 | bool QQmlDataBlob::isNull() const |
111 | { |
112 | return status() == Null; |
113 | } |
114 | |
115 | /*! |
116 | Returns true if the status is Loading. |
117 | */ |
118 | bool QQmlDataBlob::isLoading() const |
119 | { |
120 | return status() == Loading; |
121 | } |
122 | |
123 | /*! |
124 | Returns true if the status is WaitingForDependencies. |
125 | */ |
126 | bool QQmlDataBlob::isWaiting() const |
127 | { |
128 | return status() == WaitingForDependencies || |
129 | status() == ResolvingDependencies; |
130 | } |
131 | |
132 | /*! |
133 | Returns true if the status is Complete. |
134 | */ |
135 | bool QQmlDataBlob::isComplete() const |
136 | { |
137 | return status() == Complete; |
138 | } |
139 | |
140 | /*! |
141 | Returns true if the status is Error. |
142 | */ |
143 | bool QQmlDataBlob::isError() const |
144 | { |
145 | return status() == Error; |
146 | } |
147 | |
148 | /*! |
149 | Returns true if the status is Complete or Error. |
150 | */ |
151 | bool QQmlDataBlob::isCompleteOrError() const |
152 | { |
153 | Status s = status(); |
154 | return s == Error || s == Complete; |
155 | } |
156 | |
157 | /*! |
158 | Returns the data download progress from 0 to 1. |
159 | */ |
160 | qreal QQmlDataBlob::progress() const |
161 | { |
162 | quint8 p = m_data.progress(); |
163 | if (p == 0xFF) return 1.; |
164 | else return qreal(p) / qreal(0xFF); |
165 | } |
166 | |
167 | /*! |
168 | Returns the physical url of the data. Initially this is the same as |
169 | finalUrl(), but if a URL interceptor is set, it will work on this URL |
170 | and leave finalUrl() alone. |
171 | |
172 | \sa finalUrl() |
173 | */ |
174 | QUrl QQmlDataBlob::url() const |
175 | { |
176 | return m_url; |
177 | } |
178 | |
179 | QString QQmlDataBlob::urlString() const |
180 | { |
181 | if (m_urlString.isEmpty()) |
182 | m_urlString = m_url.toString(); |
183 | |
184 | return m_urlString; |
185 | } |
186 | |
187 | /*! |
188 | Returns the logical URL to be used for resolving further URLs referred to in |
189 | the code. |
190 | |
191 | This is the blob url passed to the constructor. If a URL interceptor rewrites |
192 | the URL, this one stays the same. If a network redirect happens while fetching |
193 | the data, this url is updated to reflect the new location. Therefore, if both |
194 | an interception and a redirection happen, the final url will indirectly |
195 | incorporate the result of the interception, potentially breaking further |
196 | lookups. |
197 | |
198 | \sa url() |
199 | */ |
200 | QUrl QQmlDataBlob::finalUrl() const |
201 | { |
202 | return m_finalUrl; |
203 | } |
204 | |
205 | /*! |
206 | Returns the finalUrl() as a string. |
207 | */ |
208 | QString QQmlDataBlob::finalUrlString() const |
209 | { |
210 | if (m_finalUrlString.isEmpty()) |
211 | m_finalUrlString = m_finalUrl.toString(); |
212 | |
213 | return m_finalUrlString; |
214 | } |
215 | |
216 | /*! |
217 | Return the errors on this blob. |
218 | |
219 | May only be called from the load thread, or after the blob isCompleteOrError(). |
220 | */ |
221 | QList<QQmlError> QQmlDataBlob::errors() const |
222 | { |
223 | Q_ASSERT(isCompleteOrError() || (m_typeLoader && m_typeLoader->m_thread->isThisThread())); |
224 | return m_errors; |
225 | } |
226 | |
227 | /*! |
228 | Mark this blob as having \a errors. |
229 | |
230 | All outstanding dependencies will be cancelled. Requests to add new dependencies |
231 | will be ignored. Entry into the Error state is irreversable. |
232 | |
233 | The setError() method may only be called from within a QQmlDataBlob callback. |
234 | */ |
235 | void QQmlDataBlob::setError(const QQmlError &errors) |
236 | { |
237 | ASSERT_CALLBACK(); |
238 | |
239 | QList<QQmlError> l; |
240 | l << errors; |
241 | setError(l); |
242 | } |
243 | |
244 | /*! |
245 | \overload |
246 | */ |
247 | void QQmlDataBlob::setError(const QList<QQmlError> &errors) |
248 | { |
249 | ASSERT_CALLBACK(); |
250 | |
251 | Q_ASSERT(status() != Error); |
252 | Q_ASSERT(m_errors.isEmpty()); |
253 | |
254 | // m_errors must be set before the m_data fence |
255 | m_errors.reserve(asize: errors.size()); |
256 | for (const QQmlError &error : errors) { |
257 | if (error.url().isEmpty()) { |
258 | QQmlError mutableError = error; |
259 | mutableError.setUrl(url()); |
260 | m_errors.append(t: mutableError); |
261 | } else { |
262 | m_errors.append(t: error); |
263 | } |
264 | } |
265 | |
266 | m_data.setStatus(Error); |
267 | |
268 | if (dumpErrors()) { |
269 | qWarning().nospace() << "Errors for " << urlString(); |
270 | for (int ii = 0; ii < errors.size(); ++ii) |
271 | qWarning().nospace() << " " << qPrintable(errors.at(ii).toString()); |
272 | } |
273 | cancelAllWaitingFor(); |
274 | |
275 | if (!m_inCallback) |
276 | tryDone(); |
277 | } |
278 | |
279 | void QQmlDataBlob::setError(const QQmlJS::DiagnosticMessage &error) |
280 | { |
281 | QQmlError e; |
282 | e.setColumn(qmlConvertSourceCoordinate<quint32, int>(n: error.loc.startColumn)); |
283 | e.setLine(qmlConvertSourceCoordinate<quint32, int>(n: error.loc.startLine)); |
284 | e.setDescription(error.message); |
285 | e.setUrl(url()); |
286 | setError(e); |
287 | } |
288 | |
289 | void QQmlDataBlob::setError(const QString &description) |
290 | { |
291 | QQmlError e; |
292 | e.setDescription(description); |
293 | e.setUrl(url()); |
294 | setError(e); |
295 | } |
296 | |
297 | /*! |
298 | Wait for \a blob to become complete or to error. If \a blob is already |
299 | complete or in error, or this blob is already complete, this has no effect. |
300 | |
301 | The setError() method may only be called from within a QQmlDataBlob callback. |
302 | */ |
303 | void QQmlDataBlob::addDependency(QQmlDataBlob *blob) |
304 | { |
305 | ASSERT_CALLBACK(); |
306 | |
307 | Q_ASSERT(status() != Null); |
308 | |
309 | if (!blob || |
310 | blob->status() == Error || blob->status() == Complete || |
311 | status() == Error || status() == Complete || m_isDone) |
312 | return; |
313 | |
314 | for (const auto &existingDep: std::as_const(t&: m_waitingFor)) |
315 | if (existingDep.data() == blob) |
316 | return; |
317 | |
318 | m_data.setStatus(WaitingForDependencies); |
319 | |
320 | m_waitingFor.append(t: blob); |
321 | blob->m_waitingOnMe.append(t: this); |
322 | |
323 | // Check circular dependency |
324 | if (m_waitingOnMe.indexOf(t: blob) >= 0) { |
325 | qCWarning(lcCycle) << "Cyclic dependency detected between" << this->url().toString() |
326 | << "and" << blob->url().toString(); |
327 | m_data.setStatus(Error); |
328 | } |
329 | } |
330 | |
331 | /*! |
332 | \fn void QQmlDataBlob::dataReceived(const Data &data) |
333 | |
334 | Invoked when data for the blob is received. Implementors should use this callback |
335 | to determine a blob's dependencies. Within this callback you may call setError() |
336 | or addDependency(). |
337 | */ |
338 | |
339 | /*! |
340 | Invoked once data has either been received or a network error occurred, and all |
341 | dependencies are complete. |
342 | |
343 | You can set an error in this method, but you cannot add new dependencies. Implementors |
344 | should use this callback to finalize processing of data. |
345 | |
346 | The default implementation does nothing. |
347 | |
348 | XXX Rename processData() or some such to avoid confusion between done() (processing thread) |
349 | and completed() (main thread) |
350 | */ |
351 | void QQmlDataBlob::done() |
352 | { |
353 | } |
354 | |
355 | #if QT_CONFIG(qml_network) |
356 | /*! |
357 | Invoked if there is a network error while fetching this blob. |
358 | |
359 | The default implementation sets an appropriate QQmlError. |
360 | */ |
361 | void QQmlDataBlob::networkError(QNetworkReply::NetworkError networkError) |
362 | { |
363 | Q_UNUSED(networkError); |
364 | |
365 | QQmlError error; |
366 | error.setUrl(m_url); |
367 | |
368 | const char *errorString = nullptr; |
369 | switch (networkError) { |
370 | default: |
371 | errorString = "Network error" ; |
372 | break; |
373 | case QNetworkReply::ConnectionRefusedError: |
374 | errorString = "Connection refused" ; |
375 | break; |
376 | case QNetworkReply::RemoteHostClosedError: |
377 | errorString = "Remote host closed the connection" ; |
378 | break; |
379 | case QNetworkReply::HostNotFoundError: |
380 | errorString = "Host not found" ; |
381 | break; |
382 | case QNetworkReply::TimeoutError: |
383 | errorString = "Timeout" ; |
384 | break; |
385 | case QNetworkReply::ProxyConnectionRefusedError: |
386 | case QNetworkReply::ProxyConnectionClosedError: |
387 | case QNetworkReply::ProxyNotFoundError: |
388 | case QNetworkReply::ProxyTimeoutError: |
389 | case QNetworkReply::ProxyAuthenticationRequiredError: |
390 | case QNetworkReply::UnknownProxyError: |
391 | errorString = "Proxy error" ; |
392 | break; |
393 | case QNetworkReply::ContentAccessDenied: |
394 | errorString = "Access denied" ; |
395 | break; |
396 | case QNetworkReply::ContentNotFoundError: |
397 | errorString = "File not found" ; |
398 | break; |
399 | case QNetworkReply::AuthenticationRequiredError: |
400 | errorString = "Authentication required" ; |
401 | break; |
402 | }; |
403 | |
404 | error.setDescription(QLatin1String(errorString)); |
405 | |
406 | setError(error); |
407 | } |
408 | #endif // qml_network |
409 | |
410 | /*! |
411 | Called if \a blob, which was previously waited for, has an error. |
412 | |
413 | The default implementation does nothing. |
414 | */ |
415 | void QQmlDataBlob::dependencyError(QQmlDataBlob *blob) |
416 | { |
417 | Q_UNUSED(blob); |
418 | } |
419 | |
420 | /*! |
421 | Called if \a blob, which was previously waited for, has completed. |
422 | |
423 | The default implementation does nothing. |
424 | */ |
425 | void QQmlDataBlob::dependencyComplete(QQmlDataBlob *blob) |
426 | { |
427 | Q_UNUSED(blob); |
428 | } |
429 | |
430 | /*! |
431 | Called when all blobs waited for have completed. This occurs regardless of |
432 | whether they are in error, or complete state. |
433 | |
434 | The default implementation does nothing. |
435 | */ |
436 | void QQmlDataBlob::allDependenciesDone() |
437 | { |
438 | m_data.setStatus(QQmlDataBlob::ResolvingDependencies); |
439 | } |
440 | |
441 | /*! |
442 | Called when the download progress of this blob changes. \a progress goes |
443 | from 0 to 1. |
444 | |
445 | This callback is only invoked if an asynchronous load for this blob is |
446 | made. An asynchronous load is one in which the Asynchronous mode is |
447 | specified explicitly, or one that is implicitly delayed due to a network |
448 | operation. |
449 | |
450 | The default implementation does nothing. |
451 | */ |
452 | void QQmlDataBlob::downloadProgressChanged(qreal progress) |
453 | { |
454 | Q_UNUSED(progress); |
455 | } |
456 | |
457 | /*! |
458 | Invoked on the main thread sometime after done() was called on the load thread. |
459 | |
460 | You cannot modify the blobs state at all in this callback and cannot depend on the |
461 | order or timeliness of these callbacks. Implementors should use this callback to notify |
462 | dependencies on the main thread that the blob is done and not a lot else. |
463 | |
464 | This callback is only invoked if an asynchronous load for this blob is |
465 | made. An asynchronous load is one in which the Asynchronous mode is |
466 | specified explicitly, or one that is implicitly delayed due to a network |
467 | operation. |
468 | |
469 | The default implementation does nothing. |
470 | */ |
471 | void QQmlDataBlob::completed() |
472 | { |
473 | } |
474 | |
475 | |
476 | void QQmlDataBlob::tryDone() |
477 | { |
478 | if (status() != Loading && m_waitingFor.isEmpty() && !m_isDone) { |
479 | m_isDone = true; |
480 | addref(); |
481 | |
482 | #ifdef DATABLOB_DEBUG |
483 | qWarning("QQmlDataBlob::done() %s" , qPrintable(urlString())); |
484 | #endif |
485 | done(); |
486 | |
487 | if (status() != Error) |
488 | m_data.setStatus(Complete); |
489 | |
490 | notifyAllWaitingOnMe(); |
491 | |
492 | // Locking is not required here, as anyone expecting callbacks must |
493 | // already be protected against the blob being completed (as set above); |
494 | #ifdef DATABLOB_DEBUG |
495 | qWarning("QQmlDataBlob: Dispatching completed" ); |
496 | #endif |
497 | m_typeLoader->m_thread->callCompleted(b: this); |
498 | |
499 | release(); |
500 | } |
501 | } |
502 | |
503 | void QQmlDataBlob::cancelAllWaitingFor() |
504 | { |
505 | while (m_waitingFor.size()) { |
506 | QQmlRefPointer<QQmlDataBlob> blob = m_waitingFor.takeLast(); |
507 | |
508 | Q_ASSERT(blob->m_waitingOnMe.contains(this)); |
509 | |
510 | blob->m_waitingOnMe.removeOne(t: this); |
511 | } |
512 | } |
513 | |
514 | void QQmlDataBlob::notifyAllWaitingOnMe() |
515 | { |
516 | while (m_waitingOnMe.size()) { |
517 | QQmlDataBlob *blob = m_waitingOnMe.takeLast(); |
518 | |
519 | Q_ASSERT(std::any_of(blob->m_waitingFor.constBegin(), blob->m_waitingFor.constEnd(), |
520 | [this](const QQmlRefPointer<QQmlDataBlob> &waiting) { return waiting.data() == this; })); |
521 | |
522 | blob->notifyComplete(this); |
523 | } |
524 | } |
525 | |
526 | void QQmlDataBlob::notifyComplete(QQmlDataBlob *blob) |
527 | { |
528 | Q_ASSERT(blob->status() == Error || blob->status() == Complete); |
529 | Q_TRACE_SCOPE(QQmlCompiling, blob->url()); |
530 | QQmlCompilingProfiler prof(typeLoader()->profiler(), blob); |
531 | |
532 | m_inCallback = true; |
533 | |
534 | QQmlRefPointer<QQmlDataBlob> blobRef; |
535 | for (int i = 0; i < m_waitingFor.size(); ++i) { |
536 | if (m_waitingFor.at(i).data() == blob) { |
537 | blobRef = m_waitingFor.takeAt(i); |
538 | break; |
539 | } |
540 | } |
541 | Q_ASSERT(blobRef); |
542 | |
543 | if (blob->status() == Error) { |
544 | dependencyError(blob); |
545 | } else if (blob->status() == Complete) { |
546 | dependencyComplete(blob); |
547 | } |
548 | |
549 | if (!isError() && m_waitingFor.isEmpty()) |
550 | allDependenciesDone(); |
551 | |
552 | m_inCallback = false; |
553 | |
554 | tryDone(); |
555 | } |
556 | |
557 | QString QQmlDataBlob::SourceCodeData::readAll(QString *error) const |
558 | { |
559 | error->clear(); |
560 | if (hasInlineSourceCode) |
561 | return inlineSourceCode; |
562 | |
563 | QFile f(fileInfo.absoluteFilePath()); |
564 | if (!f.open(flags: QIODevice::ReadOnly)) { |
565 | *error = f.errorString(); |
566 | return QString(); |
567 | } |
568 | |
569 | const qint64 fileSize = fileInfo.size(); |
570 | |
571 | if (uchar *mappedData = f.map(offset: 0, size: fileSize)) { |
572 | QString source = QString::fromUtf8(utf8: reinterpret_cast<const char *>(mappedData), size: fileSize); |
573 | f.unmap(address: mappedData); |
574 | return source; |
575 | } |
576 | |
577 | QByteArray data(fileSize, Qt::Uninitialized); |
578 | if (f.read(data: data.data(), maxlen: data.size()) != data.size()) { |
579 | *error = f.errorString(); |
580 | return QString(); |
581 | } |
582 | return QString::fromUtf8(ba: data); |
583 | } |
584 | |
585 | QDateTime QQmlDataBlob::SourceCodeData::sourceTimeStamp() const |
586 | { |
587 | if (hasInlineSourceCode) |
588 | return QDateTime(); |
589 | |
590 | return fileInfo.lastModified(); |
591 | } |
592 | |
593 | bool QQmlDataBlob::SourceCodeData::exists() const |
594 | { |
595 | if (hasInlineSourceCode) |
596 | return true; |
597 | return fileInfo.exists(); |
598 | } |
599 | |
600 | bool QQmlDataBlob::SourceCodeData::isEmpty() const |
601 | { |
602 | if (hasInlineSourceCode) |
603 | return inlineSourceCode.isEmpty(); |
604 | return fileInfo.size() == 0; |
605 | } |
606 | |
607 | QT_END_NAMESPACE |
608 | |