diff --git a/src/network/access/qformdatabuilder.cpp b/src/network/access/qformdatabuilder.cpp index 9cde45cddde..2cb5cc1f0e8 100644 --- a/src/network/access/qformdatabuilder.cpp +++ b/src/network/access/qformdatabuilder.cpp @@ -66,27 +66,6 @@ QFormDataPartBuilder::QFormDataPartBuilder(QLatin1StringView name, PrivateConstr QFormDataPartBuilder::~QFormDataPartBuilder() = default; - -static auto encodeFileName(QStringView view) -{ - struct R { QByteArrayView encoding; QByteArray encoded; }; - - 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''"; - } - } - - return R{encoding, needsUtf8 ? view.toUtf8() : view.toLatin1()}; -} - static void convertInto_impl(QByteArray &dst, QUtf8StringView in) { dst.clear(); @@ -199,9 +178,23 @@ QHttpPart QFormDataPartBuilder::build() QHttpPart httpPart; if (!m_originalBodyName.isNull()) { - const auto enc = encodeFileName(m_originalBodyName); - m_headerValue += "; filename" + enc.encoding - + enc.encoded.toPercentEncoding(); // RFC 5987 Section 3.2.1 + const bool utf8 = !QtPrivate::isAscii(m_originalBodyName); + const auto enc = utf8 ? m_originalBodyName.toUtf8() : m_originalBodyName.toLatin1(); + m_headerValue += "; filename=\""; + for (auto c : enc) { + if (c == '"' || c == '\\') + m_headerValue += '\\'; + m_headerValue += c; + } + m_headerValue += "\""; + if (utf8) { + // For 'filename*' production see + // https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1 + // For providing both filename and filename* parameters see + // https://datatracker.ietf.org/doc/html/rfc6266#section-4.3 and + // https://datatracker.ietf.org/doc/html/rfc8187#section-4.2 + m_headerValue += "; filename*=UTF-8''" + enc.toPercentEncoding(); + } } #if QT_CONFIG(mimetype) diff --git a/tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp b/tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp index 8bb0396ea1c..2dd86e7be58 100644 --- a/tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp +++ b/tests/auto/network/access/qformdatabuilder/tst_qformdatabuilder.cpp @@ -21,8 +21,8 @@ private Q_SLOTS: void escapesBackslashAndQuotesInFilenameAndName_data(); void escapesBackslashAndQuotesInFilenameAndName(); - void picksUtf8EncodingOnlyIfL1OrAsciiDontSuffice_data(); - void picksUtf8EncodingOnlyIfL1OrAsciiDontSuffice(); + void picksUtf8FilenameEncodingIfAsciiDontSuffice_data(); + void picksUtf8FilenameEncodingIfAsciiDontSuffice(); void setHeadersDoesNotAffectHeaderFieldsManagedByBuilder_data(); void setHeadersDoesNotAffectHeaderFieldsManagedByBuilder(); @@ -38,40 +38,53 @@ void tst_QFormDataBuilder::generateQHttpPartWithDevice_data() QTest::addColumn("body_name_data"); QTest::addColumn("expected_content_type_data"); QTest::addColumn("expected_content_disposition_data"); + QTest::addColumn("content_disposition_must_not_contain_data"); QTest::newRow("txt-ascii") << "text"_L1 << u"rfc3252.txt"_s << u"rfc3252.txt"_s << u"text/plain"_s - << uR"(form-data; name="text"; filename=rfc3252.txt)"_s; + << uR"(form-data; name="text"; filename="rfc3252.txt")"_s + << u"filename*"_s; QTest::newRow("txt-latin") << "text"_L1 << u"rfc3252.txt"_s << u"szöveg.txt"_s << u"text/plain"_s - << uR"(form-data; name="text"; filename*=ISO-8859-1''sz%F6veg.txt)"_s; + << uR"(form-data; name="text"; filename="szöveg.txt"; filename*=UTF-8''sz%C3%B6veg.txt)"_s + << u""_s; QTest::newRow("txt-unicode") << "text"_L1 << u"rfc3252.txt"_s << u"テキスト.txt"_s << u"text/plain"_s - << uR"(form-data; name="text"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.txt)"_s; + << uR"(form-data; name="text"; filename="テキスト.txt"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.txt)"_s + << u""_s; QTest::newRow("jpg-ascii") << "image"_L1 << u"image1.jpg"_s << u"image1.jpg"_s << u"image/jpeg"_s - << uR"(form-data; name="image"; filename=image1.jpg)"_s; + << uR"(form-data; name="image"; filename="image1.jpg")"_s + << u"filename*"_s; QTest::newRow("jpg-latin") << "image"_L1 << u"image1.jpg"_s << u"kép.jpg"_s << u"image/jpeg"_s - << uR"(form-data; name="image"; filename*=ISO-8859-1''k%E9p.jpg)"_s; + << uR"(form-data; name="image"; filename="kép.jpg"; filename*=UTF-8''k%C3%A9p.jpg)"_s + << u""_s; QTest::newRow("jpg-unicode") << "image"_L1 << u"image1.jpg"_s << u"絵.jpg"_s << u"image/jpeg"_s - << uR"(form-data; name="image"; filename*=UTF-8''%E7%B5%B5)"_s; + << uR"(form-data; name="image"; filename="絵.jpg"; filename*=UTF-8''%E7%B5%B5.jpg)"_s + << u""_s; QTest::newRow("doc-ascii") << "text"_L1 << u"document.docx"_s << u"word.docx"_s << u"application/vnd.openxmlformats-officedocument.wordprocessingml.document"_s - << uR"(form-data; name="text"; filename=word.docx)"_s; + << uR"(form-data; name="text"; filename="word.docx")"_s + << u"filename*"_s; QTest::newRow("doc-latin") << "text"_L1 << u"document.docx"_s << u"szöveg.docx"_s << u"application/vnd.openxmlformats-officedocument.wordprocessingml.document"_s - << uR"(form-data; name="text"; filename*=ISO-8859-1''sz%F6veg.docx)"_s; + << uR"(form-data; name="text"; filename="szöveg.docx"; filename*=UTF-8''sz%C3%B6veg.docx)"_s + << u""_s; QTest::newRow("doc-unicode") << "text"_L1 << u"document.docx"_s << u"テキスト.docx"_s << u"application/vnd.openxmlformats-officedocument.wordprocessingml.document"_s - << uR"(form-data; name="text"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.docx)"_s; + << uR"(form-data; name="text"; filename="テキスト.docx"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.docx)"_s + << u""_s; QTest::newRow("xls-ascii") << "spreadsheet"_L1 << u"sheet.xlsx"_s << u"sheet.xlsx"_s << u"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"_s - << uR"(form-data; name="spreadsheet"; filename=sheet.xlsx)"_s; + << uR"(form-data; name="spreadsheet"; filename="sheet.xlsx")"_s + << u"filename*"_s; QTest::newRow("xls-latin") << "spreadsheet"_L1 << u"sheet.xlsx"_s << u"szöveg.xlsx"_s << u"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"_s - << uR"(form-data; name="spreadsheet"; filename*=ISO-8859-1''sz%F6veg.xlsx)"_s; + << uR"(form-data; name="spreadsheet"; filename="szöveg.xlsx"; filename*=UTF-8''sz%C3%B6veg.xlsx)"_s + << u""_s; QTest::newRow("xls-unicode") << "spreadsheet"_L1 << u"sheet.xlsx"_s << u"テキスト.xlsx"_s << u"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"_s - << uR"(form-data; name="spreadsheet"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.xlsx)"_s; + << uR"(form-data; name="spreadsheet"; filename="テキスト.xlsx"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.xlsx)"_s + << u""_s; } void tst_QFormDataBuilder::generateQHttpPartWithDevice() @@ -81,6 +94,7 @@ void tst_QFormDataBuilder::generateQHttpPartWithDevice() QFETCH(const QString, body_name_data); QFETCH(const QString, expected_content_type_data); QFETCH(const QString, expected_content_disposition_data); + QFETCH(const QString, content_disposition_must_not_contain_data); QString testData = QFileInfo(QFINDTESTDATA(real_file_name)).absoluteFilePath(); QFile data_file(testData); @@ -92,6 +106,8 @@ void tst_QFormDataBuilder::generateQHttpPartWithDevice() const auto msg = QDebug::toString(httpPart); QVERIFY(msg.contains(expected_content_type_data)); QVERIFY(msg.contains(expected_content_disposition_data)); + if (!content_disposition_must_not_contain_data.isEmpty()) + QVERIFY(!msg.contains(content_disposition_must_not_contain_data)); } void tst_QFormDataBuilder::escapesBackslashAndQuotesInFilenameAndName_data() @@ -101,23 +117,23 @@ void tst_QFormDataBuilder::escapesBackslashAndQuotesInFilenameAndName_data() QTest::addColumn("expected_content_type_data"); QTest::addColumn("expected_content_disposition_data"); - QTest::newRow("quote") << "t\"ext"_L1 << "rfc3252.txt" << u"text/plain"_s - << uR"(form-data; name="t\"ext"; filename=rfc3252.txt)"_s; + QTest::newRow("quote") << "t\"ext"_L1 << uR"(rfc32"52.txt)"_s << u"text/plain"_s + << uR"(form-data; name="t\"ext"; filename="rfc32\"52.txt")"_s; - QTest::newRow("slash") << "t\\ext"_L1 << "rfc3252.txt" << u"text/plain"_s - << uR"(form-data; name="t\\ext"; filename=rfc3252.txt)"_s; + QTest::newRow("slash") << "t\\ext"_L1 << uR"(rfc32\52.txt)"_s << u"text/plain"_s + << uR"(form-data; name="t\\ext"; filename="rfc32\\52.txt")"_s; - QTest::newRow("quotes") << "t\"e\"xt"_L1 << "rfc3252.txt" << u"text/plain"_s - << uR"(form-data; name="t\"e\"xt"; filename=rfc3252.txt)"_s; + QTest::newRow("quotes") << "t\"e\"xt"_L1 << uR"(rfc3"25"2.txt)"_s << u"text/plain"_s + << uR"(form-data; name="t\"e\"xt"; filename="rfc3\"25\"2.txt")"_s; - QTest::newRow("slashes") << "t\\\\ext"_L1 << "rfc3252.txt" << u"text/plain"_s - << uR"(form-data; name="t\\\\ext"; filename=rfc3252.txt)"_s; + QTest::newRow("slashes") << "t\\\\ext"_L1 << uR"(rfc32\\52.txt)"_s << u"text/plain"_s + << uR"(form-data; name="t\\\\ext"; filename="rfc32\\\\52.txt")"_s; - QTest::newRow("quote-slash") << "t\"ex\\t"_L1 << "rfc3252.txt" << u"text/plain"_s - << uR"(form-data; name="t\"ex\\t"; filename=rfc3252.txt)"_s; + QTest::newRow("quote-slash") << "t\"ex\\t"_L1 << uR"(rfc"32\52.txt)"_s << u"text/plain"_s + << uR"(form-data; name="t\"ex\\t"; filename="rfc\"32\\52.txt")"_s; - QTest::newRow("quotes-slashes") << "t\"e\"x\\t\\"_L1 << "rfc3252.txt" << u"text/plain"_s - << uR"(form-data; name="t\"e\"x\\t\\"; filename=rfc3252.txt)"_s; + QTest::newRow("quotes-slashes") << "t\"e\"x\\t\\"_L1 << uR"(r"f"c3\2\52.txt)"_s << u"text/plain"_s + << uR"(form-data; name="t\"e\"x\\t\\"; filename="r\"f\"c3\\2\\52.txt")"_s; } void tst_QFormDataBuilder::escapesBackslashAndQuotesInFilenameAndName() @@ -138,47 +154,63 @@ void tst_QFormDataBuilder::escapesBackslashAndQuotesInFilenameAndName() QVERIFY(msg.contains(expected_content_disposition_data)); } -void tst_QFormDataBuilder::picksUtf8EncodingOnlyIfL1OrAsciiDontSuffice_data() +void tst_QFormDataBuilder::picksUtf8FilenameEncodingIfAsciiDontSuffice_data() { QTest::addColumn("name_data"); QTest::addColumn("body_name_data"); QTest::addColumn("expected_content_type_data"); QTest::addColumn("expected_content_disposition_data"); + QTest::addColumn("content_disposition_must_not_contain_data"); QTest::newRow("latin1-ascii") << "text"_L1 << QAnyStringView("rfc3252.txt"_L1) << u"text/plain"_s - << uR"(form-data; name="text"; filename=rfc3252.txt)"_s; + << uR"(form-data; name="text"; filename="rfc3252.txt")"_s + << u"filename*"_s; QTest::newRow("u8-ascii") << "text"_L1 << QAnyStringView(u8"rfc3252.txt") << u"text/plain"_s - << uR"(form-data; name="text"; filename=rfc3252.txt)"_s; + << uR"(form-data; name="text"; filename="rfc3252.txt")"_s + << u"filename*"_s; QTest::newRow("u-ascii") << "text"_L1 << QAnyStringView(u"rfc3252.txt") << u"text/plain"_s - << uR"(form-data; name="text"; filename=rfc3252.txt)"_s; + << uR"(form-data; name="text"; filename="rfc3252.txt")"_s + << u"filename*"_s; - QTest::newRow("latin1-latin") << "text"_L1 << QAnyStringView("sz\366veg.txt"_L1) << u"text/plain"_s - << uR"(form-data; name="text"; filename*=ISO-8859-1''sz%F6veg.txt)"_s; + // 0xF6 is 'ö', use hex value with Latin-1 to avoid interpretation as UTF-8 (0xC3 0xB6) + QTest::newRow("latin1-latin") << "text"_L1 << QAnyStringView("sz\xF6veg.txt"_L1) << u"text/plain"_s + << uR"(form-data; name="text"; filename="szöveg.txt"; filename*=UTF-8''sz%C3%B6veg.txt)"_s + << u""_s; QTest::newRow("u8-latin") << "text"_L1 << QAnyStringView(u8"szöveg.txt") << u"text/plain"_s - << uR"(form-data; name="text"; filename*=ISO-8859-1''sz%F6veg.txt)"_s; + << uR"(form-data; name="text"; filename="szöveg.txt"; filename*=UTF-8''sz%C3%B6veg.txt)"_s + << u""_s; QTest::newRow("u-latin") << "text"_L1 << QAnyStringView(u"szöveg.txt") << u"text/plain"_s - << uR"(form-data; name="text"; filename*=ISO-8859-1''sz%F6veg.txt)"_s; + << uR"(form-data; name="text"; filename="szöveg.txt"; filename*=UTF-8''sz%C3%B6veg.txt)"_s + << u""_s; QTest::newRow("u8-u8") << "text"_L1 << QAnyStringView(u8"テキスト.txt") << u"text/plain"_s - << uR"(form-data; name="text"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.txt)"_s; + << uR"(form-data; name="text"; filename="テキスト.txt"; filename*=UTF-8''%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88.txt)"_s + << u""_s; } -void tst_QFormDataBuilder::picksUtf8EncodingOnlyIfL1OrAsciiDontSuffice() +void tst_QFormDataBuilder::picksUtf8FilenameEncodingIfAsciiDontSuffice() { QFETCH(const QLatin1StringView, name_data); QFETCH(const QAnyStringView, body_name_data); QFETCH(const QString, expected_content_type_data); QFETCH(const QString, expected_content_disposition_data); + QFETCH(const QString, content_disposition_must_not_contain_data); QBuffer buff; - QFormDataBuilder qfdb; QFormDataPartBuilder &qfdpb = qfdb.part(name_data).setBodyDevice(&buff, body_name_data); const QHttpPart httpPart = qfdpb.build(); const auto msg = QDebug::toString(httpPart); - QVERIFY(msg.contains(expected_content_type_data)); - QVERIFY(msg.contains(expected_content_disposition_data)); + QVERIFY2(msg.contains(expected_content_type_data), + qPrintable(u"content-type not found : "_s + expected_content_type_data)); + QVERIFY2(msg.contains(expected_content_disposition_data), + qPrintable(u"content-disposition not found : "_s + expected_content_disposition_data)); + if (!content_disposition_must_not_contain_data.isEmpty()) { + QVERIFY2(!msg.contains(content_disposition_must_not_contain_data), + qPrintable(u"content-disposition contained data it shouldn't : "_s + + content_disposition_must_not_contain_data)); + } } void tst_QFormDataBuilder::setHeadersDoesNotAffectHeaderFieldsManagedByBuilder_data() @@ -193,21 +225,21 @@ void tst_QFormDataBuilder::setHeadersDoesNotAffectHeaderFieldsManagedByBuilder_d << "text"_L1 << QAnyStringView("rfc3252.txt"_L1) << false << false << QStringList{ - uR"("content-disposition":"form-data; name=\"text\"; filename=rfc3252.txt")"_s, + uR"("content-disposition":"form-data; name=\"text\"; filename=\"rfc3252.txt\"")"_s, uR"("content-type":"text/plain")"_s}; QTest::newRow("default-overwrites-preset-content-disposition") << "text"_L1 << QAnyStringView("rfc3252.txt"_L1) << true << false << QStringList{ - uR"("content-disposition":"form-data; name=\"text\"; filename=rfc3252.txt")"_s, + uR"("content-disposition":"form-data; name=\"text\"; filename=\"rfc3252.txt\"")"_s, uR"("content-type":"text/plain")"_s}; QTest::newRow("added-extra-header") << "text"_L1 << QAnyStringView("rfc3252.txt"_L1) << false << true << QStringList{ - uR"("content-disposition":"form-data; name=\"text\"; filename=rfc3252.txt")"_s, + uR"("content-disposition":"form-data; name=\"text\"; filename=\"rfc3252.txt\"")"_s, uR"("content-type":"text/plain")"_s, uR"("content-length":"70")"_s}; @@ -215,7 +247,7 @@ void tst_QFormDataBuilder::setHeadersDoesNotAffectHeaderFieldsManagedByBuilder_d << "text"_L1 << QAnyStringView("rfc3252.txt"_L1) << true << true << QStringList{ - uR"("content-disposition":"form-data; name=\"text\"; filename=rfc3252.txt")"_s, + uR"("content-disposition":"form-data; name=\"text\"; filename=\"rfc3252.txt\"")"_s, uR"("content-type":"text/plain")"_s, uR"("content-length":"70")"_s}; }