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); + static_assert(std::is_nothrow_move_assignable_v); + + 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(&m_body)) + httpPart.setBodyDevice(*d); + else if (auto b = std::get_if(&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); + static_assert(std::is_nothrow_move_assignable_v); + + 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 QFormDataBuilder::buildMultiPart() +{ + auto multiPart = std::make_unique(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 +#include +#include + +#include +#include +#include + +#include +#include +#include + +#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 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 buildMultiPart(); +private: + std::vector 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 Binary files /dev/null and b/tests/auto/network/access/qformdatabuilder/document.docx 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 Binary files /dev/null and b/tests/auto/network/access/qformdatabuilder/image1.jpg 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 Binary files /dev/null and b/tests/auto/network/access/qformdatabuilder/sheet.xlsx 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 + +#include +#include + +#include + +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("name_data"); + QTest::addColumn("real_file_name"); + QTest::addColumn("body_name_data"); + QTest::addColumn("expected_content_type_data"); + QTest::addColumn("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("name_data"); + QTest::addColumn("body_name_data"); + QTest::addColumn("expected_content_type_data"); + QTest::addColumn("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("name_data"); + QTest::addColumn("body_name_data"); + QTest::addColumn("expected_content_type_data"); + QTest::addColumn("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"