Skip to main content

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