runtime/globals/fetch/
body.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 */
6
7use std::fmt::{Display, Formatter};
8use std::pin::Pin;
9use std::task::Poll;
10use std::{fmt, task};
11
12use bytes::Bytes;
13use form_urlencoded::Serializer;
14use http::header::CONTENT_TYPE;
15use http::{HeaderMap, HeaderValue};
16use http_body_util::{BodyExt as _, Full};
17use hyper::body::{Frame, Incoming, SizeHint};
18use ion::conversions::FromValue;
19use ion::{Context, Error, ErrorKind, Traceable, Value};
20use mozjs::jsapi::Heap;
21use mozjs::jsval::JSVal;
22use pin_project_lite::pin_project;
23
24use crate::globals::file::{Blob, BufferSource};
25use crate::globals::url::URLSearchParams;
26
27#[derive(Debug, Clone, Traceable)]
28#[non_exhaustive]
29enum FetchBodyInner {
30	None,
31	Bytes(#[trace(no_trace)] Bytes),
32}
33
34#[derive(Clone, Debug, Traceable)]
35#[non_exhaustive]
36pub enum FetchBodyKind {
37	String,
38	Blob(String),
39	URLSearchParams,
40}
41
42impl Display for FetchBodyKind {
43	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
44		match self {
45			FetchBodyKind::String => f.write_str("text/plain;charset=UTF-8"),
46			FetchBodyKind::Blob(mime) => f.write_str(mime),
47			FetchBodyKind::URLSearchParams => f.write_str("application/x-www-form-urlencoded;charset=UTF-8"),
48		}
49	}
50}
51
52#[derive(Debug, Traceable)]
53pub struct FetchBody {
54	body: FetchBodyInner,
55	source: Option<Box<Heap<JSVal>>>,
56	pub(crate) kind: Option<FetchBodyKind>,
57}
58
59impl FetchBody {
60	pub fn is_none(&self) -> bool {
61		matches!(&self.body, FetchBodyInner::None)
62	}
63
64	pub fn is_empty(&self) -> bool {
65		match &self.body {
66			FetchBodyInner::None => true,
67			FetchBodyInner::Bytes(bytes) => bytes.is_empty(),
68		}
69	}
70
71	pub fn len(&self) -> Option<usize> {
72		match &self.body {
73			FetchBodyInner::None => None,
74			FetchBodyInner::Bytes(bytes) => Some(bytes.len()),
75		}
76	}
77
78	pub fn is_stream(&self) -> bool {
79		!matches!(&self.body, FetchBodyInner::None | FetchBodyInner::Bytes(_))
80	}
81
82	pub fn to_http_body(&self) -> Body {
83		match &self.body {
84			FetchBodyInner::None => Body::Empty,
85			FetchBodyInner::Bytes(bytes) => Body::from(bytes.clone()),
86		}
87	}
88
89	pub async fn read_to_bytes(&self) -> ion::Result<Vec<u8>> {
90		Ok(self.to_http_body().collect().await?.to_bytes().to_vec())
91	}
92
93	pub(crate) fn add_content_type_header(&self, headers: &mut HeaderMap) {
94		if let Some(kind) = &self.kind
95			&& !headers.contains_key(CONTENT_TYPE)
96		{
97			headers.append(CONTENT_TYPE, HeaderValue::from_str(&kind.to_string()).unwrap());
98		}
99	}
100}
101
102impl Clone for FetchBody {
103	fn clone(&self) -> FetchBody {
104		FetchBody {
105			body: self.body.clone(),
106			source: self.source.as_ref().map(|s| Heap::boxed(s.get())),
107			kind: self.kind.clone(),
108		}
109	}
110}
111
112impl Default for FetchBody {
113	fn default() -> FetchBody {
114		FetchBody {
115			body: FetchBodyInner::None,
116			source: None,
117			kind: None,
118		}
119	}
120}
121
122impl<'cx> FromValue<'cx> for FetchBody {
123	type Config = ();
124	fn from_value(cx: &'cx Context, value: &Value, strict: bool, _: ()) -> ion::Result<FetchBody> {
125		if value.handle().is_string() {
126			return Ok(FetchBody {
127				body: FetchBodyInner::Bytes(Bytes::from(String::from_value(cx, value, strict, ()).unwrap())),
128				source: Some(Heap::boxed(value.get())),
129				kind: Some(FetchBodyKind::String),
130			});
131		} else if value.handle().is_object() {
132			if let Ok(source) = BufferSource::from_value(cx, value, strict, false) {
133				return Ok(FetchBody {
134					body: FetchBodyInner::Bytes(source.to_bytes()),
135					source: Some(Heap::boxed(value.get())),
136					kind: None,
137				});
138			} else if let Ok(blob) = <&Blob>::from_value(cx, value, strict, ()) {
139				return Ok(FetchBody {
140					body: FetchBodyInner::Bytes(blob.bytes.clone()),
141					source: Some(Heap::boxed(value.get())),
142					kind: blob.kind.clone().map(FetchBodyKind::Blob),
143				});
144			} else if let Ok(search_params) = <&URLSearchParams>::from_value(cx, value, strict, ()) {
145				return Ok(FetchBody {
146					body: FetchBodyInner::Bytes(Bytes::from(
147						Serializer::new(String::new()).extend_pairs(search_params.pairs()).finish(),
148					)),
149					source: Some(Heap::boxed(value.get())),
150					kind: Some(FetchBodyKind::URLSearchParams),
151				});
152			}
153		}
154		Err(Error::new("Expected Valid Body", ErrorKind::Type))
155	}
156}
157
158pin_project! {
159	#[project = BodyProject]
160	#[derive(Default)]
161	pub enum Body {
162		#[default]
163		Empty,
164		Once {
165			#[pin]
166			bytes: Full<Bytes>
167		},
168		Incoming {
169			#[pin]
170			incoming: Incoming
171		},
172	}
173}
174
175impl hyper::body::Body for Body {
176	type Data = Bytes;
177	type Error = hyper::Error;
178
179	fn poll_frame(
180		self: Pin<&mut Self>, cx: &mut task::Context<'_>,
181	) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
182		match self.project() {
183			BodyProject::Empty => Poll::Ready(None),
184			BodyProject::Once { bytes } => bytes.poll_frame(cx).map_err(|e| match e {}),
185			BodyProject::Incoming { incoming } => incoming.poll_frame(cx),
186		}
187	}
188
189	fn is_end_stream(&self) -> bool {
190		match self {
191			Body::Empty => true,
192			Body::Once { bytes } => bytes.is_end_stream(),
193			Body::Incoming { incoming } => incoming.is_end_stream(),
194		}
195	}
196
197	fn size_hint(&self) -> SizeHint {
198		match self {
199			Body::Empty => SizeHint::with_exact(0),
200			Body::Once { bytes } => bytes.size_hint(),
201			Body::Incoming { incoming } => incoming.size_hint(),
202		}
203	}
204}
205
206impl From<Bytes> for Body {
207	fn from(bytes: Bytes) -> Body {
208		Body::Once { bytes: Full::new(bytes) }
209	}
210}