runtime/globals/fetch/
scheme.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::collections::Bound;
8use std::iter::once;
9use std::str;
10
11use arrayvec::ArrayVec;
12use bytes::Bytes;
13use data_url::DataUrl;
14use headers::{HeaderMapExt as _, Range};
15use http::header::{CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE};
16use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
17use ion::class::Reflector;
18use ion::{ClassDefinition as _, Context, Local, Object};
19use tokio::fs::read;
20use url::Url;
21
22use crate::ContextExt as _;
23use crate::globals::fetch::header::HeadersKind;
24use crate::globals::fetch::response::network_error;
25use crate::globals::fetch::{Headers, Request, Response};
26use crate::globals::file::Blob;
27use crate::globals::url::parse_uuid_from_url_path;
28
29pub async fn scheme_fetch(cx: &Context, scheme: &str, request: &Request, url: Url) -> Response {
30	match scheme {
31		"about" if url.path() == "blank" => about_blank_fetch(cx, url),
32		"blob" => blob_fetch(cx, request, url),
33		"data" => data_fetch(cx, url),
34		"file" => file_fetch(cx, request, url).await,
35		_ => network_error(),
36	}
37}
38
39fn about_blank_fetch(cx: &Context, url: Url) -> Response {
40	let response = Response::new_from_bytes(Bytes::default(), url);
41	let headers = Headers {
42		reflector: Reflector::default(),
43		headers: HeaderMap::from_iter(once((
44			CONTENT_TYPE,
45			HeaderValue::from_static("text/html;charset=UTF-8"),
46		))),
47		kind: HeadersKind::Immutable,
48	};
49	response.headers.set(Headers::new_object(cx, Box::new(headers)));
50	response
51}
52
53fn blob_fetch(cx: &Context, request: &Request, url: Url) -> Response {
54	if request.method != Method::GET {
55		return network_error();
56	}
57
58	let Some(uuid) = parse_uuid_from_url_path(&url) else {
59		return network_error();
60	};
61	let blob = unsafe {
62		match cx.get_private().blob_store.get(&uuid) {
63			Some(blob) => blob,
64			_ => return network_error(),
65		}
66	};
67
68	let blob = Object::from(unsafe { Local::from_heap(blob) });
69	let blob = Blob::get_private(cx, &blob).unwrap();
70
71	let kind = blob.kind.as_deref().unwrap_or("");
72	let Ok(kind) = HeaderValue::from_str(kind) else {
73		return network_error();
74	};
75
76	let mut response_headers = ArrayVec::<_, 3>::new();
77	response_headers.push((CONTENT_TYPE, kind));
78
79	let mut bytes = blob.bytes.clone();
80
81	let headers = Object::from(unsafe { Local::from_heap(&request.headers) });
82	let headers = &Headers::get_mut_private(cx, &headers).unwrap().headers;
83
84	let (status, range_requested) = match get_ranged_bytes(headers, &mut bytes, &mut response_headers) {
85		Ok((status, range_requested)) => (status, range_requested),
86		Err(err) => return err,
87	};
88
89	response_headers.push((CONTENT_LENGTH, HeaderValue::from(bytes.len())));
90
91	let mut response = Response::new_from_bytes(bytes, url);
92	response.status = Some(status);
93	response.range_requested = range_requested;
94
95	let headers = Headers {
96		reflector: Reflector::default(),
97		headers: HeaderMap::from_iter(response_headers),
98		kind: HeadersKind::Immutable,
99	};
100	response.headers.set(Headers::new_object(cx, Box::new(headers)));
101	response
102}
103
104fn data_fetch(cx: &Context, url: Url) -> Response {
105	let Ok(data_url) = DataUrl::process(url.as_str()) else {
106		return network_error();
107	};
108	let Ok((body, _)) = data_url.decode_to_vec() else {
109		return network_error();
110	};
111
112	let mime = data_url.mime_type();
113	let mime = format!("{}/{}", mime.type_, mime.subtype);
114
115	let response = Response::new_from_bytes(Bytes::from(body), url);
116	let headers = Headers {
117		reflector: Reflector::default(),
118		headers: HeaderMap::from_iter(once((CONTENT_TYPE, HeaderValue::from_str(&mime).unwrap()))),
119		kind: HeadersKind::Immutable,
120	};
121	response.headers.set(Headers::new_object(cx, Box::new(headers)));
122	response
123}
124
125async fn file_fetch(cx: &Context, request: &Request, url: Url) -> Response {
126	if request.method != Method::GET {
127		return network_error();
128	}
129
130	match url.to_file_path() {
131		Ok(path) => match read(path).await {
132			Ok(bytes) => {
133				let mut bytes = Bytes::from(bytes);
134
135				let headers = Object::from(unsafe { Local::from_heap(&request.headers) });
136				let headers = &Headers::get_mut_private(cx, &headers).unwrap().headers;
137
138				let mut response_headers = ArrayVec::<_, 2>::new();
139				let (status, range_requested) = match get_ranged_bytes(headers, &mut bytes, &mut response_headers) {
140					Ok((status, range_requested)) => (status, range_requested),
141					Err(err) => return err,
142				};
143
144				response_headers.push((CONTENT_LENGTH, HeaderValue::from(bytes.len())));
145
146				let mut response = Response::new_from_bytes(bytes, url);
147				response.status = Some(status);
148				response.range_requested = range_requested;
149
150				let headers = Headers {
151					reflector: Reflector::default(),
152					headers: HeaderMap::from_iter(response_headers),
153					kind: HeadersKind::Immutable,
154				};
155				response.headers.set(Headers::new_object(cx, Box::new(headers)));
156
157				response
158			}
159			Err(_) => network_error(),
160		},
161		Err(_) => network_error(),
162	}
163}
164
165#[expect(clippy::result_large_err)]
166fn get_ranged_bytes<const N: usize>(
167	headers: &HeaderMap, bytes: &mut Bytes, response_headers: &mut ArrayVec<(HeaderName, HeaderValue), N>,
168) -> Result<(StatusCode, bool), Response> {
169	match headers.typed_try_get::<Range>() {
170		Ok(Some(range)) => {
171			let len = bytes.len();
172			if let Some((start, end)) = range.satisfiable_ranges(len as u64).next() {
173				let (start, end) = (start.map(|s| s as usize), end.map(|e| e as usize));
174				*bytes = bytes.slice((start, end));
175
176				let (start, end) = match (start, end) {
177					(Bound::Included(s), Bound::Included(e)) => (s, e),
178					(Bound::Included(s), Bound::Unbounded) => (s, len - 1),
179					_ => unreachable!(),
180				};
181				let Ok(range) = HeaderValue::from_str(&format!("{start}-{end}/{len}")) else {
182					return Err(network_error());
183				};
184
185				response_headers.push((CONTENT_RANGE, range));
186
187				Ok((StatusCode::PARTIAL_CONTENT, true))
188			} else {
189				Err(network_error())
190			}
191		}
192		Ok(None) => Ok((StatusCode::OK, false)),
193		Err(_) => Err(network_error()),
194	}
195}