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
18QT_BEGIN_NAMESPACE
19
20using namespace Qt::StringLiterals;
21Q_DECLARE_LOGGING_CATEGORY(lcQrest)
22
23/*!
24 \class QRestReply
25 \since 6.7
26 \brief QRestReply is a convenience wrapper for QNetworkReply.
27
28 \reentrant
29 \ingroup network
30 \inmodule QtNetwork
31
32 QRestReply wraps a QNetworkReply and provides convenience methods for data
33 and status handling. The methods provide convenience for typical RESTful
34 client applications.
35
36 QRestReply doesn't take ownership of the wrapped QNetworkReply, and the
37 lifetime and ownership of the reply is as defined by QNetworkAccessManager
38 documentation.
39
40 QRestReply object is not copyable, but is movable.
41
42 \sa QRestAccessManager, QNetworkReply, QNetworkAccessManager,
43 QNetworkAccessManager::setAutoDeleteReplies()
44*/
45
46/*!
47 Creates a QRestReply and initializes the wrapped QNetworkReply to \a reply.
48*/
49QRestReply::QRestReply(QNetworkReply *reply)
50 : wrapped(reply)
51{
52 if (!wrapped)
53 qCWarning(lcQrest, "QRestReply: QNetworkReply is nullptr");
54}
55
56/*!
57 Destroys this QRestReply object.
58*/
59QRestReply::~QRestReply()
60{
61 delete d;
62}
63
64/*!
65 \fn QRestReply::QRestReply(QRestReply &&other) noexcept
66
67 Move-constructs the reply from \a other.
68
69 \note The moved-from object \a other is placed in a
70 partially-formed state, in which the only valid operations are
71 destruction and assignment of a new value.
72*/
73
74/*!
75 \fn QRestReply &QRestReply::operator=(QRestReply &&other) noexcept
76
77 Move-assigns \a other and returns a reference to this reply.
78
79 \note The moved-from object \a other is placed in a
80 partially-formed state, in which the only valid operations are
81 destruction and assignment of a new value.
82*/
83
84/*!
85 Returns a pointer to the underlying QNetworkReply wrapped by this object.
86*/
87QNetworkReply *QRestReply::networkReply() const
88{
89 return wrapped;
90}
91
92/*!
93 Returns the received data as a QJsonDocument.
94
95 The returned value is wrapped in \c std::optional. If the conversion
96 from the received data fails (empty data or JSON parsing error),
97 \c std::nullopt is returned, and \a error is filled with details.
98
99 Calling this function consumes the received data, and any further calls
100 to get response data will return empty.
101
102 This function returns \c {std::nullopt} and will not consume
103 any data if the reply is not finished. If \a error is passed, it will be
104 set to QJsonParseError::NoError to distinguish this case from an actual
105 error.
106
107 \sa readBody(), readText()
108*/
109std::optional<QJsonDocument> QRestReply::readJson(QJsonParseError *error)
110{
111 if (!wrapped) {
112 if (error)
113 *error = {.offset: 0, .error: QJsonParseError::ParseError::NoError};
114 return std::nullopt;
115 }
116
117 if (!wrapped->isFinished()) {
118 qCWarning(lcQrest, "readJson() called on an unfinished reply, ignoring");
119 if (error)
120 *error = {.offset: 0, .error: QJsonParseError::ParseError::NoError};
121 return std::nullopt;
122 }
123 QJsonParseError parseError;
124 const QByteArray data = wrapped->readAll();
125 const QJsonDocument doc = QJsonDocument::fromJson(json: data, error: &parseError);
126 if (error)
127 *error = parseError;
128 if (parseError.error)
129 return std::nullopt;
130 return doc;
131}
132
133/*!
134 Returns the received data as a QByteArray.
135
136 Calling this function consumes the data received so far, and any further
137 calls to get response data will return empty until further data has been
138 received.
139
140 \sa readJson(), readText(), QNetworkReply::bytesAvailable(),
141 QNetworkReply::readyRead()
142*/
143QByteArray QRestReply::readBody()
144{
145 return wrapped ? wrapped->readAll() : QByteArray{};
146}
147
148/*!
149 Returns the received data as a QString.
150
151 The received data is decoded into a QString (UTF-16). If available, the decoding
152 uses the \e Content-Type header's \e charset parameter to determine the
153 source encoding. If the encoding information is not available or not supported
154 by \l QStringConverter, UTF-8 is used by default.
155
156 Calling this function consumes the data received so far. Returns
157 a default constructed value if no new data is available, or if the
158 decoding is not supported by \l QStringConverter, or if the decoding
159 has errors (for example invalid characters).
160
161 \sa readJson(), readBody(), QNetworkReply::readyRead()
162*/
163QString QRestReply::readText()
164{
165 QString result;
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(reply: wrapped);
180 d->decoder.emplace(args: 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
194/*!
195 Returns the HTTP status received in the server response.
196 The value is \e 0 if not available (the status line has not been received,
197 yet).
198
199 \note The HTTP status is reported as indicated by the received HTTP
200 response. An error() may occur after receiving the status, for instance
201 due to network disconnection while receiving a long response.
202 These potential subsequent errors are not represented by the reported
203 HTTP status.
204
205 \sa isSuccess(), hasError(), error()
206*/
207int QRestReply::httpStatus() const
208{
209 return wrapped ? wrapped->attribute(code: QNetworkRequest::HttpStatusCodeAttribute).toInt() : 0;
210}
211
212/*!
213 \fn bool QRestReply::isSuccess() const
214
215 Returns whether the HTTP status is between 200..299 and no
216 further errors have occurred while receiving the response (for example,
217 abrupt disconnection while receiving the body data). This function
218 is a convenient way to check whether the response is considered successful.
219
220 \sa httpStatus(), hasError(), error()
221*/
222
223/*!
224 Returns whether the HTTP status is between 200..299.
225
226 \sa isSuccess(), httpStatus(), hasError(), error()
227*/
228bool QRestReply::isHttpStatusSuccess() const
229{
230 const int status = httpStatus();
231 return status >= 200 && status < 300;
232}
233
234/*!
235 Returns whether an error has occurred. This includes errors such as
236 network and protocol errors, but excludes cases where the server
237 successfully responded with an HTTP error status (for example
238 \c {500 Internal Server Error}). Use \l httpStatus() or
239 \l isHttpStatusSuccess() to get the HTTP status information.
240
241 \sa httpStatus(), isSuccess(), error(), errorString()
242*/
243bool QRestReply::hasError() const
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
257/*!
258 Returns the last error, if any. The errors include
259 errors such as network and protocol errors, but exclude
260 cases when the server successfully responded with an HTTP status.
261
262 \sa httpStatus(), isSuccess(), hasError(), errorString()
263*/
264QNetworkReply::NetworkError QRestReply::error() const
265{
266 if (!hasError())
267 return QNetworkReply::NetworkError::NoError;
268 return wrapped->error();
269}
270
271/*!
272 Returns a human-readable description of the last network error.
273
274 \sa httpStatus(), isSuccess(), hasError(), error()
275*/
276QString QRestReply::errorString() const
277{
278 if (hasError())
279 return wrapped->errorString();
280 return {};
281}
282
283QRestReplyPrivate::QRestReplyPrivate()
284 = default;
285
286QRestReplyPrivate::~QRestReplyPrivate()
287 = default;
288
289#ifndef QT_NO_DEBUG_STREAM
290static QLatin1StringView operationName(QNetworkAccessManager::Operation operation)
291{
292 switch (operation) {
293 case QNetworkAccessManager::Operation::GetOperation:
294 return "GET"_L1;
295 case QNetworkAccessManager::Operation::HeadOperation:
296 return "HEAD"_L1;
297 case QNetworkAccessManager::Operation::PostOperation:
298 return "POST"_L1;
299 case QNetworkAccessManager::Operation::PutOperation:
300 return "PUT"_L1;
301 case QNetworkAccessManager::Operation::DeleteOperation:
302 return "DELETE"_L1;
303 case QNetworkAccessManager::Operation::CustomOperation:
304 return "CUSTOM"_L1;
305 case QNetworkAccessManager::Operation::UnknownOperation:
306 return "UNKNOWN"_L1;
307 }
308 Q_UNREACHABLE_RETURN({});
309}
310
311/*!
312 \fn QDebug QRestReply::operator<<(QDebug debug, const QRestReply &reply)
313
314 Writes the \a reply into the \a debug object for debugging purposes.
315
316 \sa {Debugging Techniques}
317*/
318QDebug operator<<(QDebug debug, const QRestReply &reply)
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(operation: 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{.ows: data.first(n: i), .tail: data.sliced(pos: 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(c: '"'))
370 return R{.quotedString: {}, .tail: 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{.quotedString: data.first(n: i), .tail: data.sliced(pos: 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{.quotedString: {}, .tail: 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{.comment: {}, .tail: 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(c: '('))
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{.comment: data.first(n: i), .tail: data.sliced(pos: 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(ch: data[i]))
478 ++i;
479
480 return R{.token: data.first(n: i), .tail: data.sliced(pos: i)};
481}
482
483static constexpr auto parse_parameter(QByteArrayView data, qxp::function_ref<void(char) const> yield)
484{
485 struct R {
486 QLatin1StringView name; QByteArrayView value; QByteArrayView tail;
487 constexpr explicit operator bool() const noexcept { return !name.isEmpty(); }
488 };
489
490 const auto invalid = R{.name: {}, .value: {}, .tail: 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(c: '='))
504 return invalid;
505 data = data.sliced(pos: 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{.name: QLatin1StringView{name.token}, .value: value.quotedString, .tail: 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{.name: QLatin1StringView{name.token}, .value: value.token, .tail: data};
526 }
527}
528
529static auto parse_content_type(QByteArrayView data)
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(c: '/'))
547 return R{};
548 data = data.sliced(pos: 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{.type: QLatin1StringView{type.token}, .subtype: QLatin1StringView{subtype.token}, .charset: {}};
560
561 while (data.startsWith(c: ';')) {
562
563 data = data.sliced(pos: 1); // eat ';'
564
565 eat_CWS(data);
566
567 const auto param = parse_parameter(data, yield: [&](char ch) { r.charset.append(n: 1, c: ch); });
568 if (param.name.compare(other: "charset"_L1, cs: Qt::CaseInsensitive) == 0) {
569 if (r.charset.empty() && !param.value.startsWith(c: '"')) // wasn't a quoted-string
570 r.charset.assign(first: param.value.begin(), last: 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
585QByteArray QRestReplyPrivate::contentCharset(const QNetworkReply* reply)
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 =
597 reply->headers().value(name: QHttpHeaders::WellKnownHeader::ContentType).toByteArray();
598
599 const auto r = parse_content_type(data: 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
606QT_END_NAMESPACE
607

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtbase/src/network/access/qrestreply.cpp