summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMate Barany <[email protected]>2023-11-16 17:06:33 +0100
committerMarc Mutz <[email protected]>2024-05-30 18:52:42 +0000
commit32610561e3e7480ea103e730c11e5e3a9675a54a (patch)
tree6d9c47dc60e0e61e2b60f341b2ec5c5b7838dd59
parenta5953d20e27ab73774058dd06ac514f9310a41e8 (diff)
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 <[email protected]>
-rw-r--r--src/network/CMakeLists.txt2
-rw-r--r--src/network/access/qformdatabuilder.cpp324
-rw-r--r--src/network/access/qformdatabuilder.h124
-rw-r--r--tests/auto/network/access/CMakeLists.txt1
-rw-r--r--tests/auto/network/access/qformdatabuilder/CMakeLists.txt22
-rw-r--r--tests/auto/network/access/qformdatabuilder/document.docxbin0 -> 10548 bytes
-rw-r--r--tests/auto/network/access/qformdatabuilder/image1.jpgbin0 -> 518 bytes
-rw-r--r--tests/auto/network/access/qformdatabuilder/rfc3252.txt1
-rw-r--r--tests/auto/network/access/qformdatabuilder/sheet.xlsxbin0 -> 8534 bytes
-rw-r--r--tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp204
10 files changed, 678 insertions, 0 deletions
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt
index e977400245e..08789d89dec 100644
--- a/src/network/CMakeLists.txt
+++ b/src/network/CMakeLists.txt
@@ -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
diff --git a/src/network/access/qformdatabuilder.cpp b/src/network/access/qformdatabuilder.cpp
new file mode 100644
index 00000000000..8f467e2c576
--- /dev/null
+++ b/src/network/access/qformdatabuilder.cpp
@@ -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
diff --git a/src/network/access/qformdatabuilder.h b/src/network/access/qformdatabuilder.h
new file mode 100644
index 00000000000..1bbf9066cd7
--- /dev/null
+++ b/src/network/access/qformdatabuilder.h
@@ -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
diff --git a/tests/auto/network/access/CMakeLists.txt b/tests/auto/network/access/CMakeLists.txt
index d1130f832e7..ed99aa87460 100644
--- a/tests/auto/network/access/CMakeLists.txt
+++ b/tests/auto/network/access/CMakeLists.txt
@@ -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()
diff --git a/tests/auto/network/access/qformdatabuilder/CMakeLists.txt b/tests/auto/network/access/qformdatabuilder/CMakeLists.txt
new file mode 100644
index 00000000000..dde2dc10e0c
--- /dev/null
+++ b/tests/auto/network/access/qformdatabuilder/CMakeLists.txt
@@ -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
+)
+
diff --git a/tests/auto/network/access/qformdatabuilder/document.docx b/tests/auto/network/access/qformdatabuilder/document.docx
new file mode 100644
index 00000000000..49005f14b6d
--- /dev/null
+++ b/tests/auto/network/access/qformdatabuilder/document.docx
Binary files differ
diff --git a/tests/auto/network/access/qformdatabuilder/image1.jpg b/tests/auto/network/access/qformdatabuilder/image1.jpg
new file mode 100644
index 00000000000..d1d27bf7cfa
--- /dev/null
+++ b/tests/auto/network/access/qformdatabuilder/image1.jpg
Binary files differ
diff --git a/tests/auto/network/access/qformdatabuilder/rfc3252.txt b/tests/auto/network/access/qformdatabuilder/rfc3252.txt
new file mode 100644
index 00000000000..5436ce5b26d
--- /dev/null
+++ b/tests/auto/network/access/qformdatabuilder/rfc3252.txt
@@ -0,0 +1 @@
+some text for reference
diff --git a/tests/auto/network/access/qformdatabuilder/sheet.xlsx b/tests/auto/network/access/qformdatabuilder/sheet.xlsx
new file mode 100644
index 00000000000..2cb1ec73611
--- /dev/null
+++ b/tests/auto/network/access/qformdatabuilder/sheet.xlsx
Binary files differ
diff --git a/tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp b/tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp
new file mode 100644
index 00000000000..70d9b4b1bdc
--- /dev/null
+++ b/tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp
@@ -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"