Skip to main content

script/
body.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::io::Cursor;
6use std::rc::Rc;
7use std::{fs, ptr, slice, str};
8
9use encoding_rs::{Encoding, UTF_8};
10use http::HeaderMap;
11use http::header::{CONTENT_DISPOSITION, CONTENT_TYPE};
12use ipc_channel::ipc::{self, IpcReceiver, IpcSender};
13use ipc_channel::router::ROUTER;
14use js::jsapi::{Heap, JSObject, Value as JSValue};
15use js::jsval::{JSVal, UndefinedValue};
16use js::realm::CurrentRealm;
17use js::rust::HandleValue;
18use js::rust::wrappers2::{JS_ClearPendingException, JS_GetPendingException, JS_ParseJSON};
19use js::typedarray::{ArrayBufferU8, Uint8};
20use mime::{self, Mime};
21use net_traits::request::{
22    BodyChunkRequest, BodyChunkResponse, BodySource as NetBodySource, RequestBody,
23};
24use script_bindings::reflector::DomObject;
25use servo_base::generic_channel::GenericSharedMemory;
26use servo_constellation_traits::BlobImpl;
27use url::form_urlencoded;
28
29use crate::dom::bindings::buffer_source::create_buffer_source;
30use crate::dom::bindings::codegen::Bindings::BlobBinding::Blob_Binding::BlobMethods;
31use crate::dom::bindings::codegen::Bindings::FormDataBinding::FormDataMethods;
32use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
33use crate::dom::bindings::error::{Error, Fallible};
34use crate::dom::bindings::inheritance::Castable;
35use crate::dom::bindings::refcounted::Trusted;
36use crate::dom::bindings::reflector::DomGlobal;
37use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
38use crate::dom::bindings::str::{DOMString, USVString};
39use crate::dom::bindings::trace::RootedTraceableBox;
40use crate::dom::blob::{Blob, normalize_type_string};
41use crate::dom::file::File;
42use crate::dom::formdata::FormData;
43use crate::dom::globalscope::GlobalScope;
44use crate::dom::html::htmlformelement::{encode_multipart_form_data, generate_boundary};
45use crate::dom::promise::Promise;
46use crate::dom::promisenativehandler::{Callback, PromiseNativeHandler};
47use crate::dom::readablestream::{ReadableStream, get_read_promise_bytes, get_read_promise_done};
48use crate::dom::urlsearchparams::URLSearchParams;
49use crate::mime_multipart::{Node, read_multipart_body};
50use crate::realms::enter_auto_realm;
51use crate::script_runtime::CanGc;
52use crate::task_source::SendableTaskSource;
53
54/// <https://fetch.spec.whatwg.org/#concept-body-clone>
55pub(crate) fn clone_body_stream_for_dom_body(
56    cx: &mut js::context::JSContext,
57    original_body_stream: &MutNullableDom<ReadableStream>,
58    cloned_body_stream: &MutNullableDom<ReadableStream>,
59) -> Fallible<()> {
60    // To clone a body *body*, run these steps:
61
62    let Some(stream) = original_body_stream.get() else {
63        return Ok(());
64    };
65
66    // step 1. Let « out1, out2 » be the result of teeing body’s stream.
67    let branches = stream.tee(cx, true)?;
68    let out1 = &*branches[0];
69    let out2 = &*branches[1];
70
71    // step 2. Set body’s stream to out1.
72    // step 3. Return a body whose stream is out2 and other members are copied from body.
73    original_body_stream.set(Some(out1));
74    cloned_body_stream.set(Some(out2));
75
76    Ok(())
77}
78
79/// The Dom object, or ReadableStream, that is the source of a body.
80/// <https://fetch.spec.whatwg.org/#concept-body-source>
81#[derive(Clone, PartialEq)]
82pub(crate) enum BodySource {
83    /// A ReadableStream comes with a null-source.
84    Null,
85    /// Another Dom object as source,
86    /// TODO: store the actual object
87    /// and re-extract a stream on re-direct.
88    Object,
89}
90
91/// The reason to stop reading from the body.
92enum StopReading {
93    /// The stream has errored.
94    Error,
95    /// The stream is done.
96    Done,
97}
98
99/// The IPC route handler
100/// for <https://fetch.spec.whatwg.org/#concept-request-transmit-body>.
101/// This route runs in the script process,
102/// and will queue tasks to perform operations
103/// on the stream and transmit body chunks over IPC.
104#[derive(Clone)]
105struct TransmitBodyConnectHandler {
106    stream: Trusted<ReadableStream>,
107    task_source: SendableTaskSource,
108    bytes_sender: Option<IpcSender<BodyChunkResponse>>,
109    control_sender: Option<IpcSender<BodyChunkRequest>>,
110    in_memory: Option<GenericSharedMemory>,
111    in_memory_done: bool,
112    source: BodySource,
113}
114
115impl TransmitBodyConnectHandler {
116    pub(crate) fn new(
117        stream: Trusted<ReadableStream>,
118        task_source: SendableTaskSource,
119        control_sender: IpcSender<BodyChunkRequest>,
120        in_memory: Option<GenericSharedMemory>,
121        source: BodySource,
122    ) -> TransmitBodyConnectHandler {
123        TransmitBodyConnectHandler {
124            stream,
125            task_source,
126            bytes_sender: None,
127            control_sender: Some(control_sender),
128            in_memory,
129            in_memory_done: false,
130            source,
131        }
132    }
133
134    /// Reset `in_memory_done`, called when a stream is
135    /// re-extracted from the source to support a re-direct.
136    pub(crate) fn reset_in_memory_done(&mut self) {
137        self.in_memory_done = false;
138    }
139
140    /// Re-extract the source to support streaming it again for a re-direct.
141    /// TODO: actually re-extract the source, instead of just cloning data, to support Blob.
142    fn re_extract(&mut self, chunk_request_receiver: IpcReceiver<BodyChunkRequest>) {
143        let mut body_handler = self.clone();
144        body_handler.reset_in_memory_done();
145
146        ROUTER.add_typed_route(
147            chunk_request_receiver,
148            Box::new(move |message| {
149                let request = message.unwrap();
150                match request {
151                    BodyChunkRequest::Connect(sender) => {
152                        body_handler.start_reading(sender);
153                    },
154                    BodyChunkRequest::Extract(receiver) => {
155                        body_handler.re_extract(receiver);
156                    },
157                    BodyChunkRequest::Chunk => body_handler.transmit_source(),
158                    // Note: this is actually sent from this process
159                    // by the TransmitBodyPromiseHandler when reading stops.
160                    BodyChunkRequest::Done => {
161                        body_handler.stop_reading(StopReading::Done);
162                    },
163                    // Note: this is actually sent from this process
164                    // by the TransmitBodyPromiseHandler when the stream errors.
165                    BodyChunkRequest::Error => {
166                        body_handler.stop_reading(StopReading::Error);
167                    },
168                }
169            }),
170        );
171    }
172
173    /// In case of re-direct, and of a source available in memory,
174    /// send it all in one chunk.
175    ///
176    /// TODO: this method should be deprecated
177    /// in favor of making `re_extract` actually re-extract a stream from the source.
178    /// See #26686
179    fn transmit_source(&mut self) {
180        if self.in_memory_done {
181            // Step 5.1.3
182            self.stop_reading(StopReading::Done);
183            return;
184        }
185
186        if let BodySource::Null = self.source {
187            panic!("ReadableStream(Null) sources should not re-direct.");
188        }
189
190        if let Some(bytes) = self.in_memory.clone() {
191            // The memoized bytes are sent so we mark it as done again
192            self.in_memory_done = true;
193            let _ = self
194                .bytes_sender
195                .as_ref()
196                .expect("No bytes sender to transmit source.")
197                .send(BodyChunkResponse::Chunk(bytes));
198            return;
199        }
200        warn!("Re-directs for file-based Blobs not supported yet.");
201    }
202
203    /// Take the IPC sender sent by `net`, so we can send body chunks with it.
204    /// Also the entry point to <https://fetch.spec.whatwg.org/#concept-request-transmit-body>
205    fn start_reading(&mut self, sender: IpcSender<BodyChunkResponse>) {
206        self.bytes_sender = Some(sender);
207
208        // If we're using an actual ReadableStream, acquire a reader for it.
209        if self.source == BodySource::Null {
210            let stream = self.stream.clone();
211            self.task_source
212                .queue(task!(start_reading_request_body_stream: move |cx| {
213                    // Step 1, Let body be request’s body.
214                    let rooted_stream = stream.root();
215
216                    // TODO: Step 2, If body is null.
217
218                    // Step 3, get a reader for stream.
219                    rooted_stream.acquire_default_reader(cx)
220                        .expect("Couldn't acquire a reader for the body stream.");
221
222                    // Note: this algorithm continues when the first chunk is requested by `net`.
223                }));
224        }
225    }
226
227    /// Drop the IPC sender sent by `net`
228    /// It is important to drop the control_sender as this will allow us to clean ourselves up.
229    /// Otherwise, the following cycle will happen: The control sender is owned by us which keeps the control receiver
230    /// alive in the router which keeps us alive.
231    fn stop_reading(&mut self, reason: StopReading) {
232        let bytes_sender = self
233            .bytes_sender
234            .take()
235            .expect("Stop reading called multiple times on TransmitBodyConnectHandler.");
236        match reason {
237            StopReading::Error => {
238                let _ = bytes_sender.send(BodyChunkResponse::Error);
239            },
240            StopReading::Done => {
241                let _ = bytes_sender.send(BodyChunkResponse::Done);
242            },
243        }
244        let _ = self.control_sender.take();
245    }
246
247    /// Step 4 and following of <https://fetch.spec.whatwg.org/#concept-request-transmit-body>
248    fn transmit_body_chunk(&mut self) {
249        if self.in_memory_done {
250            // Step 5.1.3
251            self.stop_reading(StopReading::Done);
252            return;
253        }
254
255        let stream = self.stream.clone();
256        let control_sender = self.control_sender.clone();
257        let bytes_sender = self
258            .bytes_sender
259            .clone()
260            .expect("No bytes sender to transmit chunk.");
261
262        // In case of the data being in-memory, send everything in one chunk, by-passing SpiderMonkey.
263        if let Some(bytes) = self.in_memory.clone() {
264            let _ = bytes_sender.send(BodyChunkResponse::Chunk(bytes));
265            // Mark this body as `done` so that we can stop reading in the next tick,
266            // matching the behavior of the promise-based flow
267            self.in_memory_done = true;
268            return;
269        }
270
271        self.task_source.queue(
272            task!(setup_native_body_promise_handler: move |cx| {
273                let rooted_stream = stream.root();
274                let global = rooted_stream.global();
275
276                // Step 4, the result of reading a chunk from body’s stream with reader.
277                let promise = rooted_stream.read_a_chunk(cx);
278
279                // Step 5, the parallel steps waiting for and handling the result of the read promise,
280                // are a combination of the promise native handler here,
281                // and the corresponding IPC route in `component::net::http_loader`.
282                rooted!(&in(cx) let mut promise_handler = Some(TransmitBodyPromiseHandler {
283                    bytes_sender: bytes_sender.clone(),
284                    stream: Dom::from_ref(&rooted_stream),
285                    control_sender: control_sender.clone().unwrap(),
286                }));
287
288                rooted!(&in(cx) let mut rejection_handler = Some(TransmitBodyPromiseRejectionHandler {
289                    bytes_sender,
290                    stream: Dom::from_ref(&rooted_stream),
291                    control_sender: control_sender.unwrap(),
292                }));
293
294                let handler =
295                    PromiseNativeHandler::new(cx, &global, promise_handler.take().map(|h| Box::new(h) as Box<_>), rejection_handler.take().map(|h| Box::new(h) as Box<_>));
296
297                let mut realm = enter_auto_realm(cx, &*global);
298                let realm = &mut realm.current_realm();
299                promise.append_native_handler(realm, &handler);
300            })
301        );
302    }
303}
304
305/// The handler of read promises of body streams used in
306/// <https://fetch.spec.whatwg.org/#concept-request-transmit-body>.
307#[derive(Clone, JSTraceable, MallocSizeOf)]
308#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
309struct TransmitBodyPromiseHandler {
310    #[no_trace]
311    bytes_sender: IpcSender<BodyChunkResponse>,
312    stream: Dom<ReadableStream>,
313    #[no_trace]
314    control_sender: IpcSender<BodyChunkRequest>,
315}
316
317impl js::gc::Rootable for TransmitBodyPromiseHandler {}
318
319impl Callback for TransmitBodyPromiseHandler {
320    /// Step 5 of <https://fetch.spec.whatwg.org/#concept-request-transmit-body>
321    fn callback(&self, cx: &mut CurrentRealm, v: HandleValue) {
322        let is_done = match get_read_promise_done(cx, &v) {
323            Ok(is_done) => is_done,
324            Err(_) => {
325                // Step 5.5, the "otherwise" steps.
326                // TODO: terminate fetch.
327                let _ = self.control_sender.send(BodyChunkRequest::Done);
328                return self.stream.stop_reading(cx);
329            },
330        };
331
332        if is_done {
333            // Step 5.3, the "done" steps.
334            // TODO: queue a fetch task on request to process request end-of-body.
335            let _ = self.control_sender.send(BodyChunkRequest::Done);
336            return self.stream.stop_reading(cx);
337        }
338
339        let chunk = match get_read_promise_bytes(cx, &v) {
340            Ok(chunk) => chunk,
341            Err(_) => {
342                // Step 5.5, the "otherwise" steps.
343                let _ = self.control_sender.send(BodyChunkRequest::Error);
344                return self.stream.stop_reading(cx);
345            },
346        };
347
348        // Step 5.1 and 5.2, transmit chunk.
349        // Send the chunk to the body transmitter in net::http_loader::obtain_response.
350        // TODO: queue a fetch task on request to process request body for request.
351        let _ = self
352            .bytes_sender
353            .send(BodyChunkResponse::Chunk(GenericSharedMemory::from_vec(
354                chunk,
355            )));
356    }
357}
358
359/// The handler of read promises rejection of body streams used in
360/// <https://fetch.spec.whatwg.org/#concept-request-transmit-body>.
361#[derive(Clone, JSTraceable, MallocSizeOf)]
362#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
363struct TransmitBodyPromiseRejectionHandler {
364    #[no_trace]
365    bytes_sender: IpcSender<BodyChunkResponse>,
366    stream: Dom<ReadableStream>,
367    #[no_trace]
368    control_sender: IpcSender<BodyChunkRequest>,
369}
370
371impl js::gc::Rootable for TransmitBodyPromiseRejectionHandler {}
372
373impl Callback for TransmitBodyPromiseRejectionHandler {
374    /// <https://fetch.spec.whatwg.org/#concept-request-transmit-body>
375    fn callback(&self, cx: &mut CurrentRealm, _v: HandleValue) {
376        // Step 5.4, the "rejection" steps.
377        let _ = self.control_sender.send(BodyChunkRequest::Error);
378        self.stream.stop_reading(cx);
379    }
380}
381
382/// <https://fetch.spec.whatwg.org/#body-with-type>
383pub(crate) struct ExtractedBody {
384    /// <https://fetch.spec.whatwg.org/#concept-body-stream>
385    pub(crate) stream: DomRoot<ReadableStream>,
386    /// <https://fetch.spec.whatwg.org/#concept-body-source>
387    pub(crate) source: BodySource,
388    /// <https://fetch.spec.whatwg.org/#concept-body-total-bytes>
389    pub(crate) total_bytes: Option<usize>,
390    /// <https://fetch.spec.whatwg.org/#body-with-type-type>
391    pub(crate) content_type: Option<DOMString>,
392}
393
394impl ExtractedBody {
395    /// Build a request body from the extracted body,
396    /// to be sent over IPC to net to use with `concept-request-transmit-body`,
397    /// see <https://fetch.spec.whatwg.org/#concept-request-transmit-body>.
398    ///
399    /// Also returning the corresponding readable stream,
400    /// to be stored on the request in script,
401    /// and potentially used as part of `consume_body`,
402    /// see <https://fetch.spec.whatwg.org/#concept-body-consume-body>
403    ///
404    /// Transmitting a body over fetch, and consuming it in script,
405    /// are mutually exclusive operations, since each will lock the stream to a reader.
406    pub(crate) fn into_net_request_body(self) -> (RequestBody, DomRoot<ReadableStream>) {
407        let ExtractedBody {
408            stream,
409            total_bytes,
410            content_type: _,
411            source,
412        } = self;
413
414        // First, setup some infra to be used to transmit body
415        //  from `components::script` to `components::net`.
416        let (chunk_request_sender, chunk_request_receiver) = ipc::channel().unwrap();
417
418        let trusted_stream = Trusted::new(&*stream);
419
420        let global = stream.global();
421        let task_source = global.task_manager().networking_task_source();
422
423        // In case of the data being in-memory, send everything in one chunk, by-passing SM.
424        // Empty extracted bodies are always representable as an in-memory empty payload.
425        let in_memory = stream.get_in_memory_bytes().or_else(|| {
426            if total_bytes == Some(0) {
427                Some(GenericSharedMemory::from_bytes(&[]))
428            } else {
429                None
430            }
431        });
432
433        let net_source = match source {
434            BodySource::Null => NetBodySource::Null,
435            _ => NetBodySource::Object,
436        };
437
438        let mut body_handler = TransmitBodyConnectHandler::new(
439            trusted_stream,
440            task_source.into(),
441            chunk_request_sender.clone(),
442            in_memory,
443            source,
444        );
445
446        ROUTER.add_typed_route(
447            chunk_request_receiver,
448            Box::new(move |message| {
449                match message.unwrap() {
450                    BodyChunkRequest::Connect(sender) => {
451                        body_handler.start_reading(sender);
452                    },
453                    BodyChunkRequest::Extract(receiver) => {
454                        body_handler.re_extract(receiver);
455                    },
456                    BodyChunkRequest::Chunk => body_handler.transmit_body_chunk(),
457                    // Note: this is actually sent from this process
458                    // by the TransmitBodyPromiseHandler when reading stops.
459                    BodyChunkRequest::Done => {
460                        body_handler.stop_reading(StopReading::Done);
461                    },
462                    // Note: this is actually sent from this process
463                    // by the TransmitBodyPromiseHandler when the stream errors.
464                    BodyChunkRequest::Error => {
465                        body_handler.stop_reading(StopReading::Error);
466                    },
467                }
468            }),
469        );
470
471        // Return `components::net` view into this request body,
472        // which can be used by `net` to transmit it over the network.
473        let request_body = RequestBody::new(chunk_request_sender, net_source, total_bytes);
474
475        // Also return the stream for this body, which can be used by script to consume it.
476        (request_body, stream)
477    }
478
479    /// Is the data of the stream of this extracted body available in memory?
480    pub(crate) fn in_memory(&self) -> bool {
481        self.stream.in_memory()
482    }
483}
484
485/// <https://fetch.spec.whatwg.org/#concept-bodyinit-extract>
486pub(crate) trait Extractable {
487    fn extract(
488        &self,
489        cx: &mut js::context::JSContext,
490        global: &GlobalScope,
491        keep_alive: bool,
492    ) -> Fallible<ExtractedBody>;
493}
494
495/// Part of <https://fetch.spec.whatwg.org/#concept-bodyinit-extract>
496fn stream_from_body_init_bytes(
497    cx: &mut js::context::JSContext,
498    global: &GlobalScope,
499    bytes: Vec<u8>,
500) -> Fallible<DomRoot<ReadableStream>> {
501    // Step 4: "Otherwise, set stream to a new ReadableStream object, and set up stream with byte reading support."
502    // Step 11: "If source is a byte sequence, then set action to a step that returns source and length to source’s length."
503    // Step 12.1: "Whenever one or more bytes are available and stream is not errored, enqueue the result of creating a Uint8Array from the available bytes into stream."
504    // Step 12.1: "When running action is done, close stream."
505    ReadableStream::new_from_bytes_with_byte_reading_support(cx, global, bytes)
506}
507
508impl Extractable for BodyInit {
509    /// <https://fetch.spec.whatwg.org/#concept-bodyinit-extract>
510    fn extract(
511        &self,
512        cx: &mut js::context::JSContext,
513        global: &GlobalScope,
514        keep_alive: bool,
515    ) -> Fallible<ExtractedBody> {
516        match self {
517            BodyInit::String(s) => s.extract(cx, global, keep_alive),
518            BodyInit::URLSearchParams(usp) => usp.extract(cx, global, keep_alive),
519            BodyInit::Blob(b) => b.extract(cx, global, keep_alive),
520            BodyInit::FormData(formdata) => formdata.extract(cx, global, keep_alive),
521            BodyInit::ArrayBuffer(typedarray) => {
522                let bytes = typedarray.to_vec();
523                let total_bytes = bytes.len();
524                let stream = stream_from_body_init_bytes(cx, global, bytes)?;
525                Ok(ExtractedBody {
526                    stream,
527                    total_bytes: Some(total_bytes),
528                    content_type: None,
529                    source: BodySource::Object,
530                })
531            },
532            BodyInit::ArrayBufferView(typedarray) => {
533                let bytes = typedarray.to_vec();
534                let total_bytes = bytes.len();
535                let stream = stream_from_body_init_bytes(cx, global, bytes)?;
536                Ok(ExtractedBody {
537                    stream,
538                    total_bytes: Some(total_bytes),
539                    content_type: None,
540                    source: BodySource::Object,
541                })
542            },
543            BodyInit::ReadableStream(stream) => {
544                // If keepalive is true, then throw a TypeError.
545                if keep_alive {
546                    return Err(Error::Type(
547                        c"The body's stream is for a keepalive request".to_owned(),
548                    ));
549                }
550                // If object is disturbed or locked, then throw a TypeError.
551                if stream.is_locked() || stream.is_disturbed() {
552                    return Err(Error::Type(
553                        c"The body's stream is disturbed or locked".to_owned(),
554                    ));
555                }
556
557                Ok(ExtractedBody {
558                    stream: stream.clone(),
559                    total_bytes: None,
560                    content_type: None,
561                    source: BodySource::Null,
562                })
563            },
564        }
565    }
566}
567
568impl Extractable for Vec<u8> {
569    fn extract(
570        &self,
571        cx: &mut js::context::JSContext,
572        global: &GlobalScope,
573        _keep_alive: bool,
574    ) -> Fallible<ExtractedBody> {
575        let bytes = self.clone();
576        let total_bytes = self.len();
577        let stream = stream_from_body_init_bytes(cx, global, bytes)?;
578        Ok(ExtractedBody {
579            stream,
580            total_bytes: Some(total_bytes),
581            content_type: None,
582            // A vec is used only in `submit_entity_body`.
583            source: BodySource::Object,
584        })
585    }
586}
587
588impl Extractable for Blob {
589    fn extract(
590        &self,
591        cx: &mut js::context::JSContext,
592        _global: &GlobalScope,
593        _keep_alive: bool,
594    ) -> Fallible<ExtractedBody> {
595        let blob_type = self.Type();
596        let content_type = if blob_type.is_empty() {
597            None
598        } else {
599            Some(blob_type)
600        };
601        let total_bytes = self.Size() as usize;
602        let stream = self.get_stream(cx)?;
603        Ok(ExtractedBody {
604            stream,
605            total_bytes: Some(total_bytes),
606            content_type,
607            source: BodySource::Object,
608        })
609    }
610}
611
612impl Extractable for DOMString {
613    fn extract(
614        &self,
615        cx: &mut js::context::JSContext,
616        global: &GlobalScope,
617        _keep_alive: bool,
618    ) -> Fallible<ExtractedBody> {
619        let bytes = self.as_bytes().to_owned();
620        let total_bytes = bytes.len();
621        let content_type = Some(DOMString::from("text/plain;charset=UTF-8"));
622        let stream = stream_from_body_init_bytes(cx, global, bytes)?;
623        Ok(ExtractedBody {
624            stream,
625            total_bytes: Some(total_bytes),
626            content_type,
627            source: BodySource::Object,
628        })
629    }
630}
631
632impl Extractable for FormData {
633    fn extract(
634        &self,
635        cx: &mut js::context::JSContext,
636        global: &GlobalScope,
637        _keep_alive: bool,
638    ) -> Fallible<ExtractedBody> {
639        let boundary = generate_boundary();
640        let bytes = encode_multipart_form_data(&mut self.datums(), boundary.clone(), UTF_8);
641        let total_bytes = bytes.len();
642        let content_type = Some(DOMString::from(format!(
643            "multipart/form-data; boundary={}",
644            boundary
645        )));
646        let stream = stream_from_body_init_bytes(cx, global, bytes)?;
647        Ok(ExtractedBody {
648            stream,
649            total_bytes: Some(total_bytes),
650            content_type,
651            source: BodySource::Object,
652        })
653    }
654}
655
656impl Extractable for URLSearchParams {
657    fn extract(
658        &self,
659        cx: &mut js::context::JSContext,
660        global: &GlobalScope,
661        _keep_alive: bool,
662    ) -> Fallible<ExtractedBody> {
663        let bytes = self.serialize_utf8().into_bytes();
664        let total_bytes = bytes.len();
665        let content_type = Some(DOMString::from(
666            "application/x-www-form-urlencoded;charset=UTF-8",
667        ));
668        let stream = stream_from_body_init_bytes(cx, global, bytes)?;
669        Ok(ExtractedBody {
670            stream,
671            total_bytes: Some(total_bytes),
672            content_type,
673            source: BodySource::Object,
674        })
675    }
676}
677
678#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
679pub(crate) enum BodyType {
680    Blob,
681    Bytes,
682    FormData,
683    Json,
684    Text,
685    ArrayBuffer,
686}
687
688pub(crate) enum FetchedData {
689    Text(String),
690    Json(RootedTraceableBox<Heap<JSValue>>),
691    BlobData(DomRoot<Blob>),
692    Bytes(RootedTraceableBox<Heap<*mut JSObject>>),
693    FormData(DomRoot<FormData>),
694    ArrayBuffer(RootedTraceableBox<Heap<*mut JSObject>>),
695    JSException(RootedTraceableBox<Heap<JSVal>>),
696}
697
698/// <https://fetch.spec.whatwg.org/#concept-body-consume-body>
699/// <https://fetch.spec.whatwg.org/#body-fully-read>
700/// A combination of parts of both algorithms,
701/// `body-fully-read` can be fully implemented, and separated, later,
702/// see #36049.
703pub(crate) fn consume_body<T: BodyMixin + DomObject>(
704    cx: &mut js::context::JSContext,
705    object: &T,
706    body_type: BodyType,
707) -> Rc<Promise> {
708    let global = object.global();
709
710    // Enter the realm of the object whose body is being consumed.
711    let mut realm = enter_auto_realm(cx, &*global);
712    let cx: &mut _ = &mut realm.current_realm();
713
714    // Let promise be a new promise.
715    // Note: re-ordered so we can return the promise below.
716    let promise = Promise::new_in_realm(cx);
717
718    // If object is unusable, then return a promise rejected with a TypeError.
719    if object.is_unusable() {
720        promise.reject_error_with_cx(
721            cx,
722            Error::Type(c"The body's stream is disturbed or locked".to_owned()),
723        );
724        return promise;
725    }
726
727    let stream = match object.body() {
728        Some(stream) => stream,
729        None => {
730            // If object’s body is null, then run successSteps with an empty byte sequence.
731            let mime_type = object.get_mime_type(cx);
732            resolve_result_promise(cx, body_type, &promise, mime_type, Vec::with_capacity(0));
733            return promise;
734        },
735    };
736
737    // <https://fetch.spec.whatwg.org/#concept-body-consume-body>
738    // Otherwise, fully read object’s body given successSteps, errorSteps, and object’s relevant global object.
739    //
740    // <https://fetch.spec.whatwg.org/#body-fully-read>
741    // Let reader be the result of getting a reader for body’s stream.
742    // Read all bytes from reader, given successSteps and errorSteps.
743    //
744    // <https://streams.spec.whatwg.org/#readable-stream-default-reader-read>
745    // Set stream.[[disturbed]] to true.
746    // Otherwise, if stream.[[state]] is "errored", perform readRequest’s error steps given stream.[[storedError]].
747    //
748    // If the body stream is already errored (for example, the fetch was aborted after the Response exists),
749    // the normal fully read path would reject with [[storedError]] but would also mark the stream disturbed.
750    // Once the stream is disturbed, later calls reject with TypeError ("disturbed or locked") instead of the
751    // original AbortError. This early return rejects with the same [[storedError]] without disturbing the
752    // stream, so repeated calls (for example, calling text() twice) keep rejecting with AbortError.
753    if stream.is_errored() {
754        rooted!(&in(cx) let mut stored_error = UndefinedValue());
755        stream.get_stored_error(stored_error.handle_mut());
756        promise.reject_with_cx(cx, stored_error.handle());
757        return promise;
758    }
759
760    // Note: from `fully_read`.
761    // Let reader be the result of getting a reader for body’s stream.
762    // If that threw an exception,
763    // then run errorSteps with that exception and return.
764    let reader = match stream.acquire_default_reader(cx) {
765        Ok(r) => r,
766        Err(e) => {
767            promise.reject_error_with_cx(cx, e);
768            return promise;
769        },
770    };
771
772    // Let errorSteps given error be to reject promise with error.
773    let error_promise = promise.clone();
774
775    // Let successSteps given a byte sequence data be to resolve promise
776    // with the result of running convertBytesToJSValue with data.
777    // If that threw an exception, then run errorSteps with that exception.
778    let mime_type = object.get_mime_type(cx);
779    let success_promise = promise.clone();
780
781    // Read all bytes from reader, given successSteps and errorSteps.
782    // Note: spec uses an intermediary concept of `fully_read`,
783    // which seems useful when invoking fetch from other places.
784    // TODO: #36049
785    reader.read_all_bytes(
786        cx,
787        Rc::new(move |cx, bytes: &[u8]| {
788            resolve_result_promise(
789                cx,
790                body_type,
791                &success_promise,
792                mime_type.clone(),
793                bytes.to_vec(),
794            );
795        }),
796        Rc::new(move |cx, v| {
797            error_promise.reject_with_cx(cx, v);
798        }),
799    );
800
801    promise
802}
803
804/// The success steps of
805/// <https://fetch.spec.whatwg.org/#concept-body-consume-body>.
806fn resolve_result_promise(
807    cx: &mut js::context::JSContext,
808    body_type: BodyType,
809    promise: &Promise,
810    mime_type: Vec<u8>,
811    body: Vec<u8>,
812) {
813    let pkg_data_results = run_package_data_algorithm(cx, body, body_type, mime_type);
814
815    match pkg_data_results {
816        Ok(results) => {
817            match results {
818                FetchedData::Text(s) => promise.resolve_native_with_cx(cx, &USVString(s)),
819                FetchedData::Json(j) => promise.resolve_native_with_cx(cx, &j),
820                FetchedData::BlobData(b) => promise.resolve_native_with_cx(cx, &b),
821                FetchedData::FormData(f) => promise.resolve_native_with_cx(cx, &f),
822                FetchedData::Bytes(b) => promise.resolve_native_with_cx(cx, &b),
823                FetchedData::ArrayBuffer(a) => promise.resolve_native_with_cx(cx, &a),
824                FetchedData::JSException(e) => {
825                    promise.reject_native(&e.handle(), CanGc::from_cx(cx))
826                },
827            };
828        },
829        Err(err) => promise.reject_error_with_cx(cx, err),
830    }
831}
832
833/// The algorithm that takes a byte sequence
834/// and returns a JavaScript value or throws an exception of
835/// <https://fetch.spec.whatwg.org/#concept-body-consume-body>.
836fn run_package_data_algorithm(
837    cx: &mut js::context::JSContext,
838    bytes: Vec<u8>,
839    body_type: BodyType,
840    mime_type: Vec<u8>,
841) -> Fallible<FetchedData> {
842    let mime = &*mime_type;
843    let realm = CurrentRealm::assert(cx);
844    let global = GlobalScope::from_current_realm(&realm);
845    match body_type {
846        BodyType::Text => run_text_data_algorithm(bytes),
847        BodyType::Json => run_json_data_algorithm(cx, bytes),
848        BodyType::Blob => run_blob_data_algorithm(cx, &global, bytes, mime),
849        BodyType::FormData => run_form_data_algorithm(cx, &global, bytes, mime),
850        BodyType::ArrayBuffer => run_array_buffer_data_algorithm(cx, bytes),
851        BodyType::Bytes => run_bytes_data_algorithm(cx, bytes),
852    }
853}
854
855/// <https://fetch.spec.whatwg.org/#ref-for-concept-body-consume-body%E2%91%A4>
856fn run_text_data_algorithm(bytes: Vec<u8>) -> Fallible<FetchedData> {
857    // This implements the Encoding standard's "decode UTF-8", which removes the
858    // BOM if present.
859    let no_bom_bytes = if bytes.starts_with(b"\xEF\xBB\xBF") {
860        &bytes[3..]
861    } else {
862        &bytes
863    };
864    Ok(FetchedData::Text(
865        String::from_utf8_lossy(no_bom_bytes).into_owned(),
866    ))
867}
868
869#[expect(unsafe_code)]
870/// <https://fetch.spec.whatwg.org/#ref-for-concept-body-consume-body%E2%91%A3>
871fn run_json_data_algorithm(
872    cx: &mut js::context::JSContext,
873    bytes: Vec<u8>,
874) -> Fallible<FetchedData> {
875    // The JSON spec allows implementations to either ignore UTF-8 BOM or treat it as an error.
876    // `JS_ParseJSON` treats this as an error, so it is necessary for us to strip it if present.
877    //
878    // https://datatracker.ietf.org/doc/html/rfc8259#section-8.1
879    let json_text = decode_to_utf16_with_bom_removal(&bytes, UTF_8);
880    rooted!(&in(cx) let mut rval = UndefinedValue());
881    unsafe {
882        if !JS_ParseJSON(
883            cx,
884            json_text.as_ptr(),
885            json_text.len() as u32,
886            rval.handle_mut(),
887        ) {
888            rooted!(&in(cx) let mut exception = UndefinedValue());
889            assert!(JS_GetPendingException(cx, exception.handle_mut()));
890            JS_ClearPendingException(cx);
891            return Ok(FetchedData::JSException(RootedTraceableBox::from_box(
892                Heap::boxed(exception.get()),
893            )));
894        }
895        let rooted_heap = RootedTraceableBox::from_box(Heap::boxed(rval.get()));
896        Ok(FetchedData::Json(rooted_heap))
897    }
898}
899
900/// <https://fetch.spec.whatwg.org/#ref-for-concept-body-consume-body%E2%91%A0>
901fn run_blob_data_algorithm(
902    cx: &mut js::context::JSContext,
903    root: &GlobalScope,
904    bytes: Vec<u8>,
905    mime: &[u8],
906) -> Fallible<FetchedData> {
907    let mime_string = if let Ok(s) = String::from_utf8(mime.to_vec()) {
908        s
909    } else {
910        "".to_string()
911    };
912    let blob = Blob::new(
913        cx,
914        root,
915        BlobImpl::new_from_bytes(bytes, normalize_type_string(&mime_string)),
916    );
917    Ok(FetchedData::BlobData(blob))
918}
919
920fn extract_name_from_content_disposition(headers: &HeaderMap) -> Option<String> {
921    let cd = headers.get(CONTENT_DISPOSITION)?.to_str().ok()?;
922
923    for part in cd.split(';').map(|s| s.trim()) {
924        if let Some(rest) = part.strip_prefix("name=") {
925            let v = rest.trim();
926            let v = v.strip_prefix('"').unwrap_or(v);
927            let v = v.strip_suffix('"').unwrap_or(v);
928            return Some(v.to_string());
929        }
930    }
931    None
932}
933
934fn extract_filename_from_content_disposition(headers: &HeaderMap) -> Option<String> {
935    let cd = headers.get(CONTENT_DISPOSITION)?.to_str().ok()?;
936    if let Some(index) = cd.find("filename=") {
937        let start = index + "filename=".len();
938        return Some(
939            cd.get(start..)
940                .unwrap_or_default()
941                .trim_matches('"')
942                .to_owned(),
943        );
944    }
945    if let Some(index) = cd.find("filename*=UTF-8''") {
946        let start = index + "filename*=UTF-8''".len();
947        return Some(
948            cd.get(start..)
949                .unwrap_or_default()
950                .trim_matches('"')
951                .to_owned(),
952        );
953    }
954    None
955}
956
957fn content_type_from_headers(headers: &HeaderMap) -> Result<String, Error> {
958    match headers.get(CONTENT_TYPE) {
959        Some(value) => Ok(value
960            .to_str()
961            .map_err(|_| Error::Type(c"Inappropriate MIME-type for Body".to_owned()))?
962            .to_string()),
963        None => Ok("text/plain".to_string()),
964    }
965}
966
967fn append_form_data_entry_from_part(
968    cx: &mut js::context::JSContext,
969    root: &GlobalScope,
970    formdata: &FormData,
971    headers: &HeaderMap,
972    body: Vec<u8>,
973) -> Fallible<()> {
974    let Some(name) = extract_name_from_content_disposition(headers) else {
975        return Ok(());
976    };
977    // 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.
978    let filename = extract_filename_from_content_disposition(headers);
979    if let Some(filename) = filename {
980        // 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.
981        //
982        // The name attribute of the File object must have the value of the `filename` parameter of the part.
983        //
984        // 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.
985        let content_type = content_type_from_headers(headers)?;
986        let file = File::new(
987            root,
988            BlobImpl::new_from_bytes(body, normalize_type_string(&content_type)),
989            DOMString::from(filename),
990            None,
991            CanGc::from_cx(cx),
992        );
993        let blob = file.upcast::<Blob>();
994        formdata.Append_(USVString(name), blob, None);
995    } else {
996        // 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.
997
998        let (value, _) = UTF_8.decode_without_bom_handling(&body);
999        formdata.Append(USVString(name), USVString(value.to_string()));
1000    }
1001    Ok(())
1002}
1003
1004fn append_multipart_nodes(
1005    cx: &mut js::context::JSContext,
1006    root: &GlobalScope,
1007    formdata: &FormData,
1008    nodes: Vec<Node>,
1009) -> Fallible<()> {
1010    for node in nodes {
1011        match node {
1012            Node::Part(part) => {
1013                append_form_data_entry_from_part(cx, root, formdata, &part.headers, part.body)?;
1014            },
1015            Node::File(file_part) => {
1016                let body = fs::read(&file_part.path)
1017                    .map_err(|_| Error::Type(c"file part could not be read".to_owned()))?;
1018                append_form_data_entry_from_part(cx, root, formdata, &file_part.headers, body)?;
1019            },
1020            Node::Multipart((_, inner)) => {
1021                append_multipart_nodes(cx, root, formdata, inner)?;
1022            },
1023        }
1024    }
1025    Ok(())
1026}
1027
1028/// <https://fetch.spec.whatwg.org/#ref-for-concept-body-consume-body%E2%91%A2>
1029fn run_form_data_algorithm(
1030    cx: &mut js::context::JSContext,
1031    root: &GlobalScope,
1032    bytes: Vec<u8>,
1033    mime: &[u8],
1034) -> Fallible<FetchedData> {
1035    // The formData() method steps are to return the result of running consume body
1036    // with this and the following steps given a byte sequence bytes:
1037    let mime_str = str::from_utf8(mime).unwrap_or_default();
1038    let mime: Mime = mime_str
1039        .parse()
1040        .map_err(|_| Error::Type(c"Inappropriate MIME-type for Body".to_owned()))?;
1041
1042    // Let mimeType be the result of get the MIME type with this.
1043    //
1044    // If mimeType is non-null, then switch on mimeType’s essence and run the corresponding steps:
1045    if mime.type_() == mime::MULTIPART && mime.subtype() == mime::FORM_DATA {
1046        // "multipart/form-data"
1047        // Parse bytes, using the value of the `boundary` parameter from mimeType,
1048        // per the rules set forth in Returning Values from Forms: multipart/form-data. [RFC7578]
1049        let mut headers = HeaderMap::new();
1050        headers.insert(
1051            CONTENT_TYPE,
1052            mime_str
1053                .parse()
1054                .map_err(|_| Error::Type(c"Inappropriate MIME-type for Body".to_owned()))?,
1055        );
1056
1057        if let Some(boundary) = mime.get_param(mime::BOUNDARY) {
1058            let closing_boundary = format!("--{}--", boundary.as_str()).into_bytes();
1059            let trimmed_bytes = bytes.strip_suffix(b"\r\n").unwrap_or(&bytes);
1060            if trimmed_bytes == closing_boundary {
1061                let formdata = FormData::new(None, root, CanGc::from_cx(cx));
1062                return Ok(FetchedData::FormData(formdata));
1063            }
1064        }
1065
1066        let mut cursor = Cursor::new(bytes);
1067        // If that fails for some reason, then throw a TypeError.
1068        let nodes = read_multipart_body(&mut cursor, &headers, false)
1069            .map_err(|_| Error::Type(c"Inappropriate MIME-type for Body".to_owned()))?;
1070        // The above is a rough approximation of what is needed for `multipart/form-data`,
1071        // a more detailed parsing specification is to be written. Volunteers welcome.
1072
1073        // Return a new FormData object, appending each entry, resulting from the parsing operation, to its entry list.
1074        let formdata = FormData::new(None, root, CanGc::from_cx(cx));
1075
1076        append_multipart_nodes(cx, root, &formdata, nodes)?;
1077
1078        return Ok(FetchedData::FormData(formdata));
1079    }
1080
1081    if mime.type_() == mime::APPLICATION && mime.subtype() == mime::WWW_FORM_URLENCODED {
1082        // "application/x-www-form-urlencoded"
1083        // Let entries be the result of parsing bytes.
1084        //
1085        // Return a new FormData object whose entry list is entries.
1086        let entries = form_urlencoded::parse(&bytes);
1087        let formdata = FormData::new(None, root, CanGc::from_cx(cx));
1088        for (k, e) in entries {
1089            formdata.Append(USVString(k.into_owned()), USVString(e.into_owned()));
1090        }
1091        return Ok(FetchedData::FormData(formdata));
1092    }
1093
1094    // Throw a TypeError.
1095    Err(Error::Type(c"Inappropriate MIME-type for Body".to_owned()))
1096}
1097
1098/// <https://fetch.spec.whatwg.org/#ref-for-concept-body-consume-body%E2%91%A1>
1099fn run_bytes_data_algorithm(
1100    cx: &mut js::context::JSContext,
1101    bytes: Vec<u8>,
1102) -> Fallible<FetchedData> {
1103    rooted!(&in(cx) let mut array_buffer_ptr = ptr::null_mut::<JSObject>());
1104
1105    create_buffer_source::<Uint8>(
1106        cx.into(),
1107        &bytes,
1108        array_buffer_ptr.handle_mut(),
1109        CanGc::from_cx(cx),
1110    )
1111    .map_err(|_| Error::JSFailed)?;
1112
1113    let rooted_heap = RootedTraceableBox::from_box(Heap::boxed(array_buffer_ptr.get()));
1114    Ok(FetchedData::Bytes(rooted_heap))
1115}
1116
1117/// <https://fetch.spec.whatwg.org/#ref-for-concept-body-consume-body>
1118pub(crate) fn run_array_buffer_data_algorithm(
1119    cx: &mut js::context::JSContext,
1120    bytes: Vec<u8>,
1121) -> Fallible<FetchedData> {
1122    rooted!(&in(cx) let mut array_buffer_ptr = ptr::null_mut::<JSObject>());
1123
1124    create_buffer_source::<ArrayBufferU8>(
1125        cx.into(),
1126        &bytes,
1127        array_buffer_ptr.handle_mut(),
1128        CanGc::from_cx(cx),
1129    )
1130    .map_err(|_| Error::JSFailed)?;
1131
1132    let rooted_heap = RootedTraceableBox::from_box(Heap::boxed(array_buffer_ptr.get()));
1133    Ok(FetchedData::ArrayBuffer(rooted_heap))
1134}
1135
1136#[expect(unsafe_code)]
1137pub(crate) fn decode_to_utf16_with_bom_removal(
1138    bytes: &[u8],
1139    encoding: &'static Encoding,
1140) -> Vec<u16> {
1141    let mut decoder = encoding.new_decoder_with_bom_removal();
1142    let capacity = decoder
1143        .max_utf16_buffer_length(bytes.len())
1144        .expect("Overflow");
1145    let mut utf16 = Vec::with_capacity(capacity);
1146    let extra = unsafe { slice::from_raw_parts_mut(utf16.as_mut_ptr(), capacity) };
1147    let (_, read, written, _) = decoder.decode_to_utf16(bytes, extra, true);
1148    assert_eq!(read, bytes.len());
1149    unsafe { utf16.set_len(written) }
1150    utf16
1151}
1152
1153/// <https://fetch.spec.whatwg.org/#body>
1154pub(crate) trait BodyMixin {
1155    /// <https://fetch.spec.whatwg.org/#dom-body-bodyused>
1156    fn is_body_used(&self) -> bool;
1157    /// <https://fetch.spec.whatwg.org/#body-unusable>
1158    fn is_unusable(&self) -> bool;
1159    /// <https://fetch.spec.whatwg.org/#dom-body-body>
1160    fn body(&self) -> Option<DomRoot<ReadableStream>>;
1161    /// <https://fetch.spec.whatwg.org/#concept-body-mime-type>
1162    fn get_mime_type(&self, cx: &mut js::context::JSContext) -> Vec<u8>;
1163}