diff --git a/Libraries/LibWeb/Fetch/Body.cpp b/Libraries/LibWeb/Fetch/Body.cpp index 36374998156..9649f3d831c 100644 --- a/Libraries/LibWeb/Fetch/Body.cpp +++ b/Libraries/LibWeb/Fetch/Body.cpp @@ -5,6 +5,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -16,10 +17,13 @@ #include #include #include +#include #include #include +#include #include #include +#include #include #include #include @@ -137,10 +141,15 @@ WebIDL::ExceptionOr package_data(JS::Realm& realm, ByteBuffer bytes, case PackageDataType::FormData: // If mimeType’s essence is "multipart/form-data", then: if (mime_type.has_value() && mime_type->essence() == "multipart/form-data"sv) { - // FIXME: 1. Parse bytes, using the value of the `boundary` parameter from mimeType, per the rules set forth in Returning Values from Forms: multipart/form-data. [RFC7578] - // FIXME: 2. If that fails for some reason, then throw a TypeError. - // FIXME: 3. Return a new FormData object, appending each entry, resulting from the parsing operation, to its entry list. - return JS::js_null(); + // 1. Parse bytes, using the value of the `boundary` parameter from mimeType, per the rules set forth in Returning Values from Forms: multipart/form-data. [RFC7578] + auto error_or_entry_list = parse_multipart_form_data(realm, bytes, mime_type.value()); + + // 2. If that fails for some reason, then throw a TypeError. + if (error_or_entry_list.is_error()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, MUST(String::formatted("Failed to parse multipart form data: {}", error_or_entry_list.release_error().message)) }; + + // 3. Return a new FormData object, appending each entry, resulting from the parsing operation, to its entry list. + return TRY(XHR::FormData::create(realm, error_or_entry_list.release_value())); } // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: else if (mime_type.has_value() && mime_type->essence() == "application/x-www-form-urlencoded"sv) { @@ -231,4 +240,232 @@ WebIDL::ExceptionOr> consume_body(JS::Realm& realm, Bod return promise; } +// https://andreubotella.github.io/multipart-form-data/#parse-a-multipart-form-data-name +static MultipartParsingErrorOr parse_multipart_form_data_name(GenericLexer& lexer) +{ + // 1. Assert: The byte at (position - 1) is 0x22 ("). + VERIFY(lexer.peek(-1) == '"'); + + // 2. Let name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x22 ("), given position. + auto name = lexer.consume_until(is_any_of("\n\r\""sv)); + + // 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1. + if (!lexer.consume_specific('"')) + return MultipartParsingError { MUST(String::formatted("Expected \" at position {}", lexer.tell())) }; + + // 4. Replace any occurrence of the following subsequences in name with the given byte: + // - "%0A" with 0x0A (LF) + // - "%0D" with 0x0D (CR) + // - "%22" with 0x22 (") + StringBuilder builder; + for (size_t i = 0; i < name.length(); ++i) { + // Check for subsequences starting with '%' + if (name[i] == '%' && i + 2 < name.length()) { + auto subsequence = name.substring_view(i, 3); + if (subsequence == "%0A"sv) { + builder.append(0x0A); // Append LF + i += 2; // Skip the next two characters + continue; + } + if (subsequence == "%0D"sv) { + builder.append(0x0D); // Append CR + i += 2; // Skip the next two characters + continue; + } + if (subsequence == "%22"sv) { + builder.append(0x22); // Append " + i += 2; // Skip the next two characters + continue; + } + } + + // Append the current character if no substitution was made + builder.append(name[i]); + } + + return builder.to_string_without_validation(); +} + +// https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers +static MultipartParsingErrorOr parse_multipart_form_data_header(GenericLexer& lexer) +{ + // 1. Let name, filename and contentType be null. + MultiPartFormDataHeader header; + + // 2. While true: + while (true) { + // 1. If position points to a sequence of bytes starting with 0x0D 0x0A (CR LF): + if (lexer.next_is("\r\n"sv)) { + // 1. If name is null, return failure. + if (!header.name.has_value()) + return MultipartParsingError { "Missing name parameter in Content-Disposition header"_string }; + + // 2. Return name, filename and contentType. + return header; + } + + // 2. Let header name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x3A (:), given position. + auto header_name = lexer.consume_until(is_any_of("\n\r:"sv)); + + // 3. Remove any HTTP tab or space bytes from the start or end of header name. + header_name = header_name.trim(Infrastructure::HTTP_TAB_OR_SPACE, TrimMode::Both); + + // 4. If header name does not match the field-name token production, return failure. + if (!Infrastructure::is_header_name(header_name.bytes())) + return MultipartParsingError { MUST(String::formatted("Invalid header name {}", header_name)) }; + + // 5. If the byte at position is not 0x3A (:), return failure. + // 6. Advance position by 1. + if (!lexer.consume_specific(':')) + return MultipartParsingError { MUST(String::formatted("Expected : at position {}", lexer.tell())) }; + + // 7. Collect a sequence of bytes that are HTTP tab or space bytes given position. (Do nothing with those bytes.) + lexer.ignore_while(Infrastructure::is_http_tab_or_space); + + // 8. Byte-lowercase header name and switch on the result: + // -> `content-disposition` + if (header_name.equals_ignoring_ascii_case("content-disposition"sv)) { + // 1. Set name and filename to null. + header.name.clear(); + header.filename.clear(); + + // 2. If position does not point to a sequence of bytes starting with `form-data; name="`, return failure. + // 3. Advance position so it points at the byte after the next 0x22 (") byte (the one in the sequence of bytes matched above). + if (!lexer.consume_specific("form-data; name=\""sv)) + return MultipartParsingError { MUST(String::formatted("Expected `form-data; name=\"` at position {}", lexer.tell())) }; + + // 4. Set name to the result of parsing a multipart/form-data name given input and position, if the result is not failure. Otherwise, return failure. + auto maybe_name = parse_multipart_form_data_name(lexer); + if (maybe_name.is_error()) + return maybe_name.release_error(); + header.name = maybe_name.release_value(); + + // 5. If position points to a sequence of bytes starting with `; filename="`: + // 1. Advance position so it points at the byte after the next 0x22 (") byte (the one in the sequence of bytes matched above). + if (lexer.consume_specific("; filename=\""sv)) { + // 2. Set filename to the result of parsing a multipart/form-data name given input and position, if the result is not failure. Otherwise, return failure. + auto maybe_filename = parse_multipart_form_data_name(lexer); + if (maybe_filename.is_error()) + return maybe_filename.release_error(); + header.filename = maybe_filename.release_value(); + } + } + // -> `content-type` + else if (header_name.equals_ignoring_ascii_case("content-type"sv)) { + // 1. Let header value be the result of collecting a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position. + auto header_value = lexer.consume_until(Infrastructure::is_http_newline); + + // 2. Remove any HTTP tab or space bytes from the end of header value. + header_value = header_value.trim(Infrastructure::HTTP_TAB_OR_SPACE, TrimMode::Right); + + // 3. Set contentType to the isomorphic decoding of header value. + header.content_type = Infra::isomorphic_decode(header_value.bytes()); + } + // -> Otherwise + else { + // 1. Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position. (Do nothing with those bytes.) + lexer.ignore_until(Infrastructure::is_http_newline); + } + + // 9. If position does not point to a sequence of bytes starting with 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2 (past the newline). + if (!lexer.consume_specific("\r\n"sv)) + return MultipartParsingError { MUST(String::formatted("Expected CRLF at position {}", lexer.tell())) }; + } + return header; +} + +// https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser +MultipartParsingErrorOr> parse_multipart_form_data(JS::Realm& realm, StringView input, MimeSniff::MimeType const& mime_type) +{ + // 1. Assert: mimeType’s essence is "multipart/form-data". + VERIFY(mime_type.essence() == "multipart/form-data"sv); + + // 2. If mimeType’s parameters["boundary"] does not exist, return failure. Otherwise, let boundary be the result of UTF-8 decoding mimeType’s parameters["boundary"]. + auto maybe_boundary = mime_type.parameters().get("boundary"sv); + if (!maybe_boundary.has_value()) + return MultipartParsingError { "Missing boundary parameter in Content-Type header"_string }; + auto boundary = maybe_boundary.release_value(); + + // 3. Let entry list be an empty entry list. + Vector entry_list; + + // 4. Let position be a pointer to a byte in input, initially pointing at the first byte. + GenericLexer lexer(input); + + auto boundary_with_dashes = MUST(String::formatted("--{}", boundary)); + + // 5. While true: + while (true) { + // 1. If position points to a sequence of bytes starting with 0x2D 0x2D (`--`) followed by boundary, advance position by 2 + the length of boundary. Otherwise, return failure. + if (!lexer.consume_specific(boundary_with_dashes)) + return MultipartParsingError { MUST(String::formatted("Expected `--` followed by boundary at position {}", lexer.tell())) }; + + // 2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A (`--` followed by CR LF) followed by the end of input, return entry list. + if (lexer.next_is("--\r\n"sv)) + return entry_list; + + // 3. If position does not point to a sequence of bytes starting with 0x0D 0x0A (CR LF), return failure. + // 4. Advance position by 2. (This skips past the newline.) + if (!lexer.consume_specific("\r\n"sv)) + return MultipartParsingError { MUST(String::formatted("Expected CRLF at position {}", lexer.tell())) }; + + // 5. Let name, filename and contentType be the result of parsing multipart/form-data headers on input and position, if the result is not failure. Otherwise, return failure. + auto header = TRY(parse_multipart_form_data_header(lexer)); + + // 6. Advance position by 2. (This skips past the empty line that marks the end of the headers.) + lexer.ignore(2); + + // 7. Let body be the empty byte sequence. + // 8. Body loop: While position is not past the end of input: + // 1. Append the code point at position to body. + // 2. If body ends with boundary: + // 1. Remove the last 4 + (length of boundary) bytes from body. + // 2. Decrease position by 4 + (length of boundary). + // 3. Break out of body loop. + auto body = lexer.consume_until(boundary_with_dashes.bytes_as_string_view()); + if (lexer.next_is(boundary_with_dashes.bytes_as_string_view())) { + constexpr size_t trailing_crlf_length = 2; + if (body.length() >= trailing_crlf_length) { + body = body.substring_view(0, body.length() - trailing_crlf_length); + lexer.retreat(trailing_crlf_length); + } + } + + // 9. If position does not point to a sequence of bytes starting with 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2. + if (!lexer.consume_specific("\r\n"sv)) + return MultipartParsingError { MUST(String::formatted("Expected CRLF at position {}", lexer.tell())) }; + + // 10. If filename is not null: + Optional value; + if (header.filename.has_value()) { + // 1. If contentType is null, set contentType to "text/plain". + if (!header.content_type.has_value()) + header.content_type = "text/plain"_string; + + // 2. If contentType is not an ASCII string, set contentType to the empty string. + if (!all_of(header.content_type->code_points(), is_ascii)) { + header.content_type = ""_string; + } + + // 3. Let value be a new File object with name filename, type contentType, and body body. + auto blob = FileAPI::Blob::create(realm, MUST(ByteBuffer::copy(body.bytes())), header.content_type.release_value()); + FileAPI::FilePropertyBag options {}; + options.type = blob->type(); + auto file = MUST(FileAPI::File::create(realm, { GC::make_root(blob) }, header.filename.release_value(), move(options))); + value = GC::make_root(file); + } + // 11. Otherwise: + else { + // 1. Let value be the UTF-8 decoding without BOM of body. + value = String::from_utf8_with_replacement_character(body, String::WithBOMHandling::No); + } + + // 12. Assert: name is a scalar value string and value is either a scalar value string or a File object. + VERIFY(header.name.has_value() && value.has_value()); + + // 13. Create an entry with name and value, and append it to entry list. + entry_list.empend(header.name.release_value(), value.release_value()); + } +} + } diff --git a/Libraries/LibWeb/Fetch/Body.h b/Libraries/LibWeb/Fetch/Body.h index 6cef9d04a0d..0c6d6094f55 100644 --- a/Libraries/LibWeb/Fetch/Body.h +++ b/Libraries/LibWeb/Fetch/Body.h @@ -8,6 +8,9 @@ #pragma once #include +#include +#include +#include #include #include #include @@ -23,6 +26,24 @@ enum class PackageDataType { Text, }; +struct MultiPartFormDataHeader { + Optional name; + Optional filename; + Optional content_type; +}; + +struct ContentDispositionHeader { + String type; + OrderedHashMap parameters; +}; + +struct MultipartParsingError { + String message; +}; + +template +using MultipartParsingErrorOr = ErrorOr; + // https://fetch.spec.whatwg.org/#body-mixin class BodyMixin { public: @@ -49,5 +70,6 @@ public: [[nodiscard]] WebIDL::ExceptionOr package_data(JS::Realm&, ByteBuffer, PackageDataType, Optional const&); [[nodiscard]] WebIDL::ExceptionOr> consume_body(JS::Realm&, BodyMixin const&, PackageDataType); +[[nodiscard]] MultipartParsingErrorOr> parse_multipart_form_data(JS::Realm&, StringView input, MimeSniff::MimeType const& mime_type); } diff --git a/Libraries/LibWeb/Fetch/Infrastructure/HTTP.h b/Libraries/LibWeb/Fetch/Infrastructure/HTTP.h index 59bfc90ef89..e2cfc37d6b4 100644 --- a/Libraries/LibWeb/Fetch/Infrastructure/HTTP.h +++ b/Libraries/LibWeb/Fetch/Infrastructure/HTTP.h @@ -37,6 +37,11 @@ constexpr bool is_http_tab_or_space(u32 const code_point) return code_point == 0x09 || code_point == 0x20; } +constexpr bool is_http_newline(u32 const code_point) +{ + return code_point == 0x0A || code_point == 0x0D; +} + enum class HttpQuotedStringExtractValue { No, Yes, diff --git a/Libraries/LibWeb/XHR/FormData.cpp b/Libraries/LibWeb/XHR/FormData.cpp index 3c1421f75b1..b6b1b9cf34a 100644 --- a/Libraries/LibWeb/XHR/FormData.cpp +++ b/Libraries/LibWeb/XHR/FormData.cpp @@ -50,6 +50,11 @@ WebIDL::ExceptionOr> FormData::create(JS::Realm& realm, Vector return construct_impl(realm, move(list)); } +WebIDL::ExceptionOr> FormData::create(JS::Realm& realm, Vector entry_list) +{ + return construct_impl(realm, move(entry_list)); +} + FormData::FormData(JS::Realm& realm, Vector entry_list) : PlatformObject(realm) , m_entry_list(move(entry_list)) diff --git a/Libraries/LibWeb/XHR/FormData.h b/Libraries/LibWeb/XHR/FormData.h index fc37565dd81..42545f0a375 100644 --- a/Libraries/LibWeb/XHR/FormData.h +++ b/Libraries/LibWeb/XHR/FormData.h @@ -28,6 +28,7 @@ public: static WebIDL::ExceptionOr> construct_impl(JS::Realm&, Vector entry_list); static WebIDL::ExceptionOr> create(JS::Realm&, Vector entry_list); + static WebIDL::ExceptionOr> create(JS::Realm&, Vector entry_list); WebIDL::ExceptionOr append(String const& name, String const& value); WebIDL::ExceptionOr append(String const& name, GC::Ref const& blob_value, Optional const& filename = {}); diff --git a/Tests/LibWeb/Text/expected/wpt-import/fetch/api/body/formdata.any.txt b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/body/formdata.any.txt new file mode 100644 index 00000000000..13934d29237 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/body/formdata.any.txt @@ -0,0 +1,8 @@ +Harness status: OK + +Found 3 tests + +3 Pass +Pass Consume empty response.formData() as FormData +Pass Consume empty request.formData() as FormData +Pass Consume multipart/form-data headers case-insensitively \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume-empty.any.txt b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume-empty.any.txt new file mode 100644 index 00000000000..bf1ac7ec0eb --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume-empty.any.txt @@ -0,0 +1,20 @@ +Harness status: OK + +Found 14 tests + +13 Pass +1 Fail +Pass Consume request's body as text +Pass Consume request's body as blob +Pass Consume request's body as arrayBuffer +Pass Consume request's body as json (error case) +Pass Consume request's body as formData with correct multipart type (error case) +Pass Consume request's body as formData with correct urlencoded type +Pass Consume request's body as formData without correct type (error case) +Pass Consume empty blob request body as arrayBuffer +Pass Consume empty text request body as arrayBuffer +Pass Consume empty blob request body as text +Pass Consume empty text request body as text +Pass Consume empty URLSearchParams request body as text +Fail Consume empty FormData request body as text +Pass Consume empty ArrayBuffer request body as text \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume.any.txt b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume.any.txt new file mode 100644 index 00000000000..b8dbb35c967 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/request/request-consume.any.txt @@ -0,0 +1,50 @@ +Harness status: OK + +Found 45 tests + +45 Pass +Pass Consume String request's body as text +Pass Consume String request's body as blob +Pass Consume String request's body as arrayBuffer +Pass Consume String request's body as bytes +Pass Consume String request's body as JSON +Pass Consume ArrayBuffer request's body as text +Pass Consume ArrayBuffer request's body as blob +Pass Consume ArrayBuffer request's body as arrayBuffer +Pass Consume ArrayBuffer request's body as bytes +Pass Consume ArrayBuffer request's body as JSON +Pass Consume Uint8Array request's body as text +Pass Consume Uint8Array request's body as blob +Pass Consume Uint8Array request's body as arrayBuffer +Pass Consume Uint8Array request's body as bytes +Pass Consume Uint8Array request's body as JSON +Pass Consume Int8Array request's body as text +Pass Consume Int8Array request's body as blob +Pass Consume Int8Array request's body as arrayBuffer +Pass Consume Int8Array request's body as bytes +Pass Consume Int8Array request's body as JSON +Pass Consume Float32Array request's body as text +Pass Consume Float32Array request's body as blob +Pass Consume Float32Array request's body as arrayBuffer +Pass Consume Float32Array request's body as bytes +Pass Consume Float32Array request's body as JSON +Pass Consume DataView request's body as text +Pass Consume DataView request's body as blob +Pass Consume DataView request's body as arrayBuffer +Pass Consume DataView request's body as bytes +Pass Consume DataView request's body as JSON +Pass Consume FormData request's body as FormData +Pass Consume blob response's body as blob +Pass Consume blob response's body as text +Pass Consume blob response's body as json +Pass Consume blob response's body as arrayBuffer +Pass Consume blob response's body as bytes +Pass Consume blob response's body as blob (empty blob as input) +Pass Consume JSON from text: '"null"' +Pass Consume JSON from text: '"1"' +Pass Consume JSON from text: '"true"' +Pass Consume JSON from text: '"\"string\""' +Pass Trying to consume bad JSON text as JSON: 'undefined' +Pass Trying to consume bad JSON text as JSON: '{' +Pass Trying to consume bad JSON text as JSON: 'a' +Pass Trying to consume bad JSON text as JSON: '[' \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume-empty.any.txt b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume-empty.any.txt new file mode 100644 index 00000000000..308fb6d906c --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume-empty.any.txt @@ -0,0 +1,20 @@ +Harness status: OK + +Found 14 tests + +13 Pass +1 Fail +Pass Consume response's body as text +Pass Consume response's body as blob +Pass Consume response's body as arrayBuffer +Pass Consume response's body as json (error case) +Pass Consume response's body as formData with correct multipart type (error case) +Pass Consume response's body as formData with correct urlencoded type +Pass Consume response's body as formData without correct type (error case) +Pass Consume empty blob response body as arrayBuffer +Pass Consume empty text response body as arrayBuffer +Pass Consume empty blob response body as text +Pass Consume empty text response body as text +Pass Consume empty URLSearchParams response body as text +Fail Consume empty FormData response body as text +Pass Consume empty ArrayBuffer response body as text \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume.txt b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume.txt new file mode 100644 index 00000000000..b2cfafd909d --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/fetch/api/response/response-consume.txt @@ -0,0 +1,46 @@ +Harness status: Error + +Found 40 tests + +24 Pass +16 Fail +Pass Consume response's body: from text to text +Pass Consume response's body: from text to blob +Pass Consume response's body: from text to arrayBuffer +Pass Consume response's body: from text to json +Fail Consume response's body: from text with correct multipart type to formData +Fail Consume response's body: from text with correct multipart type to formData with BOM +Pass Consume response's body: from text without correct multipart type to formData (error case) +Pass Consume response's body: from text with correct urlencoded type to formData +Pass Consume response's body: from text without correct urlencoded type to formData (error case) +Fail Consume response's body: from blob to blob +Fail Consume response's body: from blob to text +Fail Consume response's body: from blob to arrayBuffer +Fail Consume response's body: from blob to json +Fail Consume response's body: from blob with correct multipart type to formData +Pass Consume response's body: from blob without correct multipart type to formData (error case) +Fail Consume response's body: from blob with correct urlencoded type to formData +Pass Consume response's body: from blob without correct urlencoded type to formData (error case) +Pass Consume response's body: from FormData to formData +Pass Consume response's body: from FormData without correct type to formData (error case) +Fail Consume response's body: from FormData to blob +Pass Consume response's body: from FormData to text +Pass Consume response's body: from FormData to arrayBuffer +Pass Consume response's body: from URLSearchParams to formData +Pass Consume response's body: from URLSearchParams without correct type to formData (error case) +Fail Consume response's body: from URLSearchParams to blob +Pass Consume response's body: from URLSearchParams to text +Pass Consume response's body: from URLSearchParams to arrayBuffer +Pass Consume response's body: from stream to blob +Pass Consume response's body: from stream to text +Pass Consume response's body: from stream to arrayBuffer +Pass Consume response's body: from stream to json +Fail Consume response's body: from stream with correct multipart type to formData +Pass Consume response's body: from stream without correct multipart type to formData (error case) +Pass Consume response's body: from stream with correct urlencoded type to formData +Pass Consume response's body: from stream without correct urlencoded type to formData (error case) +Fail Consume response's body: from fetch to blob +Fail Consume response's body: from fetch to text +Fail Consume response's body: from fetch to arrayBuffer +Fail Consume response's body: from fetch without correct type to formData (error case) +Fail Consume response's body: from multipart form data blob to formData \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart-malformed.any.txt b/Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart-malformed.any.txt new file mode 100644 index 00000000000..cc91c4dd35f --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart-malformed.any.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Invalid form data should not crash the browser \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart.window.txt b/Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart.window.txt new file mode 100644 index 00000000000..898cf30f1c7 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/fetch/content-type/multipart.window.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass Ensure capital letters can be used in the boundary value. \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.html b/Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.html new file mode 100644 index 00000000000..dcfae528111 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.js b/Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.js new file mode 100644 index 00000000000..6733fa0ed70 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/body/formdata.any.js @@ -0,0 +1,25 @@ +promise_test(async t => { + const res = new Response(new FormData()); + const fd = await res.formData(); + assert_true(fd instanceof FormData); +}, 'Consume empty response.formData() as FormData'); + +promise_test(async t => { + const req = new Request('about:blank', { + method: 'POST', + body: new FormData() + }); + const fd = await req.formData(); + assert_true(fd instanceof FormData); +}, 'Consume empty request.formData() as FormData'); + +promise_test(async t => { + let formdata = new FormData(); + formdata.append('foo', new Blob([JSON.stringify({ bar: "baz", })], { type: "application/json" })); + let blob = await new Response(formdata).blob(); + let body = await blob.text(); + blob = new Blob([body.toLowerCase()], { type: blob.type.toLowerCase() }); + let formdataWithLowercaseBody = await new Response(blob).formData(); + assert_true(formdataWithLowercaseBody.has("foo")); + assert_equals(formdataWithLowercaseBody.get("foo").type, "application/json"); +}, 'Consume multipart/form-data headers case-insensitively'); diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.html b/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.html new file mode 100644 index 00000000000..b5dd65d5222 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.html @@ -0,0 +1,15 @@ + + +Request consume empty bodies + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.js b/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.js new file mode 100644 index 00000000000..0bf9672a795 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume-empty.any.js @@ -0,0 +1,89 @@ +// META: global=window,worker +// META: title=Request consume empty bodies + +function checkBodyText(test, request) { + return request.text().then(function(bodyAsText) { + assert_equals(bodyAsText, "", "Resolved value should be empty"); + assert_false(request.bodyUsed); + }); +} + +async function checkBodyBlob(test, request) { + const bodyAsBlob = await request.blob(); + const body = await bodyAsBlob.text(); + assert_equals(body, "", "Resolved value should be empty"); + assert_false(request.bodyUsed); +} + +function checkBodyArrayBuffer(test, request) { + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_false(request.bodyUsed); + }); +} + +function checkBodyJSON(test, request) { + return request.json().then( + function(bodyAsJSON) { + assert_unreached("JSON parsing should fail"); + }, + function() { + assert_false(request.bodyUsed); + }); +} + +function checkBodyFormData(test, request) { + return request.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_false(request.bodyUsed); + }); +} + +function checkBodyFormDataError(test, request) { + return promise_rejects_js(test, TypeError, request.formData()).then(function() { + assert_false(request.bodyUsed); + }); +} + +function checkRequestWithNoBody(bodyType, checkFunction, headers = []) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "headers": headers}); + assert_false(request.bodyUsed); + return checkFunction(test, request); + }, "Consume request's body as " + bodyType); +} + +checkRequestWithNoBody("text", checkBodyText); +checkRequestWithNoBody("blob", checkBodyBlob); +checkRequestWithNoBody("arrayBuffer", checkBodyArrayBuffer); +checkRequestWithNoBody("json (error case)", checkBodyJSON); +checkRequestWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); +checkRequestWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); +checkRequestWithNoBody("formData without correct type (error case)", checkBodyFormDataError); + +function checkRequestWithEmptyBody(bodyType, body, asText) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body}); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + if (asText) { + return request.text().then(function(bodyAsString) { + assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); + assert_true(request.bodyUsed, "bodyUsed is true after being consumed"); + }); + } + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_true(request.bodyUsed, "bodyUsed is true after being consumed"); + }); + }, "Consume empty " + bodyType + " request body as " + (asText ? "text" : "arrayBuffer")); +} + +// FIXME: Add BufferSource, FormData and URLSearchParams. +checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); +checkRequestWithEmptyBody("text", "", false); +checkRequestWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); +checkRequestWithEmptyBody("text", "", true); +checkRequestWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); +// FIXME: This test assumes that the empty string be returned but it is not clear whether that is right. See https://github.com/web-platform-tests/wpt/pull/3950. +checkRequestWithEmptyBody("FormData", new FormData(), true); +checkRequestWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.html b/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.html new file mode 100644 index 00000000000..cb41666eae5 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.html @@ -0,0 +1,15 @@ + + +Request consume + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.js b/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.js new file mode 100644 index 00000000000..b4cbe7457d2 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/request/request-consume.any.js @@ -0,0 +1,148 @@ +// META: global=window,worker +// META: title=Request consume +// META: script=../resources/utils.js + +function checkBodyText(request, expectedBody) { + return request.text().then(function(bodyAsText) { + assert_equals(bodyAsText, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as text: bodyUsed turned true"); + }); +} + +async function checkBodyBlob(request, expectedBody, checkContentType) { + const bodyAsBlob = await request.blob(); + + if (checkContentType) + assert_equals(bodyAsBlob.type, "text/plain", "Blob body type should be computed from the request Content-Type"); + + const body = await bodyAsBlob.text(); + assert_equals(body, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as blob: bodyUsed turned true"); +} + +function checkBodyArrayBuffer(request, expectedBody) { + return request.arrayBuffer().then(function(bodyAsArrayBuffer) { + validateBufferFromString(bodyAsArrayBuffer, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as arrayBuffer: bodyUsed turned true"); + }); +} + +function checkBodyBytes(request, expectedBody) { + return request.bytes().then(function(bodyAsUint8Array) { + assert_true(bodyAsUint8Array instanceof Uint8Array); + validateBufferFromString(bodyAsUint8Array.buffer, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as bytes: bodyUsed turned true"); + }); +} + +function checkBodyJSON(request, expectedBody) { + return request.json().then(function(bodyAsJSON) { + var strBody = JSON.stringify(bodyAsJSON) + assert_equals(strBody, expectedBody, "Retrieve and verify request's body"); + assert_true(request.bodyUsed, "body as json: bodyUsed turned true"); + }); +} + +function checkBodyFormData(request, expectedBody) { + return request.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_true(request.bodyUsed, "body as formData: bodyUsed turned true"); + }); +} + +function checkRequestBody(body, expected, bodyType) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body, "headers": [["Content-Type", "text/PLAIN"]] }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyText(request, expected); + }, "Consume " + bodyType + " request's body as text"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyBlob(request, expected); + }, "Consume " + bodyType + " request's body as blob"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyArrayBuffer(request, expected); + }, "Consume " + bodyType + " request's body as arrayBuffer"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyBytes(request, expected); + }, "Consume " + bodyType + " request's body as bytes"); + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": body }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyJSON(request, expected); + }, "Consume " + bodyType + " request's body as JSON"); +} + +var textData = JSON.stringify("This is response's body"); +var blob = new Blob([textData], { "type" : "text/plain" }); + +checkRequestBody(textData, textData, "String"); + +var string = "\"123456\""; +function getArrayBuffer() { + var arrayBuffer = new ArrayBuffer(8); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < 8; cptr++) + int8Array[cptr] = string.charCodeAt(cptr); + return arrayBuffer; +} + +function getArrayBufferWithZeros() { + var arrayBuffer = new ArrayBuffer(10); + var int8Array = new Int8Array(arrayBuffer); + for (var cptr = 0; cptr < 8; cptr++) + int8Array[cptr + 1] = string.charCodeAt(cptr); + return arrayBuffer; +} + +checkRequestBody(getArrayBuffer(), string, "ArrayBuffer"); +checkRequestBody(new Uint8Array(getArrayBuffer()), string, "Uint8Array"); +checkRequestBody(new Int8Array(getArrayBufferWithZeros(), 1, 8), string, "Int8Array"); +checkRequestBody(new Float32Array(getArrayBuffer()), string, "Float32Array"); +checkRequestBody(new DataView(getArrayBufferWithZeros(), 1, 8), string, "DataView"); + +promise_test(function(test) { + var formData = new FormData(); + formData.append("name", "value") + var request = new Request("", {"method": "POST", "body": formData }); + assert_false(request.bodyUsed, "bodyUsed is false at init"); + return checkBodyFormData(request, formData); +}, "Consume FormData request's body as FormData"); + +function checkBlobResponseBody(blobBody, blobData, bodyType, checkFunction) { + promise_test(function(test) { + var response = new Response(blobBody); + assert_false(response.bodyUsed, "bodyUsed is false at init"); + return checkFunction(response, blobData); + }, "Consume blob response's body as " + bodyType); +} + +checkBlobResponseBody(blob, textData, "blob", checkBodyBlob); +checkBlobResponseBody(blob, textData, "text", checkBodyText); +checkBlobResponseBody(blob, textData, "json", checkBodyJSON); +checkBlobResponseBody(blob, textData, "arrayBuffer", checkBodyArrayBuffer); +checkBlobResponseBody(blob, textData, "bytes", checkBodyBytes); +checkBlobResponseBody(new Blob([""]), "", "blob (empty blob as input)", checkBodyBlob); + +var goodJSONValues = ["null", "1", "true", "\"string\""]; +goodJSONValues.forEach(function(value) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": value}); + return request.json().then(function(v) { + assert_equals(v, JSON.parse(value)); + }); + }, "Consume JSON from text: '" + JSON.stringify(value) + "'"); +}); + +var badJSONValues = ["undefined", "{", "a", "["]; +badJSONValues.forEach(function(value) { + promise_test(function(test) { + var request = new Request("", {"method": "POST", "body": value}); + return promise_rejects_js(test, SyntaxError, request.json()); + }, "Trying to consume bad JSON text as JSON: '" + value + "'"); +}); diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/resources/utils.js b/Tests/LibWeb/Text/input/wpt-import/fetch/api/resources/utils.js new file mode 100644 index 00000000000..3721d9bf9cc --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/resources/utils.js @@ -0,0 +1,120 @@ +var RESOURCES_DIR = "../resources/"; + +function dirname(path) { + return path.replace(/\/[^\/]*$/, '/') +} + +function checkRequest(request, ExpectedValuesDict) { + for (var attribute in ExpectedValuesDict) { + switch(attribute) { + case "headers": + for (var key in ExpectedValuesDict["headers"].keys()) { + assert_equals(request["headers"].get(key), ExpectedValuesDict["headers"].get(key), + "Check headers attribute has " + key + ":" + ExpectedValuesDict["headers"].get(key)); + } + break; + + case "body": + //for checking body's content, a dedicated asyncronous/promise test should be used + assert_true(request["headers"].has("Content-Type") , "Check request has body using Content-Type header") + break; + + case "method": + case "referrer": + case "referrerPolicy": + case "credentials": + case "cache": + case "redirect": + case "integrity": + case "url": + case "destination": + assert_equals(request[attribute], ExpectedValuesDict[attribute], "Check " + attribute + " attribute") + break; + + default: + break; + } + } +} + +function stringToArray(str) { + var array = new Uint8Array(str.length); + for (var i=0, strLen = str.length; i < strLen; i++) + array[i] = str.charCodeAt(i); + return array; +} + +function encode_utf8(str) +{ + if (self.TextEncoder) + return (new TextEncoder).encode(str); + return stringToArray(unescape(encodeURIComponent(str))); +} + +function validateBufferFromString(buffer, expectedValue, message) +{ + return assert_array_equals(new Uint8Array(buffer !== undefined ? buffer : []), stringToArray(expectedValue), message); +} + +function validateStreamFromString(reader, expectedValue, retrievedArrayBuffer) { + // Passing Uint8Array for byte streams; non-byte streams will simply ignore it + return reader.read(new Uint8Array(64)).then(function(data) { + if (!data.done) { + assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); + var newBuffer; + if (retrievedArrayBuffer) { + newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length); + newBuffer.set(retrievedArrayBuffer, 0); + newBuffer.set(data.value, retrievedArrayBuffer.length); + } else { + newBuffer = data.value; + } + return validateStreamFromString(reader, expectedValue, newBuffer); + } + validateBufferFromString(retrievedArrayBuffer, expectedValue, "Retrieve and verify stream"); + }); +} + +function validateStreamFromPartialString(reader, expectedValue, retrievedArrayBuffer) { + // Passing Uint8Array for byte streams; non-byte streams will simply ignore it + return reader.read(new Uint8Array(64)).then(function(data) { + if (!data.done) { + assert_true(data.value instanceof Uint8Array, "Fetch ReadableStream chunks should be Uint8Array"); + var newBuffer; + if (retrievedArrayBuffer) { + newBuffer = new Uint8Array(data.value.length + retrievedArrayBuffer.length); + newBuffer.set(retrievedArrayBuffer, 0); + newBuffer.set(data.value, retrievedArrayBuffer.length); + } else { + newBuffer = data.value; + } + return validateStreamFromPartialString(reader, expectedValue, newBuffer); + } + + var string = new TextDecoder("utf-8").decode(retrievedArrayBuffer); + return assert_true(string.search(expectedValue) != -1, "Retrieve and verify stream"); + }); +} + +// From streams tests +function delay(milliseconds) +{ + return new Promise(function(resolve) { + step_timeout(resolve, milliseconds); + }); +} + +function requestForbiddenHeaders(desc, forbiddenHeaders) { + var url = RESOURCES_DIR + "inspect-headers.py"; + var requestInit = {"headers": forbiddenHeaders} + var urlParameters = "?headers=" + Object.keys(forbiddenHeaders).join("|"); + + promise_test(function(test){ + return fetch(url + urlParameters, requestInit).then(function(resp) { + assert_equals(resp.status, 200, "HTTP status is 200"); + assert_equals(resp.type , "basic", "Response's type is basic"); + for (var header in forbiddenHeaders) + assert_not_equals(resp.headers.get("x-request-" + header), forbiddenHeaders[header], header + " does not have the value we defined"); + }); + }, desc); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.html b/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.html new file mode 100644 index 00000000000..8eab67a531a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.html @@ -0,0 +1,15 @@ + + +Response consume empty bodies + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.js b/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.js new file mode 100644 index 00000000000..a5df3562586 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume-empty.any.js @@ -0,0 +1,88 @@ +// META: global=window,worker +// META: title=Response consume empty bodies + +function checkBodyText(test, response) { + return response.text().then(function(bodyAsText) { + assert_equals(bodyAsText, "", "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); +} + +async function checkBodyBlob(test, response) { + const bodyAsBlob = await response.blob(); + const body = await bodyAsBlob.text(); + + assert_equals(body, "", "Resolved value should be empty"); + assert_false(response.bodyUsed); +} + +function checkBodyArrayBuffer(test, response) { + return response.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyJSON(test, response) { + return response.json().then( + function(bodyAsJSON) { + assert_unreached("JSON parsing should fail"); + }, + function() { + assert_false(response.bodyUsed); + }); +} + +function checkBodyFormData(test, response) { + return response.formData().then(function(bodyAsFormData) { + assert_true(bodyAsFormData instanceof FormData, "Should receive a FormData"); + assert_false(response.bodyUsed); + }); +} + +function checkBodyFormDataError(test, response) { + return promise_rejects_js(test, TypeError, response.formData()).then(function() { + assert_false(response.bodyUsed); + }); +} + +function checkResponseWithNoBody(bodyType, checkFunction, headers = []) { + promise_test(function(test) { + var response = new Response(undefined, { "headers": headers }); + assert_false(response.bodyUsed); + return checkFunction(test, response); + }, "Consume response's body as " + bodyType); +} + +checkResponseWithNoBody("text", checkBodyText); +checkResponseWithNoBody("blob", checkBodyBlob); +checkResponseWithNoBody("arrayBuffer", checkBodyArrayBuffer); +checkResponseWithNoBody("json (error case)", checkBodyJSON); +checkResponseWithNoBody("formData with correct multipart type (error case)", checkBodyFormDataError, [["Content-Type", 'multipart/form-data; boundary="boundary"']]); +checkResponseWithNoBody("formData with correct urlencoded type", checkBodyFormData, [["Content-Type", "application/x-www-form-urlencoded;charset=UTF-8"]]); +checkResponseWithNoBody("formData without correct type (error case)", checkBodyFormDataError); + +function checkResponseWithEmptyBody(bodyType, body, asText) { + promise_test(function(test) { + var response = new Response(body); + assert_false(response.bodyUsed, "bodyUsed is false at init"); + if (asText) { + return response.text().then(function(bodyAsString) { + assert_equals(bodyAsString.length, 0, "Resolved value should be empty"); + assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); + }); + } + return response.arrayBuffer().then(function(bodyAsArrayBuffer) { + assert_equals(bodyAsArrayBuffer.byteLength, 0, "Resolved value should be empty"); + assert_true(response.bodyUsed, "bodyUsed is true after being consumed"); + }); + }, "Consume empty " + bodyType + " response body as " + (asText ? "text" : "arrayBuffer")); +} + +checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), false); +checkResponseWithEmptyBody("text", "", false); +checkResponseWithEmptyBody("blob", new Blob([], { "type" : "text/plain" }), true); +checkResponseWithEmptyBody("text", "", true); +checkResponseWithEmptyBody("URLSearchParams", new URLSearchParams(""), true); +checkResponseWithEmptyBody("FormData", new FormData(), true); +checkResponseWithEmptyBody("ArrayBuffer", new ArrayBuffer(), true); diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume.html b/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume.html new file mode 100644 index 00000000000..cc98f1ad89d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/api/response/response-consume.html @@ -0,0 +1,317 @@ + + + + + Response consume + + + + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.html b/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.html new file mode 100644 index 00000000000..7ce04c9f8b6 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.js b/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.js new file mode 100644 index 00000000000..9de0edc24ac --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart-malformed.any.js @@ -0,0 +1,22 @@ +// This is a repro for Chromium issue https://crbug.com/1412007. +promise_test(t => { + const form_string = + "--Boundary_with_capital_letters\r\n" + + "Content-Type: application/json\r\n" + + 'Content-Disposition: form-data; name="does_this_work"\r\n' + + "\r\n" + + 'YES\r\n' + + "--Boundary_with_capital_letters-Random junk"; + + const r = new Response(new Blob([form_string]), { + headers: [ + [ + "Content-Type", + "multipart/form-data; boundary=Boundary_with_capital_letters", + ], + ], + }); + + return promise_rejects_js(t, TypeError, r.formData(), + "form data should fail to parse"); +}, "Invalid form data should not crash the browser"); diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.html b/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.html new file mode 100644 index 00000000000..827634694b1 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.html @@ -0,0 +1,8 @@ + + +Ensure capital letters can be used in the boundary value. + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.js b/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.js new file mode 100644 index 00000000000..03b037a0e62 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/fetch/content-type/multipart.window.js @@ -0,0 +1,33 @@ +// META: title=Ensure capital letters can be used in the boundary value. +setup({ single_test: true }); +(async () => { + const form_string = + "--Boundary_with_capital_letters\r\n" + + "Content-Type: application/json\r\n" + + 'Content-Disposition: form-data; name="does_this_work"\r\n' + + "\r\n" + + 'YES\r\n' + + "--Boundary_with_capital_letters--\r\n"; + + const r = new Response(new Blob([form_string]), { + headers: [ + [ + "Content-Type", + "multipart/form-data; boundary=Boundary_with_capital_letters", + ], + ], + }); + + var s = ""; + try { + const fd = await r.formData(); + for (const [key, value] of fd.entries()) { + s += (`${key} = ${value}`); + } + } catch (ex) { + s = ex; + } + + assert_equals(s, "does_this_work = YES"); + done(); +})();