Adjust multipart 'filename' parameter encoding
The RFCs around these encodings are loose and allow several ways for dealing with non-ASCII characters. The encoding introduced in this commit should be interoperable and aligns with other frameworks (checked Curl, JS FormData, Postman, and Python requests). This consists of several adjustments: 1. Enclose the filename attribute in double quotes 2. If filename contains only ASCII characters, use them as-is 3. If filename contains characters beyond ASCII: 3.1. Encode them directly as raw UTF-8 to filename= 3.2. Set an additional filename*= parameter with percent encoded UTF-8. This is a legacy encoding for compatibility. Task-number: QTBUG-125985 Change-Id: I5a6ad5388e4bb69e142caa7f6de7127526f441ad Reviewed-by: Marc Mutz <marc.mutz@qt.io> Reviewed-by: Thiago Macieira <thiago.macieira@intel.com> (cherry picked from commit 8c8a0c06d4f77ba8a707ec0a101b423543bf30f0) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
parent
0a57b0e542
commit
b5dde79bc4
@ -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)
|
||||
|
@ -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<QString>("body_name_data");
|
||||
QTest::addColumn<QString>("expected_content_type_data");
|
||||
QTest::addColumn<QString>("expected_content_disposition_data");
|
||||
QTest::addColumn<QString>("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<QString>("expected_content_type_data");
|
||||
QTest::addColumn<QString>("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<QLatin1StringView>("name_data");
|
||||
QTest::addColumn<QAnyStringView>("body_name_data");
|
||||
QTest::addColumn<QString>("expected_content_type_data");
|
||||
QTest::addColumn<QString>("expected_content_disposition_data");
|
||||
QTest::addColumn<QString>("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};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user