Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
qrestreply.cpp
Go to the documentation of this file.
1// Copyright (C) 2023 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 "qrestreply.h"
5#include "qrestreply_p.h"
6
7#include <QtNetwork/private/qnetworkreply_p.h>
8
9#include <QtCore/qbytearrayview.h>
10#include <QtCore/qjsondocument.h>
11#include <QtCore/qlatin1stringmatcher.h>
12#include <QtCore/qlatin1stringview.h>
13#include <QtCore/qloggingcategory.h>
14#include <QtCore/qstringconverter.h>
15
16#include <QtCore/qxpfunctional.h>
17
19
20using namespace Qt::StringLiterals;
22
23
52 : wrapped(reply)
53{
54 if (!wrapped)
55 qCWarning(lcQrest, "QRestReply: QNetworkReply is nullptr");
56}
57
62{
63 delete d;
64}
65
90{
91 return wrapped;
92}
93
111std::optional<QJsonDocument> QRestReply::readJson(QJsonParseError *error)
112{
113 if (!wrapped) {
114 if (error)
116 return std::nullopt;
117 }
118
119 if (!wrapped->isFinished()) {
120 qCWarning(lcQrest, "readJson() called on an unfinished reply, ignoring");
121 if (error)
123 return std::nullopt;
124 }
125 QJsonParseError parseError;
126 const QByteArray data = wrapped->readAll();
127 const QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
128 if (error)
129 *error = parseError;
130 if (parseError.error)
131 return std::nullopt;
132 return doc;
133}
134
146{
147 return wrapped ? wrapped->readAll() : QByteArray{};
148}
149
166{
168 if (!wrapped)
169 return result;
170
171 QByteArray data = wrapped->readAll();
172 if (data.isEmpty())
173 return result;
174
175 // Text decoding needs to persist decoding state across calls to this function,
176 // so allocate decoder if not yet allocated.
177 if (!d)
178 d = new QRestReplyPrivate;
179
180 if (!d->decoder) {
181 const QByteArray charset = QRestReplyPrivate::contentCharset(wrapped);
182 d->decoder.emplace(charset.constData());
183 if (!d->decoder->isValid()) { // the decoder may not support the mimetype's charset
184 qCWarning(lcQrest, "readText(): Charset \"%s\" is not supported", charset.constData());
185 return result;
186 }
187 }
188 // Check if the decoder already had an error, or has errors after decoding current data chunk
189 if (d->decoder->hasError() || (result = (*d->decoder)(data), d->decoder->hasError())) {
190 qCWarning(lcQrest, "readText(): Decoding error occurred");
191 return {};
192 }
193 return result;
194}
195
210{
211 return wrapped ? wrapped->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() : 0;
212}
213
231{
232 const int status = httpStatus();
233 return status >= 200 && status < 300;
234}
235
246{
247 if (!wrapped)
248 return false;
249
250 const int status = httpStatus();
251 if (status > 0) {
252 // The HTTP status is set upon receiving the response headers, but the
253 // connection might still fail later while receiving the body data.
254 return wrapped->error() == QNetworkReply::RemoteHostClosedError;
255 }
256 return wrapped->error() != QNetworkReply::NoError;
257}
258
267{
268 if (!hasError())
270 return wrapped->error();
271}
272
279{
280 if (hasError())
281 return wrapped->errorString();
282 return {};
283}
284
286 = default;
287
289 = default;
290
291#ifndef QT_NO_DEBUG_STREAM
293{
294 switch (operation) {
296 return "GET"_L1;
298 return "HEAD"_L1;
300 return "POST"_L1;
302 return "PUT"_L1;
304 return "DELETE"_L1;
306 return "CUSTOM"_L1;
308 return "UNKNOWN"_L1;
309 }
310 Q_UNREACHABLE_RETURN({});
311}
312
321{
322 const QDebugStateSaver saver(debug);
323 debug.resetFormat().nospace();
324 if (!reply.networkReply()) {
325 debug << "QRestReply(no network reply)";
326 return debug;
327 }
328 debug << "QRestReply(isSuccess = " << reply.isSuccess()
329 << ", httpStatus = " << reply.httpStatus()
330 << ", isHttpStatusSuccess = " << reply.isHttpStatusSuccess()
331 << ", hasError = " << reply.hasError()
332 << ", errorString = " << reply.errorString()
333 << ", error = " << reply.error()
334 << ", isFinished = " << reply.networkReply()->isFinished()
335 << ", bytesAvailable = " << reply.networkReply()->bytesAvailable()
336 << ", url " << reply.networkReply()->url()
337 << ", operation = " << operationName(reply.networkReply()->operation())
338 << ", reply headers = " << reply.networkReply()->headers()
339 << ")";
340 return debug;
341}
342#endif // QT_NO_DEBUG_STREAM
343
344static constexpr auto parse_OWS(QByteArrayView data) noexcept
345{
346 struct R {
347 QByteArrayView ows, tail;
348 };
349
350 constexpr auto is_OWS_char = [](auto ch) { return ch == ' ' || ch == '\t'; };
351
352 qsizetype i = 0;
353 while (i < data.size() && is_OWS_char(data[i]))
354 ++i;
355
356 return R{data.first(i), data.sliced(i)};
357}
358
359static constexpr void eat_OWS(QByteArrayView &data) noexcept
360{
361 data = parse_OWS(data).tail;
362}
363
364static constexpr auto parse_quoted_string(QByteArrayView data, qxp::function_ref<void(char) const> yield)
365{
366 struct R {
367 QByteArrayView quotedString, tail;
368 constexpr explicit operator bool() const noexcept { return !quotedString.isEmpty(); }
369 };
370
371 if (!data.startsWith('"'))
372 return R{{}, data};
373
374 qsizetype i = 1; // one past initial DQUOTE
375 while (i < data.size()) {
376 switch (auto ch = data[i++]) {
377 case '"': // final DQUOTE -> end of string
378 return R{data.first(i), data.sliced(i)};
379 case '\\': // quoted-pair
380 // https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.4-3:
381 // Recipients that process the value of a quoted-string MUST handle a
382 // quoted-pair as if it were replaced by the octet following the backslash.
383 if (i == data.size())
384 break; // premature end
385 ch = data[i++]; // eat '\\'
386 [[fallthrough]];
387 default:
388 // we don't validate quoted-string octets to be only qdtext (Postel's Law)
389 yield(ch);
390 }
391 }
392
393 return R{{}, data}; // premature end
394}
395
396static constexpr bool is_tchar(char ch) noexcept
397{
398 // ### optimize
399 switch (ch) {
400 case '!':
401 case '#':
402 case '$':
403 case '%':
404 case '&':
405 case '\'':
406 case '*':
407 case '+':
408 case '-':
409 case '.':
410 case '^':
411 case '_':
412 case '`':
413 case '|':
414 case '~':
415 return true;
416 default:
417 return (ch >= 'a' && ch <= 'z')
418 || (ch >= '0' && ch <= '9')
419 || (ch >= 'A' && ch <= 'Z');
420 }
421}
422
423static constexpr auto parse_comment(QByteArrayView data) noexcept
424{
425 struct R {
426 QByteArrayView comment, tail;
427 constexpr explicit operator bool() const noexcept { return !comment.isEmpty(); }
428 };
429
430 const auto invalid = R{{}, data}; // preserves original `data`
431
432 // comment = "(" *( ctext / quoted-pair / comment ) ")"
433 // ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
434
435 if (!data.startsWith('('))
436 return invalid;
437
438 qsizetype i = 1;
439 qsizetype level = 1;
440 while (i < data.size()) {
441 switch (data[i++]) {
442 case '(': // nested comment
443 ++level;
444 break;
445 case ')': // end of comment
446 if (--level == 0)
447 return R{data.first(i), data.sliced(i)};
448 break;
449 case '\\': // quoted-pair
450 if (i == data.size())
451 return invalid; // premature end
452 ++i; // eat escaped character
453 break;
454 default:
455 ; // don't validate ctext - accept everything (Postel's Law)
456 }
457 }
458
459 return invalid; // premature end / unbalanced nesting levels
460}
461
462static constexpr void eat_CWS(QByteArrayView &data) noexcept
463{
464 eat_OWS(data);
465 while (const auto comment = parse_comment(data)) {
466 data = comment.tail;
467 eat_OWS(data);
468 }
469}
470
471static constexpr auto parse_token(QByteArrayView data) noexcept
472{
473 struct R {
474 QByteArrayView token, tail;
475 constexpr explicit operator bool() const noexcept { return !token.isEmpty(); }
476 };
477
478 qsizetype i = 0;
479 while (i < data.size() && is_tchar(data[i]))
480 ++i;
481
482 return R{data.first(i), data.sliced(i)};
483}
484
485static constexpr auto parse_parameter(QByteArrayView data, qxp::function_ref<void(char) const> yield)
486{
487 struct R {
489 constexpr explicit operator bool() const noexcept { return !name.isEmpty(); }
490 };
491
492 const auto invalid = R{{}, {}, data}; // preserves original `data`
493
494 // parameter = parameter-name "=" parameter-value
495 // parameter-name = token
496 // parameter-value = ( token / quoted-string )
497
498 const auto name = parse_token(data);
499 if (!name)
500 return invalid;
501 data = name.tail;
502
503 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
504
505 if (!data.startsWith('='))
506 return invalid;
507 data = data.sliced(1);
508
509 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
510
511 if (Q_UNLIKELY(data.startsWith('"'))) { // value is a quoted-string
512
513 const auto value = parse_quoted_string(data, yield);
514 if (!value)
515 return invalid;
516 data = value.tail;
517
518 return R{QLatin1StringView{name.token}, value.quotedString, data};
519
520 } else { // value is a token
521
522 const auto value = parse_token(data);
523 if (!value)
524 return invalid;
525 data = value.tail;
526
527 return R{QLatin1StringView{name.token}, value.token, data};
528 }
529}
530
532{
533 struct R {
534 QLatin1StringView type, subtype;
535 std::string charset;
536 constexpr explicit operator bool() const noexcept { return !type.isEmpty(); }
537 };
538
539 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
540
541 const auto type = parse_token(data);
542 if (!type)
543 return R{};
544 data = type.tail;
545
546 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
547
548 if (!data.startsWith('/'))
549 return R{};
550 data = data.sliced(1);
551
552 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
553
554 const auto subtype = parse_token(data);
555 if (!subtype)
556 return R{};
557 data = subtype.tail;
558
559 eat_CWS(data);
560
561 auto r = R{QLatin1StringView{type.token}, QLatin1StringView{subtype.token}, {}};
562
563 while (data.startsWith(';')) {
564
565 data = data.sliced(1); // eat ';'
566
567 eat_CWS(data);
568
569 const auto param = parse_parameter(data, [&](char ch) { r.charset.append(1, ch); });
570 if (param.name.compare("charset"_L1, Qt::CaseInsensitive) == 0) {
571 if (r.charset.empty() && !param.value.startsWith('"')) // wasn't a quoted-string
572 r.charset.assign(param.value.begin(), param.value.end());
573 return r; // charset found
574 }
575 r.charset.clear(); // wasn't an actual charset
576 if (param.tail.size() == data.size()) // no progress was made
577 break; // returns {type, subtype}
578 // otherwise, continue (accepting e.g. `;;`)
579 data = param.tail;
580
581 eat_CWS(data);
582 }
583
584 return r; // no charset found
585}
586
588{
589 // Content-type consists of mimetype and optional parameters, of which one may be 'charset'
590 // Example values and their combinations below are all valid, see RFC 7231 section 3.1.1.5
591 // and RFC 2045 section 5.1
592 //
593 // text/plain; charset=utf-8
594 // text/plain; charset=utf-8;version=1.7
595 // text/plain; charset = utf-8
596 // text/plain; charset ="utf-8"
597
598 const QByteArray contentTypeValue =
600
601 const auto r = parse_content_type(contentTypeValue);
602 if (r && !r.charset.empty())
603 return QByteArrayView(r.charset).toByteArray();
604 else
605 return "UTF-8"_ba; // Default to the most commonly used UTF-8.
606}
607
QByteArray toByteArray() const
Definition qbytearray.h:796
constexpr bool isEmpty() const noexcept
\inmodule QtCore
Definition qbytearray.h:57
const char * constData() const noexcept
Returns a pointer to the const data stored in the byte array.
Definition qbytearray.h:124
\inmodule QtCore
\inmodule QtCore
Q_NETWORK_EXPORT QByteArrayView value(QAnyStringView name, QByteArrayView defaultValue={}) const noexcept
Returns the value of the (first) header name, or defaultValue if it doesn't exist.
QByteArray readAll()
Reads all remaining data from the device, and returns it as a byte array.
virtual qint64 bytesAvailable() const
Returns the number of bytes that are available for reading.
QString errorString() const
Returns a human-readable description of the last device error that occurred.
\inmodule QtCore\reentrant
static QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error=nullptr)
Parses json as a UTF-8 encoded JSON document, and creates a QJsonDocument from it.
constexpr bool isEmpty() const noexcept
constexpr QLatin1StringView sliced(qsizetype pos) const
Operation
Indicates the operation this reply is processing.
The QNetworkReply class contains the data and headers for a request sent with QNetworkAccessManager.
bool isFinished() const
QNetworkAccessManager::Operation operation() const
Returns the operation that was posted for this reply.
QVariant attribute(QNetworkRequest::Attribute code) const
Returns the attribute associated with the code code.
QHttpHeaders headers() const
NetworkError error() const
Returns the error that was found during the processing of this request.
NetworkError
Indicates all possible error conditions found during the processing of the request.
QUrl url() const
Returns the URL of the content downloaded or uploaded.
std::optional< QStringDecoder > decoder
static QByteArray contentCharset(const QNetworkReply *reply)
QRestReply is a convenience wrapper for QNetworkReply.
Definition qrestreply.h:24
Q_NETWORK_EXPORT std::optional< QJsonDocument > readJson(QJsonParseError *error=nullptr)
Returns the received data as a QJsonDocument.
Q_NETWORK_EXPORT int httpStatus() const
Returns the HTTP status received in the server response.
Q_NETWORK_EXPORT bool isHttpStatusSuccess() const
Returns whether the HTTP status is between 200..299.
Q_NETWORK_EXPORT QString errorString() const
Returns a human-readable description of the last network error.
Q_NETWORK_EXPORT bool hasError() const
Returns whether an error has occurred.
Q_NETWORK_EXPORT QByteArray readBody()
Returns the received data as a QByteArray.
Q_NETWORK_EXPORT QNetworkReply * networkReply() const
Returns a pointer to the underlying QNetworkReply wrapped by this object.
Q_NETWORK_EXPORT QNetworkReply::NetworkError error() const
Returns the last error, if any.
Q_NETWORK_EXPORT QString readText()
Returns the received data as a QString.
Q_NETWORK_EXPORT ~QRestReply()
Destroys this QRestReply object.
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
int toInt(bool *ok=nullptr) const
Returns the variant as an int if the variant has userType() \l QMetaType::Int, \l QMetaType::Bool,...
Token token
Definition keywords.cpp:444
Combined button and popup list for selecting options.
@ CaseInsensitive
#define Q_UNLIKELY(x)
DBusConnection const char DBusError * error
EGLOutputLayerEXT EGLint EGLAttrib value
[5]
#define qCWarning(category,...)
#define Q_DECLARE_LOGGING_CATEGORY(name)
GLenum GLuint GLint level
GLboolean r
[2]
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLenum type
GLenum const GLint * param
GLuint name
GLuint64EXT * result
[6]
static constexpr auto parse_quoted_string(QByteArrayView data, qxp::function_ref< void(char) const > yield)
QDebug operator<<(QDebug debug, const QRestReply &reply)
static constexpr void eat_OWS(QByteArrayView &data) noexcept
static constexpr auto parse_token(QByteArrayView data) noexcept
static auto parse_content_type(QByteArrayView data)
static constexpr auto parse_parameter(QByteArrayView data, qxp::function_ref< void(char) const > yield)
static QLatin1StringView operationName(QNetworkAccessManager::Operation operation)
static constexpr auto parse_comment(QByteArrayView data) noexcept
static constexpr bool is_tchar(char ch) noexcept
static constexpr auto parse_OWS(QByteArrayView data) noexcept
static constexpr void eat_CWS(QByteArrayView &data) noexcept
ptrdiff_t qsizetype
Definition qtypes.h:165
QNetworkReply * reply
\inmodule QtCore\reentrant
ParseError error