1use crate::host::{with_host, JsObj};
19use fusevm::Value;
20use indexmap::IndexMap;
21
22pub const CLASSES: &[&str] = &[
24 "Headers",
25 "Request",
26 "Response",
27 "Blob",
28 "File",
29 "FormData",
30 "AbortController",
31 "AbortSignal",
32];
33
34pub const HEADERS_METHODS: &[&str] = &[
35 "get",
36 "getSetCookie",
37 "has",
38 "set",
39 "append",
40 "delete",
41 "forEach",
42 "keys",
43 "values",
44 "entries",
45 "@@iterator",
46];
47pub const RESPONSE_METHODS: &[&str] = &[
48 "text",
49 "json",
50 "arrayBuffer",
51 "bytes",
52 "blob",
53 "formData",
54 "clone",
55];
56pub const REQUEST_METHODS: &[&str] = &[
57 "text",
58 "json",
59 "arrayBuffer",
60 "bytes",
61 "blob",
62 "formData",
63 "clone",
64];
65pub const BLOB_METHODS: &[&str] = &["text", "arrayBuffer", "bytes", "slice", "stream"];
66pub const FORM_DATA_METHODS: &[&str] = &[
67 "append",
68 "delete",
69 "get",
70 "getAll",
71 "has",
72 "set",
73 "forEach",
74 "keys",
75 "values",
76 "entries",
77 "@@iterator",
78];
79pub const ABORT_CONTROLLER_METHODS: &[&str] = &["abort"];
80pub const ABORT_SIGNAL_METHODS: &[&str] =
81 &["throwIfAborted", "addEventListener", "removeEventListener"];
82
83pub fn methods_for(tag: &str) -> &'static [&'static str] {
84 match tag {
85 "Headers" => HEADERS_METHODS,
86 "Response" => RESPONSE_METHODS,
87 "Request" => REQUEST_METHODS,
88 "Blob" | "File" => BLOB_METHODS,
89 "FormData" => FORM_DATA_METHODS,
90 "AbortController" => ABORT_CONTROLLER_METHODS,
91 "AbortSignal" => ABORT_SIGNAL_METHODS,
92 _ => &[],
93 }
94}
95
96pub fn is_class(name: &str) -> bool {
97 CLASSES.contains(&name)
98}
99
100fn str_of(v: &Value) -> String {
103 with_host(|h| h.str_of(v))
104}
105
106fn new_str(s: impl Into<String>) -> Value {
107 let s = s.into();
108 with_host(|h| h.new_str(s))
109}
110
111fn prop(recv: &Value, key: &str) -> Option<Value> {
112 with_host(|h| match h.get(recv) {
113 Some(JsObj::Object(p)) => p.get(key).cloned(),
114 _ => None,
115 })
116 .filter(|v| !matches!(v, Value::Undef))
117}
118
119fn set_prop(recv: &Value, key: &str, val: Value) {
120 with_host(|h| {
121 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
122 p.insert(key.to_string(), val);
123 }
124 });
125}
126
127fn settled(result: Result<Value, String>) -> Value {
131 let p = with_host(|h| h.new_promise());
132 let id = with_host(|h| h.promise_id(&p).unwrap_or(0));
133 match result {
134 Ok(v) => crate::host::resolve_promise_val(id, v),
135 Err(e) => {
136 let ev = with_host(|h| crate::builtins::synth_error(h, &e));
137 crate::host::reject_promise_val(id, ev);
138 }
139 }
140 p
141}
142
143fn body_bytes(recv: &Value) -> Vec<u8> {
145 let Some(arr) = prop(recv, "@@body") else {
146 return Vec::new();
147 };
148 with_host(|h| match h.get(&arr) {
149 Some(JsObj::Array(items)) => items.iter().map(|v| h.to_number(v) as u8).collect(),
150 _ => Vec::new(),
151 })
152}
153
154fn store_body(bytes: &[u8]) -> Value {
155 with_host(|h| h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect()))
156}
157
158fn to_uint8array(bytes: &[u8]) -> Result<Value, String> {
161 let arr = with_host(|h| h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect()));
162 super::typedarray::construct("Uint8Array", &[arr])
163}
164
165fn headers_entries(recv: &Value) -> Vec<(String, String)> {
172 let Some(arr) = prop(recv, "@@entries") else {
173 return Vec::new();
174 };
175 with_host(|h| match h.get(&arr) {
176 Some(JsObj::Array(items)) => items
177 .iter()
178 .filter_map(|pair| match h.get(pair) {
179 Some(JsObj::Array(kv)) if kv.len() == 2 => {
180 Some((h.str_of(&kv[0]), h.str_of(&kv[1])))
181 }
182 _ => None,
183 })
184 .collect(),
185 _ => Vec::new(),
186 })
187}
188
189fn set_headers_entries(recv: &Value, entries: &[(String, String)]) {
190 let arr = with_host(|h| {
191 let pairs: Vec<Value> = entries
192 .iter()
193 .map(|(k, v)| {
194 let k = h.new_str(k.clone());
195 let v = h.new_str(v.clone());
196 h.new_array(vec![k, v])
197 })
198 .collect();
199 h.new_array(pairs)
200 });
201 set_prop(recv, "@@entries", arr);
202}
203
204pub fn construct_headers(args: &[Value]) -> Result<Value, String> {
207 let obj = with_host(|h| {
208 let mut m = IndexMap::new();
209 m.insert("@@native".into(), h.new_str("Headers"));
210 let empty = h.new_array(Vec::new());
211 m.insert("@@entries".into(), empty);
212 h.new_object(m)
213 });
214 with_host(|h| h.hide_prop(&obj, "@@entries"));
215 if let Some(init) = args.first().filter(|v| !matches!(v, Value::Undef)) {
216 for (k, v) in init_header_entries(init) {
217 append_header(&obj, &k, &v);
218 }
219 }
220 Ok(obj)
221}
222
223fn init_header_entries(init: &Value) -> Vec<(String, String)> {
225 if super::native_tag(init).as_deref() == Some("Headers") {
226 return headers_entries(init);
227 }
228 if with_host(|h| matches!(h.get(init), Some(JsObj::Array(_)))) {
230 return with_host(|h| match h.get(init) {
231 Some(JsObj::Array(items)) => items
232 .iter()
233 .filter_map(|pair| match h.get(pair) {
234 Some(JsObj::Array(kv)) if kv.len() >= 2 => {
235 Some((h.str_of(&kv[0]).to_ascii_lowercase(), h.str_of(&kv[1])))
236 }
237 _ => None,
238 })
239 .collect(),
240 _ => Vec::new(),
241 });
242 }
243 with_host(|h| match h.get(init) {
245 Some(JsObj::Object(p)) => p
246 .iter()
247 .filter(|(k, _)| !k.starts_with("@@"))
248 .map(|(k, v)| (k.to_ascii_lowercase(), h.str_of(v)))
249 .collect(),
250 _ => Vec::new(),
251 })
252}
253
254fn append_header(recv: &Value, name: &str, value: &str) {
255 let mut e = headers_entries(recv);
256 e.push((name.to_ascii_lowercase(), value.to_string()));
257 set_headers_entries(recv, &e);
258}
259
260fn set_header(recv: &Value, name: &str, value: &str) {
261 let name = name.to_ascii_lowercase();
262 let mut e = headers_entries(recv);
263 match e.iter().position(|(k, _)| *k == name) {
264 Some(i) => {
265 e[i].1 = value.to_string();
266 e.retain({
267 let mut seen = 0;
268 move |(k, _)| {
269 if *k != name {
270 return true;
271 }
272 seen += 1;
273 seen == 1
274 }
275 });
276 }
277 None => e.push((name, value.to_string())),
278 }
279 set_headers_entries(recv, &e);
280}
281
282fn get_header(recv: &Value, name: &str) -> Option<String> {
285 let name = name.to_ascii_lowercase();
286 let vals: Vec<String> = headers_entries(recv)
287 .into_iter()
288 .filter(|(k, _)| *k == name)
289 .map(|(_, v)| v)
290 .collect();
291 (!vals.is_empty()).then(|| vals.join(", "))
292}
293
294pub fn headers_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
295 match method {
296 "get" => Ok(match get_header(recv, &super::arg_str(args, 0)) {
297 Some(v) => new_str(v),
298 None => with_host(|h| h.null()),
299 }),
300 "getSetCookie" => {
301 let vals: Vec<Value> = headers_entries(recv)
302 .into_iter()
303 .filter(|(k, _)| k == "set-cookie")
304 .map(|(_, v)| new_str(v))
305 .collect();
306 Ok(with_host(|h| h.new_array(vals)))
307 }
308 "has" => {
309 let name = super::arg_str(args, 0).to_ascii_lowercase();
310 Ok(Value::Bool(
311 headers_entries(recv).iter().any(|(k, _)| *k == name),
312 ))
313 }
314 "set" => {
315 set_header(recv, &super::arg_str(args, 0), &super::arg_str(args, 1));
316 Ok(Value::Undef)
317 }
318 "append" => {
319 append_header(recv, &super::arg_str(args, 0), &super::arg_str(args, 1));
320 Ok(Value::Undef)
321 }
322 "delete" => {
323 let name = super::arg_str(args, 0).to_ascii_lowercase();
324 let mut e = headers_entries(recv);
325 e.retain(|(k, _)| *k != name);
326 set_headers_entries(recv, &e);
327 Ok(Value::Undef)
328 }
329 "forEach" => {
332 let cb = args.first().cloned().unwrap_or(Value::Undef);
333 for (k, v) in sorted_combined(recv) {
334 let (kv, vv) = (new_str(k), new_str(v));
335 crate::host::invoke(&cb, vec![vv, kv, recv.clone()], None)?;
336 }
337 Ok(Value::Undef)
338 }
339 "keys" | "values" | "entries" | "@@iterator" => {
340 let items: Vec<Value> = sorted_combined(recv)
341 .into_iter()
342 .map(|(k, v)| match method {
343 "keys" => new_str(k),
344 "values" => new_str(v),
345 _ => {
347 let (kv, vv) = (new_str(k), new_str(v));
348 with_host(|h| h.new_array(vec![kv, vv]))
349 }
350 })
351 .collect();
352 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
353 }
354 _ => Err(crate::host::type_error(&format!(
355 "{method} is not a function"
356 ))),
357 }
358}
359
360fn sorted_combined(recv: &Value) -> Vec<(String, String)> {
363 let mut names: Vec<String> = Vec::new();
364 for (k, _) in headers_entries(recv) {
365 if !names.contains(&k) {
366 names.push(k);
367 }
368 }
369 names.sort();
370 names
371 .into_iter()
372 .filter_map(|n| get_header(recv, &n).map(|v| (n, v)))
373 .collect()
374}
375
376fn build_response(
379 status: u16,
380 status_text: &str,
381 headers: &[(String, String)],
382 body: &[u8],
383 url: &str,
384) -> Value {
385 let headers_obj = construct_headers(&[]).unwrap_or(Value::Undef);
386 for (k, v) in headers {
387 append_header(&headers_obj, k, v);
388 }
389 let body_arr = store_body(body);
390 let obj = with_host(|h| {
391 let mut m = IndexMap::new();
392 m.insert("@@native".into(), h.new_str("Response"));
393 m.insert("status".into(), Value::Float(status as f64));
394 m.insert("statusText".into(), h.new_str(status_text.to_string()));
395 m.insert(
397 "ok".into(),
398 Value::Bool((200..300).contains(&(status as u32))),
399 );
400 m.insert("redirected".into(), Value::Bool(false));
401 m.insert("type".into(), h.new_str("basic"));
402 m.insert("url".into(), h.new_str(url.to_string()));
403 m.insert("bodyUsed".into(), Value::Bool(false));
404 m.insert("headers".into(), headers_obj);
405 m.insert("@@body".into(), body_arr);
406 h.new_object(m)
407 });
408 with_host(|h| h.hide_prop(&obj, "@@body"));
409 obj
410}
411
412pub fn construct_response(args: &[Value]) -> Result<Value, String> {
414 let body = body_init_bytes(args.first());
415 let init = args.get(1).cloned().unwrap_or(Value::Undef);
416 let status = match prop(&init, "status") {
417 Some(v) => with_host(|h| h.to_number(&v)) as u16,
418 None => 200,
419 };
420 let status_text = prop(&init, "statusText")
421 .map(|v| str_of(&v))
422 .unwrap_or_default();
423 let mut headers: Vec<(String, String)> = Vec::new();
424 if let Some(hv) = prop(&init, "headers") {
425 headers = init_header_entries(&hv);
426 }
427 Ok(build_response(status, &status_text, &headers, &body, ""))
428}
429
430pub fn construct_request(args: &[Value]) -> Result<Value, String> {
432 let input = args.first().cloned().unwrap_or(Value::Undef);
433 let url = if super::native_tag(&input).as_deref() == Some("Request") {
434 prop(&input, "url").map(|v| str_of(&v)).unwrap_or_default()
435 } else {
436 str_of(&input)
437 };
438 let init = args.get(1).cloned().unwrap_or(Value::Undef);
439 let method = prop(&init, "method")
440 .map(|v| str_of(&v).to_ascii_uppercase())
441 .unwrap_or_else(|| "GET".into());
442 let headers_obj = construct_headers(&[])?;
443 if let Some(hv) = prop(&init, "headers") {
444 for (k, v) in init_header_entries(&hv) {
445 append_header(&headers_obj, &k, &v);
446 }
447 }
448 let body = body_init_bytes(prop(&init, "body").as_ref());
449 let body_arr = store_body(&body);
450 let obj = with_host(|h| {
451 let mut m = IndexMap::new();
452 m.insert("@@native".into(), h.new_str("Request"));
453 m.insert("url".into(), h.new_str(url));
454 m.insert("method".into(), h.new_str(method));
455 m.insert("headers".into(), headers_obj);
456 m.insert("bodyUsed".into(), Value::Bool(false));
457 m.insert("@@body".into(), body_arr);
458 h.new_object(m)
459 });
460 with_host(|h| h.hide_prop(&obj, "@@body"));
461 Ok(obj)
462}
463
464fn body_init_bytes(v: Option<&Value>) -> Vec<u8> {
467 let Some(v) = v.filter(|v| !matches!(v, Value::Undef)) else {
468 return Vec::new();
469 };
470 if with_host(|h| h.is_null(v)) {
471 return Vec::new();
472 }
473 match super::native_tag(v).as_deref() {
474 Some("Blob") | Some("File") | Some("Response") | Some("Request") => return body_bytes(v),
475 Some("TypedArray") | Some("Buffer") => {
476 if let Some(e) = super::typedarray::elems_of(v) {
477 return e.iter().map(|x| *x as u8).collect();
478 }
479 }
480 _ => {}
481 }
482 if super::native_tag(v).as_deref() == Some("URLSearchParams") {
483 return str_of(v).into_bytes();
484 }
485 str_of(v).into_bytes()
486}
487
488pub fn body_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
491 match method {
492 "text" => {
493 set_prop(recv, "bodyUsed", Value::Bool(true));
494 let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
495 Ok(settled(Ok(new_str(s))))
496 }
497 "json" => {
498 set_prop(recv, "bodyUsed", Value::Bool(true));
499 let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
500 let parsed = crate::builtins::call_builtin_function("JSON.parse", vec![new_str(s)]);
501 Ok(settled(parsed))
502 }
503 "bytes" => {
504 set_prop(recv, "bodyUsed", Value::Bool(true));
505 Ok(settled(to_uint8array(&body_bytes(recv))))
506 }
507 "arrayBuffer" => {
508 set_prop(recv, "bodyUsed", Value::Bool(true));
509 let bytes = body_bytes(recv);
510 let buf = with_host(|h| {
511 let mut m = IndexMap::new();
512 m.insert("@@native".into(), h.new_str("ArrayBuffer"));
513 m.insert("byteLength".into(), Value::Float(bytes.len() as f64));
514 let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
515 m.insert("@@bytes".into(), arr);
516 h.new_object(m)
517 });
518 with_host(|h| h.hide_prop(&buf, "@@bytes"));
519 Ok(settled(Ok(buf)))
520 }
521 "blob" => {
522 set_prop(recv, "bodyUsed", Value::Bool(true));
523 let bytes = body_bytes(recv);
524 let ct = prop(recv, "headers")
525 .and_then(|h| get_header(&h, "content-type"))
526 .unwrap_or_default();
527 Ok(settled(Ok(new_blob(&bytes, &ct, None))))
528 }
529 "formData" => {
530 set_prop(recv, "bodyUsed", Value::Bool(true));
531 let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
532 let fd = construct_form_data(&[])?;
533 for pair in s.split('&').filter(|p| !p.is_empty()) {
536 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
537 form_append(&fd, &percent_decode(k), &new_str(percent_decode(v)));
538 }
539 Ok(settled(Ok(fd)))
540 }
541 "clone" => {
542 let bytes = body_bytes(recv);
543 let clone = crate::builtins::deep_clone(recv)?;
544 set_prop(&clone, "@@body", store_body(&bytes));
545 set_prop(&clone, "bodyUsed", Value::Bool(false));
546 Ok(clone)
547 }
548 "slice" => {
549 let bytes = body_bytes(recv);
550 let start = args
551 .first()
552 .map(|v| with_host(|h| h.to_number(v)) as usize)
553 .unwrap_or(0)
554 .min(bytes.len());
555 let end = args
556 .get(1)
557 .filter(|v| !matches!(v, Value::Undef))
558 .map(|v| with_host(|h| h.to_number(v)) as usize)
559 .unwrap_or(bytes.len())
560 .clamp(start, bytes.len());
561 let ct = args.get(2).map(str_of).unwrap_or_default();
562 Ok(new_blob(&bytes[start..end], &ct, None))
563 }
564 "stream" => Err(crate::host::type_error(
565 "Response.body streaming is not implemented on this build",
566 )),
567 _ => Err(crate::host::type_error(&format!(
568 "{method} is not a function"
569 ))),
570 }
571}
572
573fn throw_js(v: Value) -> String {
576 let msg = with_host(|h| h.str_of(&v));
577 with_host(|h| h.exc = Some(v));
578 msg
579}
580
581fn percent_decode(s: &str) -> String {
582 let bytes = s.replace('+', " ").into_bytes();
583 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
584 let mut i = 0;
585 while i < bytes.len() {
586 if bytes[i] == b'%' && i + 2 < bytes.len() {
587 if let Ok(b) = u8::from_str_radix(&String::from_utf8_lossy(&bytes[i + 1..i + 3]), 16) {
588 out.push(b);
589 i += 3;
590 continue;
591 }
592 }
593 out.push(bytes[i]);
594 i += 1;
595 }
596 String::from_utf8_lossy(&out).into_owned()
597}
598
599fn new_blob(bytes: &[u8], content_type: &str, file_name: Option<&str>) -> Value {
602 let body = store_body(bytes);
603 let obj = with_host(|h| {
604 let mut m = IndexMap::new();
605 m.insert(
606 "@@native".into(),
607 h.new_str(if file_name.is_some() { "File" } else { "Blob" }),
608 );
609 m.insert("size".into(), Value::Float(bytes.len() as f64));
610 m.insert("type".into(), h.new_str(content_type.to_ascii_lowercase()));
611 if let Some(n) = file_name {
612 m.insert("name".into(), h.new_str(n.to_string()));
613 m.insert("lastModified".into(), Value::Float(0.0));
614 }
615 m.insert("@@body".into(), body);
616 h.new_object(m)
617 });
618 with_host(|h| h.hide_prop(&obj, "@@body"));
619 obj
620}
621
622pub fn construct_blob(args: &[Value]) -> Result<Value, String> {
623 let mut bytes = Vec::new();
624 if let Some(parts) = args.first() {
625 for p in crate::host::iter_all(parts).unwrap_or_default() {
626 bytes.extend(body_init_bytes(Some(&p)));
627 }
628 }
629 let ct = args
630 .get(1)
631 .and_then(|o| prop(o, "type"))
632 .map(|v| str_of(&v))
633 .unwrap_or_default();
634 Ok(new_blob(&bytes, &ct, None))
635}
636
637pub fn construct_file(args: &[Value]) -> Result<Value, String> {
639 let mut bytes = Vec::new();
640 if let Some(parts) = args.first() {
641 for p in crate::host::iter_all(parts).unwrap_or_default() {
642 bytes.extend(body_init_bytes(Some(&p)));
643 }
644 }
645 let name = super::arg_str(args, 1);
646 let ct = args
647 .get(2)
648 .and_then(|o| prop(o, "type"))
649 .map(|v| str_of(&v))
650 .unwrap_or_default();
651 Ok(new_blob(&bytes, &ct, Some(&name)))
652}
653
654pub fn construct_form_data(_args: &[Value]) -> Result<Value, String> {
657 let obj = with_host(|h| {
658 let mut m = IndexMap::new();
659 m.insert("@@native".into(), h.new_str("FormData"));
660 let empty = h.new_array(Vec::new());
661 m.insert("@@entries".into(), empty);
662 h.new_object(m)
663 });
664 with_host(|h| h.hide_prop(&obj, "@@entries"));
665 Ok(obj)
666}
667
668fn form_entries(recv: &Value) -> Vec<(String, Value)> {
669 let Some(arr) = prop(recv, "@@entries") else {
670 return Vec::new();
671 };
672 with_host(|h| match h.get(&arr) {
673 Some(JsObj::Array(items)) => items
674 .iter()
675 .filter_map(|pair| match h.get(pair) {
676 Some(JsObj::Array(kv)) if kv.len() == 2 => Some((h.str_of(&kv[0]), kv[1].clone())),
677 _ => None,
678 })
679 .collect(),
680 _ => Vec::new(),
681 })
682}
683
684fn set_form_entries(recv: &Value, entries: &[(String, Value)]) {
685 let arr = with_host(|h| {
686 let pairs: Vec<Value> = entries
687 .iter()
688 .map(|(k, v)| {
689 let k = h.new_str(k.clone());
690 h.new_array(vec![k, v.clone()])
691 })
692 .collect();
693 h.new_array(pairs)
694 });
695 set_prop(recv, "@@entries", arr);
696}
697
698fn form_append(recv: &Value, name: &str, value: &Value) {
699 let mut e = form_entries(recv);
700 e.push((name.to_string(), value.clone()));
701 set_form_entries(recv, &e);
702}
703
704pub fn form_data_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
705 let name = super::arg_str(args, 0);
706 match method {
707 "append" => {
708 form_append(recv, &name, args.get(1).unwrap_or(&Value::Undef));
709 Ok(Value::Undef)
710 }
711 "set" => {
712 let mut e = form_entries(recv);
713 e.retain(|(k, _)| *k != name);
714 e.push((name, args.get(1).cloned().unwrap_or(Value::Undef)));
715 set_form_entries(recv, &e);
716 Ok(Value::Undef)
717 }
718 "delete" => {
719 let mut e = form_entries(recv);
720 e.retain(|(k, _)| *k != name);
721 set_form_entries(recv, &e);
722 Ok(Value::Undef)
723 }
724 "get" => Ok(form_entries(recv)
725 .into_iter()
726 .find(|(k, _)| *k == name)
727 .map(|(_, v)| v)
728 .unwrap_or_else(|| with_host(|h| h.null()))),
729 "getAll" => {
730 let vals: Vec<Value> = form_entries(recv)
731 .into_iter()
732 .filter(|(k, _)| *k == name)
733 .map(|(_, v)| v)
734 .collect();
735 Ok(with_host(|h| h.new_array(vals)))
736 }
737 "has" => Ok(Value::Bool(
738 form_entries(recv).iter().any(|(k, _)| *k == name),
739 )),
740 "forEach" => {
741 let cb = args.first().cloned().unwrap_or(Value::Undef);
742 for (k, v) in form_entries(recv) {
743 let kv = new_str(k);
744 crate::host::invoke(&cb, vec![v, kv, recv.clone()], None)?;
745 }
746 Ok(Value::Undef)
747 }
748 "keys" | "values" | "entries" | "@@iterator" => {
749 let items: Vec<Value> = form_entries(recv)
750 .into_iter()
751 .map(|(k, v)| match method {
752 "keys" => new_str(k),
753 "values" => v,
754 _ => {
755 let kv = new_str(k);
756 with_host(|h| h.new_array(vec![kv, v]))
757 }
758 })
759 .collect();
760 Ok(with_host(|h| h.alloc(JsObj::Iter { items, idx: 0 })))
761 }
762 _ => Err(crate::host::type_error(&format!(
763 "{method} is not a function"
764 ))),
765 }
766}
767
768pub fn new_abort_signal() -> Value {
771 with_host(|h| {
772 let mut m = IndexMap::new();
773 m.insert("@@native".into(), h.new_str("AbortSignal"));
774 m.insert("@@aborted".into(), Value::Bool(false));
775 m.insert("@@reason".into(), Value::Undef);
776 m.insert("@@onabort".into(), h.null());
777 h.new_object(m)
778 })
779}
780
781pub fn construct_abort_controller(_args: &[Value]) -> Result<Value, String> {
782 let signal = new_abort_signal();
783 Ok(with_host(|h| {
784 let mut m = IndexMap::new();
785 m.insert("@@native".into(), h.new_str("AbortController"));
786 m.insert("@@signal".into(), signal);
787 h.new_object(m)
788 }))
789}
790
791fn dom_exception(name: &str, message: &str) -> Value {
796 let msg = with_host(|h| h.new_str(message.to_string()));
797 let nm = with_host(|h| h.new_str(name.to_string()));
798 crate::builtins::dom_exception(&[msg, nm])
799}
800
801pub fn fire_timeout_abort(idx: u32) -> Result<Value, String> {
804 let signal = Value::Obj(idx);
805 let e = dom_exception("TimeoutError", "The operation was aborted due to timeout");
806 abort_signal(&signal, e)?;
807 Ok(Value::Undef)
808}
809
810fn abort_signal(signal: &Value, reason: Value) -> Result<(), String> {
812 if matches!(prop(signal, "@@aborted"), Some(Value::Bool(true))) {
813 return Ok(());
814 }
815 let reason = if matches!(reason, Value::Undef) {
816 dom_exception("AbortError", "This operation was aborted")
817 } else {
818 reason
819 };
820 set_prop(signal, "@@aborted", Value::Bool(true));
821 set_prop(signal, "@@reason", reason);
822 if let Some(cb) = prop(signal, "@@onabort") {
823 if with_host(|h| crate::host::is_callable(h, &cb)) {
824 crate::host::invoke(&cb, vec![Value::Undef], Some(signal.clone()))?;
825 }
826 }
827 if let Some(list) = prop(signal, "@@abortListeners") {
828 for cb in crate::host::iter_all(&list).unwrap_or_default() {
829 crate::host::invoke(&cb, vec![Value::Undef], Some(signal.clone()))?;
830 }
831 }
832 Ok(())
833}
834
835pub fn abort_controller_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
836 match method {
837 "abort" => {
838 let signal = prop(recv, "@@signal").unwrap_or(Value::Undef);
839 abort_signal(&signal, args.first().cloned().unwrap_or(Value::Undef))?;
840 Ok(Value::Undef)
841 }
842 _ => Err(crate::host::type_error(&format!(
843 "{method} is not a function"
844 ))),
845 }
846}
847
848pub fn abort_signal_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
849 match method {
850 "throwIfAborted" => {
851 if matches!(prop(recv, "@@aborted"), Some(Value::Bool(true))) {
852 let reason = prop(recv, "@@reason").unwrap_or(Value::Undef);
853 return Err(throw_js(reason));
854 }
855 Ok(Value::Undef)
856 }
857 "addEventListener" => {
858 if super::arg_str(args, 0) == "abort" {
859 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
860 let existing = prop(recv, "@@abortListeners");
861 let list = match existing {
862 Some(l) => l,
863 None => {
864 let l = with_host(|h| h.new_array(Vec::new()));
865 set_prop(recv, "@@abortListeners", l.clone());
866 with_host(|h| h.hide_prop(recv, "@@abortListeners"));
867 l
868 }
869 };
870 with_host(|h| {
871 if let Some(JsObj::Array(items)) = h.get_mut(&list) {
872 items.push(cb);
873 }
874 });
875 }
876 Ok(Value::Undef)
877 }
878 "removeEventListener" => Ok(Value::Undef),
879 _ => Err(crate::host::type_error(&format!(
880 "{method} is not a function"
881 ))),
882 }
883}
884
885pub fn abort_signal_static(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
887 match method {
888 "abort" => {
889 let s = new_abort_signal();
890 let r = abort_signal(&s, args.first().cloned().unwrap_or(Value::Undef));
891 Some(r.map(|_| s))
892 }
893 "timeout" => {
894 let s = new_abort_signal();
895 let ms = super::arg_num(args, 0);
899 let Value::Obj(idx) = s else {
900 return Some(Ok(s));
901 };
902 let cb = with_host(|h| h.alloc(JsObj::Builtin(format!("@@aborttimeout:{idx}"))));
903 with_host(|h| h.add_timer(ms, cb, Vec::new(), None));
904 Some(Ok(s))
905 }
906 _ => None,
907 }
908}
909
910pub fn fetch(args: &[Value]) -> Result<Value, String> {
914 let promise = with_host(|h| h.new_promise());
915 let id = with_host(|h| h.promise_id(&promise).unwrap_or(0));
916
917 let input = args.first().cloned().unwrap_or(Value::Undef);
918 let init = args.get(1).cloned().unwrap_or(Value::Undef);
919
920 let from_request = super::native_tag(&input).as_deref() == Some("Request");
923 let url = if from_request {
924 prop(&input, "url").map(|v| str_of(&v)).unwrap_or_default()
925 } else {
926 str_of(&input)
927 };
928
929 let mut method = if from_request {
930 prop(&input, "method")
931 .map(|v| str_of(&v))
932 .unwrap_or_else(|| "GET".into())
933 } else {
934 "GET".into()
935 };
936 if let Some(m) = prop(&init, "method") {
937 method = str_of(&m);
938 }
939 method = method.to_ascii_uppercase();
940
941 let mut headers: Vec<(String, String)> = Vec::new();
942 if from_request {
943 if let Some(h) = prop(&input, "headers") {
944 headers = headers_entries(&h);
945 }
946 }
947 if let Some(hv) = prop(&init, "headers") {
948 headers.extend(init_header_entries(&hv));
949 }
950
951 let body = match prop(&init, "body") {
952 Some(b) => body_init_bytes(Some(&b)),
953 None if from_request => body_bytes(&input),
954 None => Vec::new(),
955 };
956
957 if let Some(sig) = prop(&init, "signal") {
959 if matches!(prop(&sig, "@@aborted"), Some(Value::Bool(true))) {
960 let reason = prop(&sig, "@@reason")
961 .unwrap_or_else(|| dom_exception("AbortError", "This operation was aborted"));
962 crate::host::reject_promise_val(id, reason);
963 return Ok(promise);
964 }
965 }
966
967 let Some(target) = parse_target(&url) else {
968 crate::host::reject_promise_val(id, fetch_failed("unknown scheme"));
969 return Ok(promise);
970 };
971
972 let wire = build_request_bytes(&target, &method, &headers, &body);
973 let io_tx = with_host(|h| h.io_sender());
974 with_host(|h| h.incr_handle());
975 let url_for_response = url.clone();
976 std::thread::spawn(move || {
977 let raw = if target.tls {
978 let config = crate::stdlib::tls::client_config(true);
979 crate::stdlib::https::exchange(&target.host, target.port, &target.host, config, &wire)
980 } else {
981 crate::stdlib::http::exchange(&target.host, target.port, &wire)
982 };
983 let _ = io_tx.send(Box::new(move || {
984 with_host(|h| h.decr_handle());
985 match raw {
986 Ok(raw) => {
987 let parsed = crate::stdlib::http::parse_raw_response(&raw);
988 let (status, message, headers, body) =
989 (parsed.status, parsed.message, parsed.headers, parsed.body);
990 let resp = build_response(status, &message, &headers, &body, &url_for_response);
991 crate::host::resolve_promise_val(id, resp);
992 }
993 Err(msg) => crate::host::reject_promise_val(id, fetch_failed(&msg)),
994 }
995 Ok(())
996 }));
997 });
998 Ok(promise)
999}
1000
1001fn fetch_failed(cause: &str) -> Value {
1005 with_host(|h| {
1006 let e = crate::builtins::synth_error(h, "TypeError: fetch failed");
1007 let c = crate::builtins::synth_error(h, &format!("Error: {cause}"));
1008 if let Some(JsObj::Object(p)) = h.get_mut(&e) {
1009 p.insert("cause".into(), c);
1010 }
1011 e
1012 })
1013}
1014
1015struct Target {
1016 host: String,
1017 port: u16,
1018 path: String,
1019 tls: bool,
1020}
1021
1022fn parse_target(url: &str) -> Option<Target> {
1023 let (tls, rest) = match url.strip_prefix("https://") {
1026 Some(r) => (true, r),
1027 None => (false, url.strip_prefix("http://")?),
1028 };
1029 let (authority, path) = match rest.find('/') {
1030 Some(i) => (&rest[..i], rest[i..].to_string()),
1031 None => (rest, "/".to_string()),
1032 };
1033 let authority = authority.split('@').next_back().unwrap_or(authority);
1034 let (host, port) = match authority.rsplit_once(':') {
1035 Some((h, p)) => match p.parse::<u16>() {
1036 Ok(n) => (h.to_string(), n),
1037 Err(_) => (authority.to_string(), if tls { 443 } else { 80 }),
1038 },
1039 None => (authority.to_string(), if tls { 443 } else { 80 }),
1040 };
1041 if host.is_empty() {
1042 return None;
1043 }
1044 Some(Target {
1045 host,
1046 port,
1047 path,
1048 tls,
1049 })
1050}
1051
1052fn build_request_bytes(
1053 target: &Target,
1054 method: &str,
1055 headers: &[(String, String)],
1056 body: &[u8],
1057) -> Vec<u8> {
1058 let mut has_host = false;
1059 let mut has_len = false;
1060 let mut block = String::new();
1061 for (k, v) in headers {
1062 if k.eq_ignore_ascii_case("host") {
1063 has_host = true;
1064 }
1065 if k.eq_ignore_ascii_case("content-length") {
1066 has_len = true;
1067 }
1068 if k.eq_ignore_ascii_case("connection") {
1069 continue;
1070 }
1071 block.push_str(&format!("{k}: {v}\r\n"));
1072 }
1073 let default_port = if target.tls { 443 } else { 80 };
1074 let host_header = if target.port == default_port {
1075 target.host.clone()
1076 } else {
1077 format!("{}:{}", target.host, target.port)
1078 };
1079 let mut req = format!("{method} {} HTTP/1.1\r\n", target.path);
1080 if !has_host {
1081 req.push_str(&format!("Host: {host_header}\r\n"));
1082 }
1083 req.push_str(&block);
1084 if !has_len && !body.is_empty() {
1085 req.push_str(&format!("Content-Length: {}\r\n", body.len()));
1086 }
1087 req.push_str("Connection: close\r\n\r\n");
1089 let mut wire = req.into_bytes();
1090 wire.extend_from_slice(body);
1091 wire
1092}
1093
1094pub fn instance_call(
1097 tag: &str,
1098 recv: &Value,
1099 method: &str,
1100 args: &[Value],
1101) -> Result<Value, String> {
1102 match tag {
1103 "Headers" => headers_call(recv, method, args),
1104 "FormData" => form_data_call(recv, method, args),
1105 "AbortController" => abort_controller_call(recv, method, args),
1106 "AbortSignal" => abort_signal_call(recv, method, args),
1107 _ => body_call(recv, method, args),
1108 }
1109}
1110
1111pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
1112 Some(match name {
1113 "Headers" => construct_headers(args),
1114 "Request" => construct_request(args),
1115 "Response" => construct_response(args),
1116 "Blob" => construct_blob(args),
1117 "File" => construct_file(args),
1118 "FormData" => construct_form_data(args),
1119 "AbortController" => construct_abort_controller(args),
1120 "AbortSignal" => Err(crate::host::type_error(
1121 "Illegal constructor: use AbortSignal.abort() or AbortSignal.timeout()",
1122 )),
1123 _ => return None,
1124 })
1125}
1126
1127pub fn static_call(ns: &str, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
1129 match ns {
1130 "AbortSignal" => abort_signal_static(method, args),
1131 "Response" => match method {
1132 "json" => {
1133 let body = crate::builtins::call_builtin_function(
1134 "JSON.stringify",
1135 vec![args.first()?.clone()],
1136 )
1137 .map(|v| str_of(&v))
1138 .unwrap_or_default();
1139 let init = args.get(1).cloned().unwrap_or(Value::Undef);
1140 let status = match prop(&init, "status") {
1141 Some(v) => with_host(|h| h.to_number(&v)) as u16,
1142 None => 200,
1143 };
1144 let mut headers =
1145 vec![("content-type".to_string(), "application/json".to_string())];
1146 if let Some(hv) = prop(&init, "headers") {
1147 headers.extend(init_header_entries(&hv));
1148 }
1149 Some(Ok(build_response(
1150 status,
1151 "",
1152 &headers,
1153 body.as_bytes(),
1154 "",
1155 )))
1156 }
1157 "error" => Some(Ok(build_response(0, "", &[], &[], ""))),
1158 "redirect" => {
1159 let loc = super::arg_str(args, 0);
1160 let status = match args.get(1) {
1161 Some(v) => with_host(|h| h.to_number(v)) as u16,
1162 None => 302,
1163 };
1164 Some(Ok(build_response(
1165 status,
1166 "",
1167 &[("location".into(), loc)],
1168 &[],
1169 "",
1170 )))
1171 }
1172 _ => None,
1173 },
1174 _ => None,
1175 }
1176}
1177
1178pub const RESPONSE_STATICS: &[&str] = &["json", "error", "redirect"];
1179pub const ABORT_SIGNAL_STATICS: &[&str] = &["abort", "timeout"];