mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
fetch: implement multipart/form-data parsing for Request::formData() (#42041)
Testing: More test passed in `fetch/api/abort/request.any.js.ini` fixes: #25106 --------- Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>
This commit is contained in:
100
Cargo.lock
generated
100
Cargo.lock
generated
@@ -803,6 +803,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
@@ -1041,6 +1047,12 @@ dependencies = [
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "buf-read-ext"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e2c71c44e5bbc64de4ecfac946e05f9bba5cc296ea7bab4d3eda242a3ffa73c"
|
||||
|
||||
[[package]]
|
||||
name = "build-parallel"
|
||||
version = "0.1.2"
|
||||
@@ -3112,6 +3124,17 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
@@ -3120,7 +3143,7 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5375,6 +5398,21 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime-multipart-hyper1"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc819448803ee517eb413276ca59613c2850a95c4fdcd87ec82c5a6ce6cdab1a"
|
||||
dependencies = [
|
||||
"buf-read-ext",
|
||||
"http 1.4.0",
|
||||
"httparse",
|
||||
"log",
|
||||
"mime",
|
||||
"tempfile",
|
||||
"textnonce",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
@@ -5408,7 +5446,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -7168,6 +7206,19 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
"libc",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -7189,6 +7240,16 @@ dependencies = [
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
@@ -7209,6 +7270,15 @@ dependencies = [
|
||||
"rand_core 0.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom 0.1.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
@@ -7227,6 +7297,15 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-alloc"
|
||||
version = "0.1.4"
|
||||
@@ -7762,6 +7841,7 @@ dependencies = [
|
||||
"media",
|
||||
"metrics",
|
||||
"mime",
|
||||
"mime-multipart-hyper1",
|
||||
"mime_guess",
|
||||
"ml-dsa",
|
||||
"ml-kem",
|
||||
@@ -9188,6 +9268,16 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textnonce"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7743f8d70cd784ed1dc33106a18998d77758d281dc40dc3e6d050cf0f5286683"
|
||||
dependencies = [
|
||||
"base64 0.12.3",
|
||||
"rand 0.7.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
@@ -10089,6 +10179,12 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
|
||||
@@ -111,6 +111,7 @@ malloc_size_of_derive = "0.1"
|
||||
markup5ever = "0.38"
|
||||
memmap2 = "0.9.9"
|
||||
mime = "0.3.13"
|
||||
mime-multipart-hyper1 = "0.10.0"
|
||||
mime_guess = "2.0.5"
|
||||
ml-dsa = "0.0.4"
|
||||
ml-kem = { version = "0.2", features = ["deterministic"] }
|
||||
|
||||
@@ -98,6 +98,7 @@ markup5ever = { workspace = true }
|
||||
media = { path = "../media" }
|
||||
metrics = { path = "../metrics" }
|
||||
mime = { workspace = true }
|
||||
mime-multipart-hyper1 = { workspace = true }
|
||||
mime_guess = { workspace = true }
|
||||
ml-dsa = { workspace = true }
|
||||
ml-kem = { workspace = true }
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::rc::Rc;
|
||||
use std::{ptr, slice, str};
|
||||
use std::{fs, ptr, slice, str};
|
||||
|
||||
use base::generic_channel::GenericSharedMemory;
|
||||
use constellation_traits::BlobImpl;
|
||||
use encoding_rs::{Encoding, UTF_8};
|
||||
use http::HeaderMap;
|
||||
use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
|
||||
use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
|
||||
use ipc_channel::router::ROUTER;
|
||||
use js::jsapi::{Heap, JS_ClearPendingException, JSObject, Value as JSValue};
|
||||
@@ -17,6 +20,7 @@ use js::rust::HandleValue;
|
||||
use js::rust::wrappers::{JS_GetPendingException, JS_ParseJSON};
|
||||
use js::typedarray::{ArrayBufferU8, Uint8};
|
||||
use mime::{self, Mime};
|
||||
use mime_multipart_hyper1::{Node, read_multipart_body};
|
||||
use net_traits::request::{
|
||||
BodyChunkRequest, BodyChunkResponse, BodySource as NetBodySource, RequestBody,
|
||||
};
|
||||
@@ -27,12 +31,14 @@ use crate::dom::bindings::codegen::Bindings::BlobBinding::Blob_Binding::BlobMeth
|
||||
use crate::dom::bindings::codegen::Bindings::FormDataBinding::FormDataMethods;
|
||||
use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
|
||||
use crate::dom::bindings::error::{Error, Fallible};
|
||||
use crate::dom::bindings::inheritance::Castable;
|
||||
use crate::dom::bindings::refcounted::Trusted;
|
||||
use crate::dom::bindings::reflector::{DomGlobal, DomObject};
|
||||
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
|
||||
use crate::dom::bindings::str::{DOMString, USVString};
|
||||
use crate::dom::bindings::trace::RootedTraceableBox;
|
||||
use crate::dom::blob::{Blob, normalize_type_string};
|
||||
use crate::dom::file::File;
|
||||
use crate::dom::formdata::FormData;
|
||||
use crate::dom::globalscope::GlobalScope;
|
||||
use crate::dom::html::htmlformelement::{encode_multipart_form_data, generate_boundary};
|
||||
@@ -904,6 +910,114 @@ fn run_blob_data_algorithm(
|
||||
Ok(FetchedData::BlobData(blob))
|
||||
}
|
||||
|
||||
fn extract_name_from_content_disposition(headers: &HeaderMap) -> Option<String> {
|
||||
let cd = headers.get(CONTENT_DISPOSITION)?.to_str().ok()?;
|
||||
|
||||
for part in cd.split(';').map(|s| s.trim()) {
|
||||
if let Some(rest) = part.strip_prefix("name=") {
|
||||
let v = rest.trim();
|
||||
let v = v.strip_prefix('"').unwrap_or(v);
|
||||
let v = v.strip_suffix('"').unwrap_or(v);
|
||||
return Some(v.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn extract_filename_from_content_disposition(headers: &HeaderMap) -> Option<String> {
|
||||
let cd = headers.get(CONTENT_DISPOSITION)?.to_str().ok()?;
|
||||
if let Some(index) = cd.find("filename=") {
|
||||
let start = index + "filename=".len();
|
||||
return Some(
|
||||
cd.get(start..)
|
||||
.unwrap_or_default()
|
||||
.trim_matches('"')
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
if let Some(index) = cd.find("filename*=UTF-8''") {
|
||||
let start = index + "filename*=UTF-8''".len();
|
||||
return Some(
|
||||
cd.get(start..)
|
||||
.unwrap_or_default()
|
||||
.trim_matches('"')
|
||||
.to_owned(),
|
||||
);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn content_type_from_headers(headers: &HeaderMap) -> Result<String, Error> {
|
||||
match headers.get(CONTENT_TYPE) {
|
||||
Some(value) => Ok(value
|
||||
.to_str()
|
||||
.map_err(|_| Error::Type("Inappropriate MIME-type for Body".to_string()))?
|
||||
.to_string()),
|
||||
None => Ok("text/plain".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_form_data_entry_from_part(
|
||||
root: &GlobalScope,
|
||||
formdata: &FormData,
|
||||
headers: &HeaderMap,
|
||||
body: Vec<u8>,
|
||||
can_gc: CanGc,
|
||||
) -> Fallible<()> {
|
||||
let Some(name) = extract_name_from_content_disposition(headers) else {
|
||||
return Ok(());
|
||||
};
|
||||
// A part whose `Content-Disposition` header contains a `name` parameter whose value is `_charset_` is parsed like any other part. It does not change the encoding.
|
||||
let filename = extract_filename_from_content_disposition(headers);
|
||||
if let Some(filename) = filename {
|
||||
// Each part whose `Content-Disposition` header contains a `filename` parameter must be parsed into an entry whose value is a File object whose contents are the contents of the part.
|
||||
//
|
||||
// The name attribute of the File object must have the value of the `filename` parameter of the part.
|
||||
//
|
||||
// The type attribute of the File object must have the value of the `Content-Type` header of the part if the part has such header, and `text/plain` (the default defined by [RFC7578] section 4.4) otherwise.
|
||||
let content_type = content_type_from_headers(headers)?;
|
||||
let file = File::new(
|
||||
root,
|
||||
BlobImpl::new_from_bytes(body, normalize_type_string(&content_type)),
|
||||
DOMString::from(filename),
|
||||
None,
|
||||
can_gc,
|
||||
);
|
||||
let blob = file.upcast::<Blob>();
|
||||
formdata.Append_(USVString(name), blob, None);
|
||||
} else {
|
||||
// Each part whose `Content-Disposition` header does not contain a `filename` parameter must be parsed into an entry whose value is the UTF-8 decoded without BOM content of the part. This is done regardless of the presence or the value of a `Content-Type` header and regardless of the presence or the value of a `charset` parameter.
|
||||
|
||||
let (value, _) = UTF_8.decode_without_bom_handling(&body);
|
||||
formdata.Append(USVString(name), USVString(value.to_string()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_multipart_nodes(
|
||||
root: &GlobalScope,
|
||||
formdata: &FormData,
|
||||
nodes: Vec<Node>,
|
||||
can_gc: CanGc,
|
||||
) -> Fallible<()> {
|
||||
for node in nodes {
|
||||
match node {
|
||||
Node::Part(part) => {
|
||||
append_form_data_entry_from_part(root, formdata, &part.headers, part.body, can_gc)?;
|
||||
},
|
||||
Node::File(file_part) => {
|
||||
let body = fs::read(&file_part.path)
|
||||
.map_err(|_| Error::Type("file part could not be read".to_string()))?;
|
||||
append_form_data_entry_from_part(root, formdata, &file_part.headers, body, can_gc)?;
|
||||
},
|
||||
Node::Multipart((_, inner)) => {
|
||||
append_multipart_nodes(root, formdata, inner, can_gc)?;
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// <https://fetch.spec.whatwg.org/#ref-for-concept-body-consume-body%E2%91%A2>
|
||||
fn run_form_data_algorithm(
|
||||
root: &GlobalScope,
|
||||
@@ -911,15 +1025,57 @@ fn run_form_data_algorithm(
|
||||
mime: &[u8],
|
||||
can_gc: CanGc,
|
||||
) -> Fallible<FetchedData> {
|
||||
// The formData() method steps are to return the result of running consume body
|
||||
// with this and the following steps given a byte sequence bytes:
|
||||
let mime_str = str::from_utf8(mime).unwrap_or_default();
|
||||
let mime: Mime = mime_str
|
||||
.parse()
|
||||
.map_err(|_| Error::Type("Inappropriate MIME-type for Body".to_string()))?;
|
||||
|
||||
// TODO
|
||||
// ... Parser for Mime(TopLevel::Multipart, SubLevel::FormData, _)
|
||||
// ... is not fully determined yet.
|
||||
// Let mimeType be the result of get the MIME type with this.
|
||||
//
|
||||
// If mimeType is non-null, then switch on mimeType’s essence and run the corresponding steps:
|
||||
if mime.type_() == mime::MULTIPART && mime.subtype() == mime::FORM_DATA {
|
||||
// "multipart/form-data"
|
||||
// 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]
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
CONTENT_TYPE,
|
||||
mime_str
|
||||
.parse()
|
||||
.map_err(|_| Error::Type("Inappropriate MIME-type for Body".to_string()))?,
|
||||
);
|
||||
|
||||
if let Some(boundary) = mime.get_param(mime::BOUNDARY) {
|
||||
let closing_boundary = format!("--{}--", boundary.as_str()).into_bytes();
|
||||
let trimmed_bytes = bytes.strip_suffix(b"\r\n").unwrap_or(&bytes);
|
||||
if trimmed_bytes == closing_boundary {
|
||||
let formdata = FormData::new(None, root, can_gc);
|
||||
return Ok(FetchedData::FormData(formdata));
|
||||
}
|
||||
}
|
||||
|
||||
let mut cursor = Cursor::new(bytes);
|
||||
// If that fails for some reason, then throw a TypeError.
|
||||
let nodes = read_multipart_body(&mut cursor, &headers, false)
|
||||
.map_err(|_| Error::Type("Inappropriate MIME-type for Body".to_string()))?;
|
||||
// The above is a rough approximation of what is needed for `multipart/form-data`,
|
||||
// a more detailed parsing specification is to be written. Volunteers welcome.
|
||||
|
||||
// Return a new FormData object, appending each entry, resulting from the parsing operation, to its entry list.
|
||||
let formdata = FormData::new(None, root, can_gc);
|
||||
|
||||
append_multipart_nodes(root, &formdata, nodes, can_gc)?;
|
||||
|
||||
return Ok(FetchedData::FormData(formdata));
|
||||
}
|
||||
|
||||
if mime.type_() == mime::APPLICATION && mime.subtype() == mime::WWW_FORM_URLENCODED {
|
||||
// "application/x-www-form-urlencoded"
|
||||
// Let entries be the result of parsing bytes.
|
||||
//
|
||||
// Return a new FormData object whose entry list is entries.
|
||||
let entries = form_urlencoded::parse(&bytes);
|
||||
let formdata = FormData::new(None, root, can_gc);
|
||||
for (k, e) in entries {
|
||||
@@ -928,6 +1084,7 @@ fn run_form_data_algorithm(
|
||||
return Ok(FetchedData::FormData(formdata));
|
||||
}
|
||||
|
||||
// Throw a TypeError.
|
||||
Err(Error::Type("Inappropriate MIME-type for Body".to_string()))
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@ skip = [
|
||||
"bitflags",
|
||||
"cookie",
|
||||
"redox_syscall",
|
||||
# Duplicated by getrandom 0.1 and getrandom 0.2
|
||||
"wasi",
|
||||
|
||||
# New versions of these dependencies is pulled in by GStreamer / GLib.
|
||||
"itertools",
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
[request.any.html]
|
||||
[Calling formData() on an aborted request]
|
||||
expected: FAIL
|
||||
|
||||
[Aborting a request after calling formData()]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[request.any.serviceworker.html]
|
||||
expected: ERROR
|
||||
@@ -13,8 +7,3 @@
|
||||
expected: ERROR
|
||||
|
||||
[request.any.worker.html]
|
||||
[Calling formData() on an aborted request]
|
||||
expected: FAIL
|
||||
|
||||
[Aborting a request after calling formData()]
|
||||
expected: FAIL
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
[formdata.any.worker.html]
|
||||
[Consume empty response.formData() as FormData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume empty request.formData() as FormData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume multipart/form-data headers case-insensitively]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[formdata.any.html]
|
||||
[Consume empty response.formData() as FormData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume empty request.formData() as FormData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume multipart/form-data headers case-insensitively]
|
||||
expected: FAIL
|
||||
@@ -1,13 +1,3 @@
|
||||
[request-consume.any.html]
|
||||
[Consume FormData request's body as FormData]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[request-consume.any.worker.html]
|
||||
[Consume FormData request's body as FormData]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
[request-consume.any.serviceworker.html]
|
||||
expected: ERROR
|
||||
|
||||
|
||||
@@ -1,21 +1,3 @@
|
||||
[response-consume.html]
|
||||
[Consume response's body: from text with correct multipart type to formData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume response's body: from text with correct multipart type to formData with BOM]
|
||||
expected: FAIL
|
||||
|
||||
[Consume response's body: from blob with correct multipart type to formData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume response's body: from FormData to formData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume response's body: from stream with correct multipart type to formData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume response's body: from multipart form data blob to formData]
|
||||
expected: FAIL
|
||||
|
||||
[Consume response's body: from FormData to blob]
|
||||
expected: FAIL
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[multipart.window.html]
|
||||
[Ensure capital letters can be used in the boundary value.]
|
||||
expected: FAIL
|
||||
Reference in New Issue
Block a user