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#include <QtNetwork/private/qrestaccessmanager_p.h>
9
10#include <QtCore/qbytearrayview.h>
11#include <QtCore/qjsondocument.h>
12#include <QtCore/qlatin1stringmatcher.h>
13#include <QtCore/qlatin1stringview.h>
14#include <QtCore/qloggingcategory.h>
15#include <QtCore/qstringconverter.h>
16
17#include <QtCore/qxpfunctional.h>
18
20
21using namespace Qt::StringLiterals;
22
50 : wrapped(reply)
51{
52 if (!wrapped)
53 qCWarning(lcQrest, "QRestReply: QNetworkReply is nullptr");
54}
55
60{
61 delete d;
62}
63
88{
89 return wrapped;
90}
91
109std::optional<QJsonDocument> QRestReply::readJson(QJsonParseError *error)
110{
111 if (!wrapped) {
112 if (error)
114 return std::nullopt;
115 }
116
117 if (!wrapped->isFinished()) {
118 qCWarning(lcQrest, "readJson() called on an unfinished reply, ignoring");
119 if (error)
121 return std::nullopt;
122 }
123 QJsonParseError parseError;
124 const QByteArray data = wrapped->readAll();
125 const QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
126 if (error)
127 *error = parseError;
128 if (parseError.error)
129 return std::nullopt;
130 return doc;
131}
132
144{
145 return wrapped ? wrapped->readAll() : QByteArray{};
146}
147
164{
166 if (!wrapped)
167 return result;
168
169 QByteArray data = wrapped->readAll();
170 if (data.isEmpty())
171 return result;
172
173 // Text decoding needs to persist decoding state across calls to this function,
174 // so allocate decoder if not yet allocated.
175 if (!d)
176 d = new QRestReplyPrivate;
177
178 if (!d->decoder) {
179 const QByteArray charset = QRestReplyPrivate::contentCharset(wrapped);
180 d->decoder.emplace(charset.constData());
181 if (!d->decoder->isValid()) { // the decoder may not support the mimetype's charset
182 qCWarning(lcQrest, "readText(): Charset \"%s\" is not supported", charset.constData());
183 return result;
184 }
185 }
186 // Check if the decoder already had an error, or has errors after decoding current data chunk
187 if (d->decoder->hasError() || (result = (*d->decoder)(data), d->decoder->hasError())) {
188 qCWarning(lcQrest, "readText(): Decoding error occurred");
189 return {};
190 }
191 return result;
192}
193
208{
209 return wrapped ? wrapped->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() : 0;
210}
211
229{
230 const int status = httpStatus();
231 return status >= 200 && status < 300;
232}
233
244{
245 if (!wrapped)
246 return false;
247
248 const int status = httpStatus();
249 if (status > 0) {
250 // The HTTP status is set upon receiving the response headers, but the
251 // connection might still fail later while receiving the body data.
252 return wrapped->error() == QNetworkReply::RemoteHostClosedError;
253 }
254 return wrapped->error() != QNetworkReply::NoError;
255}
256
265{
266 if (!hasError())
268 return wrapped->error();
269}
270
277{
278 if (hasError())
279 return wrapped->errorString();
280 return {};
281}
282
284 = default;
285
287 = default;
288
289#ifndef QT_NO_DEBUG_STREAM
291{
292 switch (operation) {
294 return "GET"_L1;
296 return "HEAD"_L1;
298 return "POST"_L1;
300 return "PUT"_L1;
302 return "DELETE"_L1;
304 return "CUSTOM"_L1;
306 return "UNKNOWN"_L1;
307 }
308 Q_UNREACHABLE_RETURN({});
309}
310
319{
320 const QDebugStateSaver saver(debug);
321 debug.resetFormat().nospace();
322 if (!reply.networkReply()) {
323 debug << "QRestReply(no network reply)";
324 return debug;
325 }
326 debug << "QRestReply(isSuccess = " << reply.isSuccess()
327 << ", httpStatus = " << reply.httpStatus()
328 << ", isHttpStatusSuccess = " << reply.isHttpStatusSuccess()
329 << ", hasError = " << reply.hasError()
330 << ", errorString = " << reply.errorString()
331 << ", error = " << reply.error()
332 << ", isFinished = " << reply.networkReply()->isFinished()
333 << ", bytesAvailable = " << reply.networkReply()->bytesAvailable()
334 << ", url " << reply.networkReply()->url()
335 << ", operation = " << operationName(reply.networkReply()->operation())
336 << ", reply headers = " << reply.networkReply()->headers()
337 << ")";
338 return debug;
339}
340#endif // QT_NO_DEBUG_STREAM
341
342static constexpr auto parse_OWS(QByteArrayView data) noexcept
343{
344 struct R {
345 QByteArrayView ows, tail;
346 };
347
348 constexpr auto is_OWS_char = [](auto ch) { return ch == ' ' || ch == '\t'; };
349
350 qsizetype i = 0;
351 while (i < data.size() && is_OWS_char(data[i]))
352 ++i;
353
354 return R{data.first(i), data.sliced(i)};
355}
356
357static constexpr void eat_OWS(QByteArrayView &data) noexcept
358{
359 data = parse_OWS(data).tail;
360}
361
362static constexpr auto parse_quoted_string(QByteArrayView data, qxp::function_ref<void(char) const> yield)
363{
364 struct R {
365 QByteArrayView quotedString, tail;
366 constexpr explicit operator bool() const noexcept { return !quotedString.isEmpty(); }
367 };
368
369 if (!data.startsWith('"'))
370 return R{{}, data};
371
372 qsizetype i = 1; // one past initial DQUOTE
373 while (i < data.size()) {
374 switch (auto ch = data[i++]) {
375 case '"': // final DQUOTE -> end of string
376 return R{data.first(i), data.sliced(i)};
377 case '\\': // quoted-pair
378 // https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.4-3:
379 // Recipients that process the value of a quoted-string MUST handle a
380 // quoted-pair as if it were replaced by the octet following the backslash.
381 if (i == data.size())
382 break; // premature end
383 ch = data[i++]; // eat '\\'
384 [[fallthrough]];
385 default:
386 // we don't validate quoted-string octets to be only qdtext (Postel's Law)
387 yield(ch);
388 }
389 }
390
391 return R{{}, data}; // premature end
392}
393
394static constexpr bool is_tchar(char ch) noexcept
395{
396 // ### optimize
397 switch (ch) {
398 case '!':
399 case '#':
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 return true;
414 default:
415 return (ch >= 'a' && ch <= 'z')
416 || (ch >= '0' && ch <= '9')
417 || (ch >= 'A' && ch <= 'Z');
418 }
419}
420
421static constexpr auto parse_comment(QByteArrayView data) noexcept
422{
423 struct R {
424 QByteArrayView comment, tail;
425 constexpr explicit operator bool() const noexcept { return !comment.isEmpty(); }
426 };
427
428 const auto invalid = R{{}, data}; // preserves original `data`
429
430 // comment = "(" *( ctext / quoted-pair / comment ) ")"
431 // ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
432
433 if (!data.startsWith('('))
434 return invalid;
435
436 qsizetype i = 1;
437 qsizetype level = 1;
438 while (i < data.size()) {
439 switch (data[i++]) {
440 case '(': // nested comment
441 ++level;
442 break;
443 case ')': // end of comment
444 if (--level == 0)
445 return R{data.first(i), data.sliced(i)};
446 break;
447 case '\\': // quoted-pair
448 if (i == data.size())
449 return invalid; // premature end
450 ++i; // eat escaped character
451 break;
452 default:
453 ; // don't validate ctext - accept everything (Postel's Law)
454 }
455 }
456
457 return invalid; // premature end / unbalanced nesting levels
458}
459
460static constexpr void eat_CWS(QByteArrayView &data) noexcept
461{
462 eat_OWS(data);
463 while (const auto comment = parse_comment(data)) {
464 data = comment.tail;
465 eat_OWS(data);
466 }
467}
468
469static constexpr auto parse_token(QByteArrayView data) noexcept
470{
471 struct R {
472 QByteArrayView token, tail;
473 constexpr explicit operator bool() const noexcept { return !token.isEmpty(); }
474 };
475
476 qsizetype i = 0;
477 while (i < data.size() && is_tchar(data[i]))
478 ++i;
479
480 return R{data.first(i), data.sliced(i)};
481}
482
483static constexpr auto parse_parameter(QByteArrayView data, qxp::function_ref<void(char) const> yield)
484{
485 struct R {
487 constexpr explicit operator bool() const noexcept { return !name.isEmpty(); }
488 };
489
490 const auto invalid = R{{}, {}, data}; // preserves original `data`
491
492 // parameter = parameter-name "=" parameter-value
493 // parameter-name = token
494 // parameter-value = ( token / quoted-string )
495
496 const auto name = parse_token(data);
497 if (!name)
498 return invalid;
499 data = name.tail;
500
501 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
502
503 if (!data.startsWith('='))
504 return invalid;
505 data = data.sliced(1);
506
507 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
508
509 if (Q_UNLIKELY(data.startsWith('"'))) { // value is a quoted-string
510
511 const auto value = parse_quoted_string(data, yield);
512 if (!value)
513 return invalid;
514 data = value.tail;
515
516 return R{QLatin1StringView{name.token}, value.quotedString, data};
517
518 } else { // value is a token
519
520 const auto value = parse_token(data);
521 if (!value)
522 return invalid;
523 data = value.tail;
524
525 return R{QLatin1StringView{name.token}, value.token, data};
526 }
527}
528
530{
531 struct R {
532 QLatin1StringView type, subtype;
533 std::string charset;
534 constexpr explicit operator bool() const noexcept { return !type.isEmpty(); }
535 };
536
537 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
538
539 const auto type = parse_token(data);
540 if (!type)
541 return R{};
542 data = type.tail;
543
544 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
545
546 if (!data.startsWith('/'))
547 return R{};
548 data = data.sliced(1);
549
550 eat_CWS(data); // not in the grammar, but accepted under Postel's Law
551
552 const auto subtype = parse_token(data);
553 if (!subtype)
554 return R{};
555 data = subtype.tail;
556
557 eat_CWS(data);
558
559 auto r = R{QLatin1StringView{type.token}, QLatin1StringView{subtype.token}, {}};
560
561 while (data.startsWith(';')) {
562
563 data = data.sliced(1); // eat ';'
564
565 eat_CWS(data);
566
567 const auto param = parse_parameter(data, [&](char ch) { r.charset.append(1, ch); });
568 if (param.name.compare("charset"_L1, Qt::CaseInsensitive) == 0) {
569 if (r.charset.empty() && !param.value.startsWith('"')) // wasn't a quoted-string
570 r.charset.assign(param.value.begin(), param.value.end());
571 return r; // charset found
572 }
573 r.charset.clear(); // wasn't an actual charset
574 if (param.tail.size() == data.size()) // no progress was made
575 break; // returns {type, subtype}
576 // otherwise, continue (accepting e.g. `;;`)
577 data = param.tail;
578
579 eat_CWS(data);
580 }
581
582 return r; // no charset found
583}
584
586{
587 // Content-type consists of mimetype and optional parameters, of which one may be 'charset'
588 // Example values and their combinations below are all valid, see RFC 7231 section 3.1.1.5
589 // and RFC 2045 section 5.1
590 //
591 // text/plain; charset=utf-8
592 // text/plain; charset=utf-8;version=1.7
593 // text/plain; charset = utf-8
594 // text/plain; charset ="utf-8"
595
596 const QByteArray contentTypeValue =
598
599 const auto r = parse_content_type(contentTypeValue);
600 if (r && !r.charset.empty())
601 return QByteArrayView(r.charset).toByteArray();
602 else
603 return "UTF-8"_ba; // Default to the most commonly used UTF-8.
604}
605
QByteArray toByteArray() const
Definition qbytearray.h:797
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 QRestReply(QNetworkReply *reply)
Creates a QRestReply and initializes the wrapped QNetworkReply to reply.
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,...)
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