Add convenience classes to generate QHttpMultipart messages

Constructing and composing a QHttpMultipart contains some aspects that
are possible candidates for automating, such as setting the headers
manually for each included part. As a reference, when issuing a default
multipart with CURL, one does not need to manually set the headers.

Add the class QFormDataPartBuilder to simplify the construction of
QHttpPart objects.

Add the class QFormDataBuilder to simplify the construction of
QHttpMultiPart objects.

[ChangeLog][QtNetwork][QFormDataBuilder] New class to help constructing
multipart/form-data QHttpMultiParts.

Fixes: QTBUG-114647
Change-Id: Ie035dabc01a9818d65a67c239807b50001fd984a
Reviewed-by: Marc Mutz <marc.mutz@qt.io>
This commit is contained in:
Mate Barany 2023-11-16 17:06:33 +01:00 committed by Marc Mutz
parent a5953d20e2
commit 32610561e3
10 changed files with 678 additions and 0 deletions

View File

@ -110,6 +110,7 @@ qt_internal_extend_target(Network CONDITION APPLE
qt_internal_extend_target(Network CONDITION WASM
SOURCES
access/qformdatabuilder.cpp access/qformdatabuilder.h
access/qhttpmultipart.cpp access/qhttpmultipart.h access/qhttpmultipart_p.h
access/qhttpnetworkheader.cpp access/qhttpnetworkheader_p.h
access/qnetworkreplywasmimpl.cpp access/qnetworkreplywasmimpl_p.h
@ -126,6 +127,7 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http
access/http2/huffman.cpp access/http2/huffman_p.h
access/qabstractprotocolhandler.cpp access/qabstractprotocolhandler_p.h
access/qdecompresshelper.cpp access/qdecompresshelper_p.h
access/qformdatabuilder.cpp access/qformdatabuilder.h
access/qhttp1configuration.cpp access/qhttp1configuration.h
access/qhttp2configuration.cpp access/qhttp2configuration.h
access/qhttp2connection.cpp access/qhttp2connection_p.h

View File

@ -0,0 +1,324 @@
// Copyright (C) 2024 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
#include "qformdatabuilder.h"
#if QT_CONFIG(mimetype)
#include "QtCore/qmimedatabase.h"
#endif
QT_BEGIN_NAMESPACE
/*!
\class QFormDataPartBuilder
\brief The QFormDataPartBuilder class is a convenience class to simplify
the construction of QHttpPart objects.
\since 6.8
\ingroup network
\ingroup shared
\inmodule QtNetwork
The QFormDataPartBuilder class can be used to build a QHttpPart object with
the content disposition header set to be form-data by default. Then the
generated object can be used as part of a multipart message (which is
represented by the QHttpMultiPart class).
\sa QHttpPart, QHttpMultiPart, QFormDataBuilder
*/
/*!
Constructs a QFormDataPartBuilder object and sets \a name as the name
parameter of the form-data.
*/
QFormDataPartBuilder::QFormDataPartBuilder(QLatin1StringView name, PrivateConstructor /*unused*/)
{
static_assert(std::is_nothrow_move_constructible_v<decltype(m_body)>);
static_assert(std::is_nothrow_move_assignable_v<decltype(m_body)>);
m_headerValue += "form-data; name=\"";
for (auto c : name) {
if (c == '"' || c == '\\')
m_headerValue += '\\';
m_headerValue += c;
}
m_headerValue += "\"";
}
/*!
\fn QFormDataPartBuilder::QFormDataPartBuilder(QFormDataPartBuilder &&other) noexcept
Move-constructs a QFormDataPartBuilder instance, making it point at the same
object that \a other was pointing to.
*/
/*!
\fn QFormDataPartBuilder &QFormDataPartBuilder::operator=(QFormDataPartBuilder &&other)
Move-assigns \a other to this QFormDataPartBuilder instance.
*/
/*!
Destroys the QFormDataPartBuilder object.
*/
QFormDataPartBuilder::~QFormDataPartBuilder()
= default;
static QByteArray buildFileName(QLatin1StringView view)
{
QByteArray fileName;
fileName += "; filename";
QByteArrayView encoding = "=";
for (uchar c : view) {
if (c > 127) {
encoding = "*=ISO-8859-1''";
break;
}
}
fileName += encoding;
fileName += QByteArray::fromRawData(view.data(), view.size()).toPercentEncoding();
return fileName;
}
static QByteArray buildFileName(QUtf8StringView view)
{
QByteArrayView bv = view;
QByteArray fileName;
fileName += "; filename";
QByteArrayView encoding = "=";
for (uchar c : bv) {
if (c > 127) {
encoding = "*=UTF-8''";
break;
}
}
fileName += encoding;
fileName += QByteArray::fromRawData(bv.data(), bv.size()).toPercentEncoding();
return fileName;
}
static QByteArray buildFileName(QStringView view)
{
QByteArray fileName;
fileName += "; filename";
QByteArrayView encoding = "=";
bool needsUtf8 = false;
for (QChar c : view) {
if (c > u'\xff') {
encoding = "*=UTF-8''";
needsUtf8 = true;
break;
} else if (c > u'\x7f') {
encoding = "*=ISO-8859-1''";
}
}
fileName += encoding;
if (needsUtf8)
fileName += view.toUtf8().toPercentEncoding();
else
fileName += view.toLatin1().toPercentEncoding();
return fileName;
}
QFormDataPartBuilder &QFormDataPartBuilder::setBodyHelper(const QByteArray &data,
QAnyStringView fileName)
{
if (fileName.isEmpty())
m_bodyName = QByteArray();
else
m_bodyName = fileName.visit([&](auto name) { return buildFileName(name); });
m_originalBodyName = fileName.toString();
m_body = data;
return *this;
}
/*!
Sets \a data as the body of this MIME part and, if given, \a fileName as the
file name parameter in the content disposition header.
A subsequent call to setBodyDevice() discards the body and the device will
be used instead.
For a large amount of data (e.g. an image), setBodyDevice() is preferred,
which will not copy the data internally.
\sa setBodyDevice()
*/
QFormDataPartBuilder &QFormDataPartBuilder::setBody(QByteArrayView data,
QAnyStringView fileName)
{
return setBody(data.toByteArray(), fileName);
}
/*!
Sets \a body as the body device of this part and \a fileName as the file
name parameter in the content disposition header.
A subsequent call to setBody() discards the body device and the data set by
setBody() will be used instead.
For large amounts of data this method should be preferred over setBody(),
because the content is not copied when using this method, but read
directly from the device.
\a body must be open and readable. QFormDataPartBuilder does not take
ownership of \a body, i.e. the device must be closed and destroyed if
necessary.
\sa setBody(), QHttpPart::setBodyDevice()
*/
QFormDataPartBuilder &QFormDataPartBuilder::setBodyDevice(QIODevice *body, QAnyStringView fileName)
{
if (fileName.isEmpty())
m_bodyName = QByteArray();
else
m_bodyName = fileName.visit([&](auto name) { return buildFileName(name); });
m_originalBodyName = fileName.toString();
m_body = body;
return *this;
}
/*!
Sets the headers specified in \a headers.
\note The "content-type" and "content-disposition" headers, if any are
specified in \a headers, will be overwritten by the class.
*/
QFormDataPartBuilder &QFormDataPartBuilder::setHeaders(const QHttpHeaders &headers)
{
m_httpHeaders = headers;
return *this;
}
/*!
Generates a QHttpPart and sets the content disposition header as form-data.
When this function called, it uses the MIME database to deduce the type the
body based on its name and then sets the deduced type as the content type
header.
*/
QHttpPart QFormDataPartBuilder::build()
{
QHttpPart httpPart;
if (!m_bodyName.isEmpty())
m_headerValue += m_bodyName; // RFC 5987 Section 3.2.1
#if QT_CONFIG(mimetype)
QMimeDatabase db;
QMimeType mimeType = std::visit([&](auto &arg) {
return db.mimeTypeForFileNameAndData(m_originalBodyName, arg);
}, m_body);
#endif
for (qsizetype i = 0; i < m_httpHeaders.size(); i++) {
httpPart.setRawHeader(QByteArrayView(m_httpHeaders.nameAt(i)).toByteArray(),
m_httpHeaders.valueAt(i).toByteArray());
}
#if QT_CONFIG(mimetype)
httpPart.setHeader(QNetworkRequest::ContentTypeHeader, mimeType.name());
#endif
httpPart.setHeader(QNetworkRequest::ContentDispositionHeader, m_headerValue);
if (auto d = std::get_if<QIODevice*>(&m_body))
httpPart.setBodyDevice(*d);
else if (auto b = std::get_if<QByteArray>(&m_body))
httpPart.setBody(*b);
else
Q_UNREACHABLE();
return httpPart;
}
/*!
\class QFormDataBuilder
\brief The QFormDataBuilder class is a convenience class to simplify
the construction of QHttpMultiPart objects.
\since 6.8
\ingroup network
\ingroup shared
\inmodule QtNetwork
The QFormDataBuilder class can be used to build a QHttpMultiPart object
with the content type set to be FormDataType by default.
\sa QHttpPart, QHttpMultiPart, QFormDataPartBuilder
*/
/*!
Constructs an empty QFormDataBuilder object.
*/
QFormDataBuilder::QFormDataBuilder()
= default;
/*!
Destroys the QFormDataBuilder object.
*/
QFormDataBuilder::~QFormDataBuilder()
= default;
/*!
\fn QFormDataBuilder::QFormDataBuilder(QFormDataBuilder &&other) noexcept
Move-constructs a QFormDataBuilder instance, making it point at the same
object that \a other was pointing to.
*/
/*!
\fn QFormDataBuilder &QFormDataBuilder::operator=(QFormDataBuilder &&other) noexcept
Move-assigns \a other to this QFormDataBuilder instance.
*/
/*!
Constructs and returns a reference to a QFormDataPartBuilder object and sets
\a name as the name parameter of the form-data. The returned reference is
valid until the next call to this function.
\sa QFormDataPartBuilder, QHttpPart
*/
QFormDataPartBuilder &QFormDataBuilder::part(QLatin1StringView name)
{
static_assert(std::is_nothrow_move_constructible_v<decltype(m_parts)>);
static_assert(std::is_nothrow_move_assignable_v<decltype(m_parts)>);
return m_parts.emplace_back(name, QFormDataPartBuilder::PrivateConstructor());
}
/*!
Constructs and returns a pointer to a QHttpMultipart object. The caller
takes ownership of the generated QHttpMultiPart object.
\sa QHttpMultiPart
*/
std::unique_ptr<QHttpMultiPart> QFormDataBuilder::buildMultiPart()
{
auto multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType);
for (auto &part : m_parts)
multiPart->append(part.build());
return multiPart;
}
QT_END_NAMESPACE

View File

@ -0,0 +1,124 @@
// Copyright (C) 2024 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 QFORMDATABUILDER_H
#define QFORMDATABUILDER_H
#include <QtNetwork/qtnetworkglobal.h>
#include <QtNetwork/qhttpheaders.h>
#include <QtNetwork/qhttpmultipart.h>
#include <QtCore/qbytearray.h>
#include <QtCore/qiodevice.h>
#include <QtCore/qstring.h>
#include <memory>
#include <variant>
#include <vector>
#ifndef Q_OS_WASM
QT_REQUIRE_CONFIG(http);
#endif
class tst_QFormDataBuilder;
QT_BEGIN_NAMESPACE
class QHttpPartPrivate;
class QHttpMultiPart;
class QDebug;
class QFormDataPartBuilder
{
struct PrivateConstructor { explicit PrivateConstructor() = default; };
public:
Q_NETWORK_EXPORT explicit QFormDataPartBuilder(QLatin1StringView name, PrivateConstructor);
QFormDataPartBuilder(QFormDataPartBuilder &&other) noexcept
: m_headerValue(std::move(other.m_headerValue)),
m_bodyName(std::move(other.m_bodyName)),
m_originalBodyName(std::move(other.m_originalBodyName)),
m_httpHeaders(std::move(other.m_httpHeaders)),
m_body(std::move(other.m_body)),
m_reserved(std::exchange(other.m_reserved, nullptr))
{
}
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QFormDataPartBuilder)
void swap(QFormDataPartBuilder &other) noexcept
{
m_headerValue.swap(other.m_headerValue);
m_bodyName.swap(other.m_bodyName);
m_originalBodyName.swap(other.m_originalBodyName);
m_httpHeaders.swap(other.m_httpHeaders);
m_body.swap(other.m_body);
qt_ptr_swap(m_reserved, other.m_reserved);
}
Q_NETWORK_EXPORT ~QFormDataPartBuilder();
Q_WEAK_OVERLOAD QFormDataPartBuilder &setBody(const QByteArray &data,
QAnyStringView fileName = {})
{ return setBodyHelper(data, fileName); }
Q_NETWORK_EXPORT QFormDataPartBuilder &setBody(QByteArrayView data,
QAnyStringView fileName = {});
Q_NETWORK_EXPORT QFormDataPartBuilder &setBodyDevice(QIODevice *body,
QAnyStringView fileName = {});
Q_NETWORK_EXPORT QFormDataPartBuilder &setHeaders(const QHttpHeaders &headers);
private:
Q_DISABLE_COPY(QFormDataPartBuilder)
Q_NETWORK_EXPORT QFormDataPartBuilder &setBodyHelper(const QByteArray &data,
QAnyStringView fileName = {});
Q_NETWORK_EXPORT QHttpPart build();
QByteArray m_headerValue;
QByteArray m_bodyName;
QString m_originalBodyName;
QHttpHeaders m_httpHeaders;
std::variant<QIODevice*, QByteArray> m_body;
void *m_reserved = nullptr;
friend class QFormDataBuilder;
friend class ::tst_QFormDataBuilder;
friend void swap(QFormDataPartBuilder &lhs, QFormDataPartBuilder &rhs) noexcept
{ lhs.swap(rhs); }
};
class QFormDataBuilder
{
public:
Q_NETWORK_EXPORT explicit QFormDataBuilder();
QFormDataBuilder(QFormDataBuilder &&other) noexcept
: m_parts(std::move(other.m_parts)),
m_reserved(std::exchange(other.m_reserved, nullptr))
{
}
QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QFormDataBuilder)
void swap(QFormDataBuilder &other) noexcept
{
m_parts.swap(other.m_parts);
qt_ptr_swap(m_reserved, other.m_reserved);
}
Q_NETWORK_EXPORT ~QFormDataBuilder();
Q_NETWORK_EXPORT QFormDataPartBuilder &part(QLatin1StringView name);
Q_NETWORK_EXPORT std::unique_ptr<QHttpMultiPart> buildMultiPart();
private:
std::vector<QFormDataPartBuilder> m_parts;
void *m_reserved = nullptr;
friend void swap(QFormDataBuilder &lhs, QFormDataBuilder &rhs) noexcept
{ lhs.swap(rhs); }
Q_DISABLE_COPY(QFormDataBuilder)
};
QT_END_NAMESPACE
#endif // QFORMDATABUILDER_H

View File

@ -14,6 +14,7 @@ add_subdirectory(qnetworkcachemetadata)
add_subdirectory(qabstractnetworkcache)
if(QT_FEATURE_http)
add_subdirectory(qnetworkreply_local)
add_subdirectory(qformdatabuilder)
add_subdirectory(qnetworkrequestfactory)
add_subdirectory(qrestaccessmanager)
endif()

View File

@ -0,0 +1,22 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
cmake_minimum_required(VERSION 3.16)
project(tst_qformdatabuilder LANGUAGES CXX)
find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
endif()
qt_internal_add_test(tst_qformdatabuilder
SOURCES
tst_qformdatabuilder.cpp
LIBRARIES
Qt::Core
Qt::Network
TESTDATA
rfc3252.txt
image1.jpg
document.docx
sheet.xlsx
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

View File

@ -0,0 +1 @@
some text for reference

Binary file not shown.

View File

@ -0,0 +1,204 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtNetwork/qformdatabuilder.h>
#include <QtCore/qbuffer.h>
#include <QtCore/qfile.h>
#include <QtTest/qtest.h>
using namespace Qt::StringLiterals;
class tst_QFormDataBuilder : public QObject
{
Q_OBJECT
private Q_SLOTS:
void generateQHttpPartWithDevice_data();
void generateQHttpPartWithDevice();
void escapesBackslashAndQuotesInFilenameAndName_data();
void escapesBackslashAndQuotesInFilenameAndName();
void picksUtf8EncodingOnlyIfL1OrAsciiDontSuffice_data();
void picksUtf8EncodingOnlyIfL1OrAsciiDontSuffice();
};
void tst_QFormDataBuilder::generateQHttpPartWithDevice_data()
{
QTest::addColumn<QLatin1StringView>("name_data");
QTest::addColumn<QString>("real_file_name");
QTest::addColumn<QString>("body_name_data");
QTest::addColumn<QByteArray>("expected_content_type_data");
QTest::addColumn<QByteArray>("expected_content_disposition_data");
QTest::newRow("txt-ascii") << "text"_L1 << "rfc3252.txt" << "rfc3252.txt" << "text/plain"_ba
<< "form-data; name=\"text\"; filename=rfc3252.txt"_ba;
QTest::newRow("txt-latin") << "text"_L1 << "rfc3252.txt" << "szöveg.txt" << "text/plain"_ba
<< "form-data; name=\"text\"; filename*=ISO-8859-1''sz%F6veg.txt"_ba;
QTest::newRow("txt-unicode") << "text"_L1 << "rfc3252.txt" << "テキスト.txt" << "text/plain"_ba
<< "form-data; name=\"text\"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.txt"_ba;
QTest::newRow("jpg-ascii") << "image"_L1 << "image1.jpg" << "image1.jpg" << "image/jpeg"_ba
<< "form-data; name=\"image\"; filename=image1.jpg"_ba;
QTest::newRow("jpg-latin") << "image"_L1 << "image1.jpg" << "kép.jpg" << "image/jpeg"_ba
<< "form-data; name=\"image\"; filename*=ISO-8859-1''k%E9p.jpg"_ba;
QTest::newRow("jpg-unicode") << "image"_L1 << "image1.jpg" << "絵.jpg" << "image/jpeg"_ba
<< "form-data; name=\"image\"; filename*=UTF-8''%E7%B5%B5"_ba;
QTest::newRow("doc-ascii") << "text"_L1 << "document.docx" << "word.docx"
<< "application/vnd.openxmlformats-officedocument.wordprocessingml.document"_ba
<< "form-data; name=\"text\"; filename=word.docx"_ba;
QTest::newRow("doc-latin") << "text"_L1 << "document.docx" << "szöveg.docx"
<< "application/vnd.openxmlformats-officedocument.wordprocessingml.document"_ba
<< "form-data; name=\"text\"; filename*=ISO-8859-1''sz%F6veg.docx"_ba;
QTest::newRow("doc-unicode") << "text"_L1 << "document.docx" << "テキスト.docx"
<< "application/vnd.openxmlformats-officedocument.wordprocessingml.document"_ba
<< "form-data; name=\"text\"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.docx"_ba;
QTest::newRow("xls-ascii") << "spreadsheet"_L1 << "sheet.xlsx" << "sheet.xlsx"
<< "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"_ba
<< "form-data; name=\"spreadsheet\"; filename=sheet.xlsx"_ba;
QTest::newRow("xls-latin") << "spreadsheet"_L1 << "sheet.xlsx" << "szöveg.xlsx"
<< "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"_ba
<< "form-data; name=\"spreadsheet\"; filename*=ISO-8859-1''sz%F6veg.xlsx"_ba;
QTest::newRow("xls-unicode") << "spreadsheet"_L1 << "sheet.xlsx" << "テキスト.xlsx"
<< "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"_ba
<< "form-data; name=\"spreadsheet\"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.xlsx"_ba;
}
void tst_QFormDataBuilder::generateQHttpPartWithDevice()
{
QFETCH(const QLatin1StringView, name_data);
QFETCH(const QString, real_file_name);
QFETCH(const QString, body_name_data);
QFETCH(const QByteArray, expected_content_type_data);
QFETCH(const QByteArray, expected_content_disposition_data);
QString testData = QFileInfo(QFINDTESTDATA(real_file_name)).absoluteFilePath();
QFile data_file(testData);
QHttpPart httpPart = QFormDataPartBuilder(name_data, QFormDataPartBuilder::PrivateConstructor())
.setBodyDevice(&data_file, body_name_data)
.build();
QByteArray msg;
{
QBuffer buf(&msg);
QVERIFY(buf.open(QIODevice::WriteOnly));
QDebug debug(&buf);
debug << httpPart;
}
QVERIFY(msg.contains(expected_content_type_data));
QVERIFY(msg.contains(expected_content_disposition_data));
}
void tst_QFormDataBuilder::escapesBackslashAndQuotesInFilenameAndName_data()
{
QTest::addColumn<QLatin1StringView>("name_data");
QTest::addColumn<QString>("body_name_data");
QTest::addColumn<QByteArray>("expected_content_type_data");
QTest::addColumn<QByteArray>("expected_content_disposition_data");
QTest::newRow("quote") << "t\"ext"_L1 << "rfc3252.txt" << "text/plain"_ba
<< R"(form-data; name="t\"ext"; filename=rfc3252.txt)"_ba;
QTest::newRow("slash") << "t\\ext"_L1 << "rfc3252.txt" << "text/plain"_ba
<< R"(form-data; name="t\\ext"; filename=rfc3252.txt)"_ba;
QTest::newRow("quotes") << "t\"e\"xt"_L1 << "rfc3252.txt" << "text/plain"_ba
<< R"(form-data; name="t\"e\"xt"; filename=rfc3252.txt)"_ba;
QTest::newRow("slashes") << "t\\\\ext"_L1 << "rfc3252.txt" << "text/plain"_ba
<< R"(form-data; name="t\\\\ext"; filename=rfc3252.txt)"_ba;
QTest::newRow("quote-slash") << "t\"ex\\t"_L1 << "rfc3252.txt" << "text/plain"_ba
<< R"(form-data; name="t\"ex\\t"; filename=rfc3252.txt)"_ba;
QTest::newRow("quotes-slashes") << "t\"e\"x\\t\\"_L1 << "rfc3252.txt" << "text/plain"_ba
<< R"(form-data; name="t\"e\"x\\t\\"; filename=rfc3252.txt)"_ba;
}
void tst_QFormDataBuilder::escapesBackslashAndQuotesInFilenameAndName()
{
QFETCH(const QLatin1StringView, name_data);
QFETCH(const QString, body_name_data);
QFETCH(const QByteArray, expected_content_type_data);
QFETCH(const QByteArray, expected_content_disposition_data);
QFile dummy_file(body_name_data);
QHttpPart httpPart = QFormDataPartBuilder(name_data, QFormDataPartBuilder::PrivateConstructor())
.setBodyDevice(&dummy_file, body_name_data)
.build();
QByteArray msg;
{
QBuffer buf(&msg);
QVERIFY(buf.open(QIODevice::WriteOnly));
QDebug debug(&buf);
debug << httpPart;
}
QVERIFY(msg.contains(expected_content_type_data));
QVERIFY(msg.contains(expected_content_disposition_data));
}
void tst_QFormDataBuilder::picksUtf8EncodingOnlyIfL1OrAsciiDontSuffice_data()
{
QTest::addColumn<QLatin1StringView>("name_data");
QTest::addColumn<QAnyStringView>("body_name_data");
QTest::addColumn<QByteArray>("expected_content_type_data");
QTest::addColumn<QByteArray>("expected_content_disposition_data");
QTest::newRow("latin1-ascii") << "text"_L1 << QAnyStringView("rfc3252.txt"_L1) << "text/plain"_ba
<< "form-data; name=\"text\"; filename=rfc3252.txt"_ba;
QTest::newRow("u8-ascii") << "text"_L1 << QAnyStringView(u8"rfc3252.txt") << "text/plain"_ba
<< "form-data; name=\"text\"; filename=rfc3252.txt"_ba;
QTest::newRow("u-ascii") << "text"_L1 << QAnyStringView(u"rfc3252.txt") << "text/plain"_ba
<< "form-data; name=\"text\"; filename=rfc3252.txt"_ba;
QTest::newRow("latin1-latin") << "text"_L1 << QAnyStringView("sz\366veg.txt"_L1) << "text/plain"_ba
<< "form-data; name=\"text\"; filename*=ISO-8859-1''sz%F6veg.txt"_ba;
QTest::newRow("u8-latin") << "text"_L1 << QAnyStringView(u8"szöveg.txt") << "text/plain"_ba
<< "form-data; name=\"text\"; filename*=ISO-8859-1''sz%F6veg.txt"_ba;
QTest::newRow("u-latin") << "text"_L1 << QAnyStringView(u"szöveg.txt") << "text/plain"_ba
<< "form-data; name=\"text\"; filename*=ISO-8859-1''sz%F6veg.txt"_ba;
QTest::newRow("u8-u8") << "text"_L1 << QAnyStringView(u8"テキスト.txt") << "text/plain"_ba
<< "form-data; name=\"text\"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.txt"_ba;
}
void tst_QFormDataBuilder::picksUtf8EncodingOnlyIfL1OrAsciiDontSuffice()
{
QFETCH(const QLatin1StringView, name_data);
QFETCH(const QAnyStringView, body_name_data);
QFETCH(const QByteArray, expected_content_type_data);
QFETCH(const QByteArray, expected_content_disposition_data);
QBuffer buff;
QHttpPart httpPart = QFormDataPartBuilder(name_data, QFormDataPartBuilder::PrivateConstructor())
.setBodyDevice(&buff, body_name_data)
.build();
QByteArray msg;
{
QBuffer buf(&msg);
QVERIFY(buf.open(QIODevice::WriteOnly));
QDebug debug(&buf);
debug << httpPart;
}
QVERIFY(msg.contains(expected_content_type_data));
QEXPECT_FAIL("u8-latin", "will be fixed in subsequent patch", Continue);
QVERIFY(msg.contains(expected_content_disposition_data));
}
QTEST_MAIN(tst_QFormDataBuilder)
#include "tst_qformdatabuilder.moc"