diff options
author | Mate Barany <[email protected]> | 2023-11-16 17:06:33 +0100 |
---|---|---|
committer | Marc Mutz <[email protected]> | 2024-05-30 18:52:42 +0000 |
commit | 32610561e3e7480ea103e730c11e5e3a9675a54a (patch) | |
tree | 6d9c47dc60e0e61e2b60f341b2ec5c5b7838dd59 | |
parent | a5953d20e27ab73774058dd06ac514f9310a41e8 (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.txt | 2 | ||||
-rw-r--r-- | src/network/access/qformdatabuilder.cpp | 324 | ||||
-rw-r--r-- | src/network/access/qformdatabuilder.h | 124 | ||||
-rw-r--r-- | tests/auto/network/access/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/network/access/qformdatabuilder/CMakeLists.txt | 22 | ||||
-rw-r--r-- | tests/auto/network/access/qformdatabuilder/document.docx | bin | 0 -> 10548 bytes | |||
-rw-r--r-- | tests/auto/network/access/qformdatabuilder/image1.jpg | bin | 0 -> 518 bytes | |||
-rw-r--r-- | tests/auto/network/access/qformdatabuilder/rfc3252.txt | 1 | ||||
-rw-r--r-- | tests/auto/network/access/qformdatabuilder/sheet.xlsx | bin | 0 -> 8534 bytes | |||
-rw-r--r-- | tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp | 204 |
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 Binary files differnew file mode 100644 index 00000000000..49005f14b6d --- /dev/null +++ b/tests/auto/network/access/qformdatabuilder/document.docx diff --git a/tests/auto/network/access/qformdatabuilder/image1.jpg b/tests/auto/network/access/qformdatabuilder/image1.jpg Binary files differnew file mode 100644 index 00000000000..d1d27bf7cfa --- /dev/null +++ b/tests/auto/network/access/qformdatabuilder/image1.jpg 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 Binary files differnew file mode 100644 index 00000000000..2cb1ec73611 --- /dev/null +++ b/tests/auto/network/access/qformdatabuilder/sheet.xlsx 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" |