Add QHttpHeaders class

New QHttpHeaders class for use in place of
std::pair<QBA,QBA>, QMap<QBA>, and QMultiMap/Hash<QBA,QBA>
to represent HTTP headers.

[ChangeLog][QtNetwork][QHttpHeaders] New QHttpHeaders class

Task-number: QTBUG-107042
Change-Id: I54766886a491acfc9a813a3414322a75011acb9d
Reviewed-by: Marc Mutz <marc.mutz@qt.io>
bb10
Juha Vuolle 2023-09-14 11:09:06 +03:00
parent 0919f97811
commit 925ce9e908
6 changed files with 1909 additions and 0 deletions

View File

@ -123,6 +123,7 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http
access/qhttp1configuration.cpp access/qhttp1configuration.h
access/qhttp2configuration.cpp access/qhttp2configuration.h
access/qhttp2protocolhandler.cpp access/qhttp2protocolhandler_p.h
access/qhttpheaders.cpp access/qhttpheaders.h
access/qhttpmultipart.cpp access/qhttpmultipart.h access/qhttpmultipart_p.h
access/qhttpnetworkconnection.cpp access/qhttpnetworkconnection_p.h
access/qhttpnetworkconnectionchannel.cpp access/qhttpnetworkconnectionchannel_p.h

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,276 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QHTTPHEADERS_H
#define QHTTPHEADERS_H
#include <QtNetwork/qnetworkrequest.h>
#include <QtCore/qshareddata.h>
#include <QtCore/qcontainerfwd.h>
#include <initializer_list>
QT_BEGIN_NAMESPACE
class QDebug;
class QHttpHeadersPrivate;
QT_DECLARE_QESDP_SPECIALIZATION_DTOR_WITH_EXPORT(QHttpHeadersPrivate, Q_NETWORK_EXPORT)
class QHttpHeaders
{
Q_GADGET_EXPORT(Q_NETWORK_EXPORT)
public:
enum class WellKnownHeader {
// IANA Permanent status:
AIM,
Accept,
AcceptAdditions,
AcceptCH,
AcceptDatetime,
AcceptEncoding,
AcceptFeatures,
AcceptLanguage,
AcceptPatch,
AcceptPost,
AcceptRanges,
AcceptSignature,
AccessControlAllowCredentials,
AccessControlAllowHeaders,
AccessControlAllowMethods,
AccessControlAllowOrigin,
AccessControlExposeHeaders,
AccessControlMaxAge,
AccessControlRequestHeaders,
AccessControlRequestMethod,
Age,
Allow,
ALPN,
AltSvc,
AltUsed,
Alternates,
ApplyToRedirectRef,
AuthenticationControl,
AuthenticationInfo,
Authorization,
CacheControl,
CacheStatus,
CalManagedID,
CalDAVTimezones,
CapsuleProtocol,
CDNCacheControl,
CDNLoop,
CertNotAfter,
CertNotBefore,
ClearSiteData,
ClientCert,
ClientCertChain,
Close,
Connection,
ContentDigest,
ContentDisposition,
ContentEncoding,
ContentID,
ContentLanguage,
ContentLength,
ContentLocation,
ContentRange,
ContentSecurityPolicy,
ContentSecurityPolicyReportOnly,
ContentType,
Cookie,
CrossOriginEmbedderPolicy,
CrossOriginEmbedderPolicyReportOnly,
CrossOriginOpenerPolicy,
CrossOriginOpenerPolicyReportOnly,
CrossOriginResourcePolicy,
DASL,
Date,
DAV,
DeltaBase,
Depth,
Destination,
DifferentialID,
DPoP,
DPoPNonce,
EarlyData,
ETag,
Expect,
ExpectCT,
Expires,
Forwarded,
From,
Hobareg,
Host,
If,
IfMatch,
IfModifiedSince,
IfNoneMatch,
IfRange,
IfScheduleTagMatch,
IfUnmodifiedSince,
IM,
IncludeReferredTokenBindingID,
KeepAlive,
Label,
LastEventID,
LastModified,
Link,
Location,
LockToken,
MaxForwards,
MementoDatetime,
Meter,
MIMEVersion,
Negotiate,
NEL,
ODataEntityId,
ODataIsolation,
ODataMaxVersion,
ODataVersion,
OptionalWWWAuthenticate,
OrderingType,
Origin,
OriginAgentCluster,
OSCORE,
OSLCCoreVersion,
Overwrite,
PingFrom,
PingTo,
Position,
Prefer,
PreferenceApplied,
Priority,
ProxyAuthenticate,
ProxyAuthenticationInfo,
ProxyAuthorization,
ProxyStatus,
PublicKeyPins,
PublicKeyPinsReportOnly,
Range,
RedirectRef,
Referer,
Refresh,
ReplayNonce,
ReprDigest,
RetryAfter,
ScheduleReply,
ScheduleTag,
SecPurpose,
SecTokenBinding,
SecWebSocketAccept,
SecWebSocketExtensions,
SecWebSocketKey,
SecWebSocketProtocol,
SecWebSocketVersion,
Server,
ServerTiming,
SetCookie,
Signature,
SignatureInput,
SLUG,
SoapAction,
StatusURI,
StrictTransportSecurity,
Sunset,
SurrogateCapability,
SurrogateControl,
TCN,
TE,
Timeout,
Topic,
Traceparent,
Tracestate,
Trailer,
TransferEncoding,
TTL,
Upgrade,
Urgency,
UserAgent,
VariantVary,
Vary,
Via,
WantContentDigest,
WantReprDigest,
WWWAuthenticate,
XContentTypeOptions,
XFrameOptions,
// IANA Deprecated status:
AcceptCharset,
CPEPInfo,
Pragma,
ProtocolInfo,
ProtocolQuery,
};
Q_ENUM(WellKnownHeader)
enum class CompareOption {
OrderInsensitive = 0x01,
OrderSensitive = 0x02,
};
Q_DECLARE_FLAGS(CompareOptions, CompareOption)
Q_NETWORK_EXPORT QHttpHeaders();
Q_NETWORK_EXPORT ~QHttpHeaders();
Q_NETWORK_EXPORT QHttpHeaders(const QHttpHeaders &other);
QHttpHeaders(QHttpHeaders &&other) noexcept = default;
Q_NETWORK_EXPORT QHttpHeaders &operator=(const QHttpHeaders &other);
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QHttpHeaders)
void swap(QHttpHeaders &other) noexcept { d.swap(other.d); }
Q_NETWORK_EXPORT bool append(QAnyStringView name, QAnyStringView value);
Q_NETWORK_EXPORT bool append(WellKnownHeader name, QAnyStringView value);
Q_NETWORK_EXPORT bool insert(qsizetype i, QAnyStringView name, QAnyStringView value);
Q_NETWORK_EXPORT bool insert(qsizetype i, WellKnownHeader name, QAnyStringView value);
Q_NETWORK_EXPORT bool replace(qsizetype i, QAnyStringView name, QAnyStringView value);
Q_NETWORK_EXPORT bool replace(qsizetype i, WellKnownHeader name, QAnyStringView value);
Q_NETWORK_EXPORT bool has(QAnyStringView name) const;
Q_NETWORK_EXPORT bool has(WellKnownHeader name) const;
Q_NETWORK_EXPORT QList<QByteArray> names() const;
Q_NETWORK_EXPORT void clear();
Q_NETWORK_EXPORT void removeAll(QAnyStringView name);
Q_NETWORK_EXPORT void removeAll(WellKnownHeader name);
Q_NETWORK_EXPORT void removeAt(qsizetype i);
Q_NETWORK_EXPORT QList<QByteArray> values(QAnyStringView name) const;
Q_NETWORK_EXPORT QList<QByteArray> values(WellKnownHeader name) const;
Q_NETWORK_EXPORT QByteArray combinedValue(QAnyStringView name) const;
Q_NETWORK_EXPORT QByteArray combinedValue(WellKnownHeader name) const;
Q_NETWORK_EXPORT qsizetype size() const noexcept;
bool isEmpty() const noexcept { return size() == 0; }
Q_NETWORK_EXPORT bool equals(const QHttpHeaders &other,
CompareOptions options = CompareOption::OrderInsensitive) const noexcept;
Q_NETWORK_EXPORT static QHttpHeaders
fromListOfPairs(const QList<std::pair<QByteArray, QByteArray>> &headers);
Q_NETWORK_EXPORT static QHttpHeaders
fromMultiMap(const QMultiMap<QByteArray, QByteArray> &headers);
Q_NETWORK_EXPORT static QHttpHeaders
fromMultiHash(const QMultiHash<QByteArray, QByteArray> &headers);
Q_NETWORK_EXPORT QList<std::pair<QByteArray, QByteArray>> toListOfPairs() const;
Q_NETWORK_EXPORT QMultiMap<QByteArray, QByteArray> toMultiMap() const;
Q_NETWORK_EXPORT QMultiHash<QByteArray, QByteArray> toMultiHash() const;
private:
#ifndef QT_NO_DEBUG_STREAM
friend Q_NETWORK_EXPORT QDebug operator<<(QDebug debug, const QHttpHeaders &headers);
#endif
QExplicitlySharedDataPointer<QHttpHeadersPrivate> d;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(QHttpHeaders::CompareOptions)
Q_DECLARE_SHARED(QHttpHeaders)
QT_END_NAMESPACE
#endif // QHTTPHEADERS_H

View File

@ -1,6 +1,7 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
add_subdirectory(qhttpheaders)
add_subdirectory(qnetworkdiskcache)
add_subdirectory(qnetworkcookiejar)
add_subdirectory(qnetworkaccessmanager)

View File

@ -0,0 +1,10 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
qt_internal_add_test(tst_qhttpheaders
SOURCES
tst_qhttpheaders.cpp
LIBRARIES
Qt::Core
Qt::Network
)

View File

@ -0,0 +1,517 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtNetwork/qhttpheaders.h>
#include <QtTest/qtest.h>
#include <QtCore/qset.h>
using namespace Qt::StringLiterals;
class tst_QHttpHeaders : public QObject
{
Q_OBJECT
private slots:
void comparison();
void constructors();
void accessors();
void headerNameField();
void headerValueField();
void valueEncoding();
private:
static constexpr QAnyStringView n1{"name1"};
static constexpr QAnyStringView n2{"name2"};
static constexpr QAnyStringView n3{"name3"};
static constexpr QAnyStringView v1{"value1"};
static constexpr QAnyStringView v2{"value2"};
static constexpr QAnyStringView v3{"value3"};
static constexpr QAnyStringView N1{"NAME1"};
static constexpr QAnyStringView N2{"NAME2"};
static constexpr QAnyStringView N3{"NAME3"};
static constexpr QAnyStringView V1{"VALUE1"};
static constexpr QAnyStringView V2{"VALUE2"};
static constexpr QAnyStringView V3{"VALUE3"};
};
void tst_QHttpHeaders::comparison()
{
// Basic comparisons
QHttpHeaders h1;
QHttpHeaders h2;
QVERIFY(h1.equals(h2)); // empties
h1.append(n1, v1);
QVERIFY(h1.equals(h1)); // self
h2.append(n1, v1);
QVERIFY(h1.equals(h2));
h1.append(n2, v2);
QVERIFY(!h1.equals(h2));
h1.removeAll(n2);
QVERIFY(h1.equals(h2));
// 'name' case-insensitivity and 'value' case-sensitivity
h1.removeAll(n1);
QVERIFY(h1.isEmpty());
h1.append(N1, v1);
QVERIFY(h1.equals(h2));
h1.removeAll(n1);
QVERIFY(h1.isEmpty());
h1.append(n1, V1);
QVERIFY(!h1.equals(h2));
// Order-insensitivity
h1.clear();
h2.clear();
QVERIFY(h1.isEmpty() && h2.isEmpty());
// Same headers but in different order
h1.append(n1, v1);
h1.append(n2, v2);
h2.append(n2, v2);
h2.append(n1, v1);
QVERIFY(h1.equals(h2));
// Add header with different name casing
h1.insert(0, n1, v1);
h2.append(N1, v1);
QVERIFY(h1.equals(h2));
// Add header with different value casing
h1.insert(0, n1, v1);
h2.append(n1, V1);
QVERIFY(!h1.equals(h2));
// Order-sensitivity
h1.clear();
h2.clear();
QVERIFY(h1.equals(h2, QHttpHeaders::CompareOption::OrderSensitive));
// Same headers but in different order
h1.append(n1, v1);
h1.append(n2, v2);
h2.append(n2, v2);
h2.append(n1, v1);
QVERIFY(!h1.equals(h2, QHttpHeaders::CompareOption::OrderSensitive));
// Different number of headers
h1.clear();
h2.clear();
h1.append(n1, v1);
h2.append(n1, v1);
h2.append(n2, v2);
QVERIFY(!h1.equals(h2));
QVERIFY(!h1.equals(h2, QHttpHeaders::CompareOption::OrderSensitive));
}
void tst_QHttpHeaders::constructors()
{
// Default ctor
QHttpHeaders h1;
QVERIFY(h1.isEmpty());
// Copy ctor
QHttpHeaders h2(h1);
QVERIFY(h2.equals(h1));
// Copy assignment
QHttpHeaders h3;
h3 = h1;
QVERIFY(h3.equals(h1));
// Move assignment
QHttpHeaders h4;
h4 = std::move(h2);
QVERIFY(h4.equals(h1));
// Move ctor
QHttpHeaders h5(std::move(h4));
QVERIFY(h5.equals(h1));
// Constructors that are counterparts to 'toXXX()' conversion getters
const QByteArray nb1{"name1"};
const QByteArray nb2{"name2"};
const QByteArray nv1{"value1"};
const QByteArray nv2{"value2"};
// Initialize three QHttpHeaders with same content and verify they match
QList<std::pair<QByteArray, QByteArray>> list{{nb1, nv1}, {nb2, nv2}, {nb2, nv2}};
QMultiMap<QByteArray, QByteArray> map{{nb1, nv1}, {nb2, nv2}, {nb2, nv2}};
QMultiHash<QByteArray, QByteArray> hash{{nb1, nv1}, {nb2, nv2}, {nb2, nv2}};
QHttpHeaders hlist = QHttpHeaders::fromListOfPairs(list);
QHttpHeaders hmap = QHttpHeaders::fromMultiMap(map);
QHttpHeaders hhash = QHttpHeaders::fromMultiHash(hash);
QVERIFY(hlist.has(nb1) && hmap.has(nb1) && hhash.has(nb1));
QVERIFY(!hlist.has(n3) && !hmap.has(n3) && !hhash.has(n3));
QVERIFY(hlist.equals(hmap));
QVERIFY(hmap.equals(hhash));
}
void tst_QHttpHeaders::accessors()
{
QHttpHeaders h1;
// isEmpty(), clear(), size()
h1.append(n1,v1);
QVERIFY(!h1.isEmpty());
QCOMPARE(h1.size(), 1);
QVERIFY(h1.append(n1, v1));
QCOMPARE(h1.size(), 2);
h1.insert(0, n1, v1);
QCOMPARE(h1.size(), 3);
h1.clear();
QVERIFY(h1.isEmpty());
// has()
h1.append(n1, v1);
QVERIFY(h1.has(n1));
QVERIFY(h1.has(N1));
QVERIFY(!h1.has(n2));
QVERIFY(!h1.has(QHttpHeaders::WellKnownHeader::Allow));
h1.append(QHttpHeaders::WellKnownHeader::Accept, "nothing");
QVERIFY(h1.has(QHttpHeaders::WellKnownHeader::Accept));
QVERIFY(h1.has("accept"));
// values()
QCOMPARE(h1.values(n1).at(0), v1);
QCOMPARE(h1.values(N1).at(0), v1);
QCOMPARE(h1.values(QHttpHeaders::WellKnownHeader::Accept).at(0), "nothing");
QCOMPARE(h1.values("Accept").at(0), "nothing");
QVERIFY(h1.values(N2).isEmpty());
QVERIFY(h1.values(QHttpHeaders::WellKnownHeader::Allow).isEmpty());
h1.clear();
QVERIFY(h1.values(n1).isEmpty());
h1.append(n1, v1);
h1.append(n1, v2);
h1.append(n1, v3);
h1.append(n2, v2);
h1.append(n3, ""); // empty value
QCOMPARE(h1.values(n1).size(), 3);
QCOMPARE(h1.values(n1).at(0), v1);
QCOMPARE(h1.values(n1).at(1), v2);
QCOMPARE(h1.values(n1).at(2), v3);
QCOMPARE(h1.values(n1), h1.values(N1));
QVERIFY(!h1.values(n3).isEmpty());
QCOMPARE(h1.values(n3).at(0), "");
QVERIFY(!h1.combinedValue(n1).isNull());
QCOMPARE(h1.combinedValue(n1), "value1,value2,value3"_ba);
h1.append(QHttpHeaders::WellKnownHeader::Accept, "nothing");
h1.append(QHttpHeaders::WellKnownHeader::Accept, "ever");
QVERIFY(!h1.combinedValue(QHttpHeaders::WellKnownHeader::Accept).isNull());
QCOMPARE(h1.combinedValue(QHttpHeaders::WellKnownHeader::Accept), "nothing,ever");
QVERIFY(h1.combinedValue("nonexistent").isNull());
// names()
h1.clear();
QVERIFY(h1.names().isEmpty());
h1.append(n1, v1);
QCOMPARE(h1.names().size(), 1);
QCOMPARE(h1.size(), 1);
QVERIFY(h1.names().contains(n1.toString().toLatin1()));
h1.append(n2, v2);
QCOMPARE(h1.names().size(), 2);
QCOMPARE(h1.size(), 2);
QVERIFY(h1.names().contains(n1.toString().toLatin1()));
QVERIFY(h1.names().contains(n2.toString().toLatin1()));
h1.append(n1, v1);
h1.append(n1, v1);
QCOMPARE(h1.size(), 4);
QCOMPARE(h1.names().size(), 2);
h1.append(N1, v1); // uppercase of n1
QCOMPARE(h1.size(), 5);
QCOMPARE(h1.names().size(), 2);
// removeAll()
h1.clear();
QVERIFY(h1.append(n1, v1));
QVERIFY(h1.append(QHttpHeaders::WellKnownHeader::Accept, "nothing"));
QVERIFY(h1.append(n1, v1));
QCOMPARE(h1.size(), 3);
h1.removeAll(n1);
QVERIFY(!h1.has(n1));
QCOMPARE(h1.size(), 1);
QVERIFY(h1.has("accept"));
h1.removeAll(QHttpHeaders::WellKnownHeader::Accept);
QVERIFY(!h1.has(QHttpHeaders::WellKnownHeader::Accept));
// removeAt()
h1.clear();
h1.append(n1, v1);
h1.append(n2, v2);
h1.append(n3, v3);
// Valid removals
QVERIFY(h1.has(n3));
h1.removeAt(2);
QVERIFY(!h1.has(n3));
QVERIFY(h1.has(n1));
h1.removeAt(0);
QVERIFY(!h1.has(n1));
QVERIFY(h1.has(n2));
h1.removeAt(0);
QVERIFY(!h1.has(n2));
QVERIFY(h1.isEmpty());
// toListOfPairs()
h1.clear();
h1.append(n1, v1);
h1.append(n2, v2);
h1.append(N3, V3); // uppercase of n3
auto list = h1.toListOfPairs();
QCOMPARE(list.size(), h1.size());
QCOMPARE(list.at(0).first, n1);
QCOMPARE(list.at(0).second, v1);
QCOMPARE(list.at(1).first, n2);
QCOMPARE(list.at(1).second, v2);
QCOMPARE(list.at(2).first, n3); // N3 has been lower-cased
QCOMPARE(list.at(2).second, V3);
// toMultiMap()
auto map = h1.toMultiMap();
QCOMPARE(map.size(), h1.size());
QCOMPARE(map.value(n1.toString().toLatin1()), v1);
QCOMPARE(map.value(n2.toString().toLatin1()), v2);
QCOMPARE(map.value(n3.toString().toLatin1()), V3);
// toMultiHash()
auto hash = h1.toMultiHash();
QCOMPARE(hash.size(), h1.size());
QCOMPARE(hash.value(n1.toString().toLatin1()), v1);
QCOMPARE(hash.value(n2.toString().toLatin1()), v2);
QCOMPARE(hash.value(n3.toString().toLatin1()), V3);
// insert()
h1.clear();
h1.append(n3, v3);
QVERIFY(h1.insert(0, n1, v1));
list = h1.toListOfPairs();
QCOMPARE(list.size(), 2);
QCOMPARE(list.at(0).first, n1);
QCOMPARE(list.at(0).second, v1);
QCOMPARE(list.at(1).first, n3);
QCOMPARE(list.at(1).second, v3);
QVERIFY(h1.insert(1, n2, v2));
list = h1.toListOfPairs();
QCOMPARE(list.size(), 3);
QCOMPARE(list.at(0).first, n1);
QCOMPARE(list.at(0).second, v1);
QCOMPARE(list.at(1).first, n2);
QCOMPARE(list.at(1).second, v2);
QCOMPARE(list.at(2).first, n3);
QCOMPARE(list.at(2).second, v3);
QVERIFY(h1.insert(1, QHttpHeaders::WellKnownHeader::Accept, "nothing"));
QCOMPARE(h1.size(), 4);
list = h1.toListOfPairs();
QCOMPARE(list.at(1).first, "accept");
QCOMPARE(list.at(1).second, "nothing");
QVERIFY(h1.insert(list.size(), "LastName", "lastValue"));
QCOMPARE(h1.size(), 5);
list = h1.toListOfPairs();
QCOMPARE(list.last().first, "lastname");
QCOMPARE(list.last().second, "lastValue");
// Failed insert
QRegularExpression re("HTTP header name contained*");
QTest::ignoreMessage(QtMsgType::QtWarningMsg, re);
QVERIFY(!h1.insert(0, "a€", "b"));
// replace
h1.clear();
h1.append(n1, v1);
h1.append(n2, v2);
QCOMPARE(h1.size(), 2);
QVERIFY(h1.replace(0, n3, v3));
QVERIFY(h1.replace(1, QHttpHeaders::WellKnownHeader::Accept, "nothing"));
QCOMPARE(h1.size(), 2);
list = h1.toListOfPairs();
QCOMPARE(list.at(0).first, n3);
QCOMPARE(list.at(0).second, v3);
QCOMPARE(list.at(1).first, "accept");
QCOMPARE(list.at(1).second, "nothing");
QVERIFY(h1.replace(1, "ACCEPT", "NOTHING"));
QCOMPARE(h1.size(), 2);
list = h1.toListOfPairs();
QCOMPARE(list.at(0).first, n3);
QCOMPARE(list.at(0).second, v3);
QCOMPARE(list.at(1).first, "accept");
QCOMPARE(list.at(1).second, "NOTHING");
// Failed replace
QTest::ignoreMessage(QtMsgType::QtWarningMsg, re);
QVERIFY(!h1.replace(0, "a€", "b"));
}
#define TEST_ILLEGAL_HEADER_NAME_CHARACTER(NAME) \
QTest::ignoreMessage(QtMsgType::QtWarningMsg, re); \
QVERIFY(!h1.append(NAME, v1)); \
QVERIFY(h1.isEmpty()); \
void tst_QHttpHeaders::headerNameField()
{
QHttpHeaders h1;
// All allowed characters in different encodings and types
// const char[]
h1.append("abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~", v1);
QCOMPARE(h1.size(), 1);
// UTF-8
h1.append(u8"abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~",
v1);
QCOMPARE(h1.size(), 2);
// UTF-16
h1.append(u"abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~", v1);
QCOMPARE(h1.size(), 3);
// QString (UTF-16)
h1.append(u"abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~"_s,
v1);
QCOMPARE(h1.size(), 4);
QCOMPARE(h1.names().size(), 1);
h1.clear();
// Error cases
// Header name must contain at least 1 character
QTest::ignoreMessage(QtMsgType::QtWarningMsg, "HTTP header name cannot be empty");
h1.append("", v1);
QVERIFY(h1.isEmpty());
// Disallowed ASCII/extended ASCII characters (not exhaustive list)
QRegularExpression re("HTTP header name contained illegal character*");
TEST_ILLEGAL_HEADER_NAME_CHARACTER("foo\x08" "bar"); // BS
TEST_ILLEGAL_HEADER_NAME_CHARACTER("foo\x7F" "bar"); // DEL
TEST_ILLEGAL_HEADER_NAME_CHARACTER("foo()" "bar"); // parantheses
TEST_ILLEGAL_HEADER_NAME_CHARACTER("foobar" "¿"); // extended ASCII
TEST_ILLEGAL_HEADER_NAME_CHARACTER("©" "foobar"); // extended ASCII
TEST_ILLEGAL_HEADER_NAME_CHARACTER("foo,bar"); // comma
// Disallowed UTF-8 characters
TEST_ILLEGAL_HEADER_NAME_CHARACTER(u8"");
TEST_ILLEGAL_HEADER_NAME_CHARACTER(u8"𝒜𝒴𝟘𝟡𐎀𐎜𐒀𐒐𝓐𝓩𝔸𝔹𝕀𝕁𝕌𝕍𓂀𓂁𓃀𓃁𓇋𓇌𓉐𓉑𓋴𓋵𓎡𓎢𓎣𓏏");
// Disallowed UTF-16 characters
TEST_ILLEGAL_HEADER_NAME_CHARACTER(u"");
TEST_ILLEGAL_HEADER_NAME_CHARACTER(u"𝒜𝒴𝟘𝟡𐎀𐎜𐒀𐒐𝓐𝓩𝔸𝔹𝕀𝕁𝕌𝕍𓂀𓂁𓃀𓃁𓇋𓇌𓉐𓉑𓋴𓋵𓎡𓎢𓎣𓏏");
// Non-null-terminated name. The 'x' below is to make sure the strings don't
// null-terminate by happenstance
h1.clear();
constexpr char L1Array[] = {'a','b','c','x'};
const QLatin1StringView nonNullLatin1{L1Array, sizeof(L1Array) - 1}; // abc
constexpr char UTF8Array[] = {0x64, 0x65, 0x66, 0x78};
const QUtf8StringView nonNullUTF8(UTF8Array, sizeof(UTF8Array) - 1); // def
constexpr QChar UTF16Array[] = {'g', 'h', 'i', 'x'};
QStringView nonNullUTF16(UTF16Array, sizeof(UTF16Array) / sizeof(QChar) - 1); // ghi
h1.append(nonNullLatin1, v1);
QCOMPARE(h1.size(), 1);
QVERIFY(h1.has(nonNullLatin1));
QCOMPARE(h1.combinedValue(nonNullLatin1), v1);
h1.append(nonNullUTF8, v2);
QCOMPARE(h1.size(), 2);
QVERIFY(h1.has(nonNullUTF8));
QCOMPARE(h1.combinedValue(nonNullUTF8), v2);
h1.append(nonNullUTF16, v3);
QCOMPARE(h1.size(), 3);
QVERIFY(h1.has(nonNullUTF16));
QCOMPARE(h1.combinedValue(nonNullUTF16), v3);
}
#define TEST_ILLEGAL_HEADER_VALUE_CHARACTER(VALUE) \
QTest::ignoreMessage(QtMsgType::QtWarningMsg, re); \
QVERIFY(!h1.append(n1, VALUE)); \
QVERIFY(h1.isEmpty()); \
void tst_QHttpHeaders::headerValueField()
{
QHttpHeaders h1;
// Visible ASCII characters and space and horizontal tab
// const char[]
h1.append(n1, "!\"#$%&'()*+,-./0123456789:; \t<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
"`abcdefghijklmnopqrstuvwxyz{|}~");
QCOMPARE(h1.size(), 1);
// UTF-8
h1.append(n1, u8"!\"#$%&'()*+,-./0123456789:; \t<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
"`abcdefghijklmnopqrstuvwxyz{|}~");
QCOMPARE(h1.size(), 2);
// UTF-16
h1.append(n1, u"!\"#$%&'()*+,-./0123456789:; \t<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
"`abcdefghijklmnopqrstuvwxyz{|}~");
QCOMPARE(h1.size(), 3);
// QString / UTF-16
h1.append(n1, u"!\"#$%&'()*+,-./0123456789:; \t<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_"
"`abcdefghijklmnopqrstuvwxyz{|}~"_s);
QCOMPARE(h1.size(), 4);
const auto values = h1.values(n1);
QVERIFY(!values.isEmpty() && values.size() == 4);
QVERIFY(values[0] == values[1]
&& values[1] == values[2]
&& values[2] == values[3]);
// Extended ASCII (explicit on Latin-1 to avoid UTF-8 interpretation)
h1.append(n1, "\x80\x09\xB2\xFF"_L1);
QCOMPARE(h1.size(), 5);
// Empty value
h1.append(n1, "");
QCOMPARE(h1.size(), 6);
// Leading and trailing space
h1.clear();
h1.append(n1, " foo ");
QCOMPARE(h1.combinedValue(n1), "foo");
h1.append(n1, "\tbar\t");
QCOMPARE(h1.combinedValue(n1), "foo,bar");
QCOMPARE(h1.size(), 2);
h1.clear();
QRegularExpression re("HTTP header value contained illegal character*");
TEST_ILLEGAL_HEADER_VALUE_CHARACTER("foo\x08" "bar"); // BS
TEST_ILLEGAL_HEADER_VALUE_CHARACTER("foo\x1B" "bar"); // ESC
// Disallowed UTF-8 characters
TEST_ILLEGAL_HEADER_VALUE_CHARACTER(u8"");
TEST_ILLEGAL_HEADER_VALUE_CHARACTER(u8"𝒜𝒴𝟘𝟡𐎀𐎜𐒀𐒐𝓐𝓩𝔸𝔹𝕀𝕁𝕌𝕍𓂀𓂁𓃀𓃁𓇋𓇌𓉐𓉑𓋴𓋵𓎡𓎢𓎣𓏏");
// Disallowed UTF-16 characters
TEST_ILLEGAL_HEADER_VALUE_CHARACTER(u"");
TEST_ILLEGAL_HEADER_VALUE_CHARACTER(u"𝒜𝒴𝟘𝟡𐎀𐎜𐒀𐒐𝓐𝓩𝔸𝔹𝕀𝕁𝕌𝕍𓂀𓂁𓃀𓃁𓇋𓇌𓉐𓉑𓋴𓋵𓎡𓎢𓎣𓏏");
// Non-null-terminated value. The 'x' below is to make sure the strings don't
// null-terminate by happenstance
h1.clear();
constexpr char L1Array[] = {'a','b','c','x'};
const QLatin1StringView nonNullLatin1{L1Array, sizeof(L1Array) - 1}; // abc
constexpr char UTF8Array[] = {0x64, 0x65, 0x66, 0x78};
const QUtf8StringView nonNullUTF8(UTF8Array, sizeof(UTF8Array) - 1); // def
constexpr QChar UTF16Array[] = {'g', 'h', 'i', 'x'};
QStringView nonNullUTF16(UTF16Array, sizeof(UTF16Array) / sizeof(QChar) - 1); // ghi
h1.append(n1, nonNullLatin1);
QCOMPARE(h1.size(), 1);
QVERIFY(h1.has(n1));
QCOMPARE(h1.combinedValue(n1), "abc");
h1.append(n2, nonNullUTF8);
QCOMPARE(h1.size(), 2);
QVERIFY(h1.has(n2));
QCOMPARE(h1.combinedValue(n2), "def");
h1.append(n3, nonNullUTF16);
QCOMPARE(h1.size(), 3);
QVERIFY(h1.has(n3));
QCOMPARE(h1.combinedValue(n3), "ghi");
}
void tst_QHttpHeaders::valueEncoding()
{
// Test that common encodings are possible to set and not blocked by
// header value character filter (ie. don't contain disallowed characters as per RFC 9110)
QHttpHeaders h1;
// Within visible ASCII range
QVERIFY(h1.append(n1, "foo"_ba.toBase64()));
QCOMPARE(h1.values(n1).at(0), "Zm9v");
h1.replace(0, n1, "foo"_ba.toPercentEncoding());
QCOMPARE(h1.values(n1).at(0), "foo");
// Outside of ASCII/Latin-1 range (€)
h1.replace(0, n1, "foo€"_ba.toBase64());
QCOMPARE(h1.values(n1).at(0), "Zm9v4oKs");
h1.replace(0, n1, "foo€"_ba.toPercentEncoding());
QCOMPARE(h1.values(n1).at(0), "foo%E2%82%AC");
}
QTEST_MAIN(tst_QHttpHeaders)
#include "tst_qhttpheaders.moc"