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| {
353 h.alloc(JsObj::Iter {
354 items,
355 idx: 0,
356 array: None,
357 })
358 }))
359 }
360 _ => Err(crate::host::type_error(&format!(
361 "{method} is not a function"
362 ))),
363 }
364}
365
366fn sorted_combined(recv: &Value) -> Vec<(String, String)> {
369 let mut names: Vec<String> = Vec::new();
370 for (k, _) in headers_entries(recv) {
371 if !names.contains(&k) {
372 names.push(k);
373 }
374 }
375 names.sort();
376 names
377 .into_iter()
378 .filter_map(|n| get_header(recv, &n).map(|v| (n, v)))
379 .collect()
380}
381
382fn build_response(
385 status: u16,
386 status_text: &str,
387 headers: &[(String, String)],
388 body: &[u8],
389 url: &str,
390) -> Value {
391 let headers_obj = construct_headers(&[]).unwrap_or(Value::Undef);
392 for (k, v) in headers {
393 append_header(&headers_obj, k, v);
394 }
395 let body_arr = store_body(body);
396 let obj = with_host(|h| {
397 let mut m = IndexMap::new();
398 m.insert("@@native".into(), h.new_str("Response"));
399 m.insert("status".into(), Value::Float(status as f64));
400 m.insert("statusText".into(), h.new_str(status_text.to_string()));
401 m.insert(
403 "ok".into(),
404 Value::Bool((200..300).contains(&(status as u32))),
405 );
406 m.insert("redirected".into(), Value::Bool(false));
407 m.insert("type".into(), h.new_str("basic"));
408 m.insert("url".into(), h.new_str(url.to_string()));
409 m.insert("bodyUsed".into(), Value::Bool(false));
410 m.insert("headers".into(), headers_obj);
411 m.insert("@@body".into(), body_arr);
412 h.new_object(m)
413 });
414 with_host(|h| h.hide_prop(&obj, "@@body"));
415 obj
416}
417
418pub fn construct_response(args: &[Value]) -> Result<Value, String> {
420 let body = body_init_bytes(args.first());
421 let init = args.get(1).cloned().unwrap_or(Value::Undef);
422 let status = match prop(&init, "status") {
423 Some(v) => with_host(|h| h.to_number(&v)) as u16,
424 None => 200,
425 };
426 let status_text = prop(&init, "statusText")
427 .map(|v| str_of(&v))
428 .unwrap_or_default();
429 let mut headers: Vec<(String, String)> = Vec::new();
430 if let Some(hv) = prop(&init, "headers") {
431 headers = init_header_entries(&hv);
432 }
433 Ok(build_response(status, &status_text, &headers, &body, ""))
434}
435
436pub fn construct_request(args: &[Value]) -> Result<Value, String> {
438 let input = args.first().cloned().unwrap_or(Value::Undef);
439 let url = if super::native_tag(&input).as_deref() == Some("Request") {
440 prop(&input, "url").map(|v| str_of(&v)).unwrap_or_default()
441 } else {
442 str_of(&input)
443 };
444 let init = args.get(1).cloned().unwrap_or(Value::Undef);
445 let method = prop(&init, "method")
446 .map(|v| str_of(&v).to_ascii_uppercase())
447 .unwrap_or_else(|| "GET".into());
448 let headers_obj = construct_headers(&[])?;
449 if let Some(hv) = prop(&init, "headers") {
450 for (k, v) in init_header_entries(&hv) {
451 append_header(&headers_obj, &k, &v);
452 }
453 }
454 let body = body_init_bytes(prop(&init, "body").as_ref());
455 let body_arr = store_body(&body);
456 let obj = with_host(|h| {
457 let mut m = IndexMap::new();
458 m.insert("@@native".into(), h.new_str("Request"));
459 m.insert("url".into(), h.new_str(url));
460 m.insert("method".into(), h.new_str(method));
461 m.insert("headers".into(), headers_obj);
462 m.insert("bodyUsed".into(), Value::Bool(false));
463 m.insert("@@body".into(), body_arr);
464 h.new_object(m)
465 });
466 with_host(|h| h.hide_prop(&obj, "@@body"));
467 Ok(obj)
468}
469
470fn body_init_bytes(v: Option<&Value>) -> Vec<u8> {
473 let Some(v) = v.filter(|v| !matches!(v, Value::Undef)) else {
474 return Vec::new();
475 };
476 if with_host(|h| h.is_null(v)) {
477 return Vec::new();
478 }
479 match super::native_tag(v).as_deref() {
480 Some("Blob") | Some("File") | Some("Response") | Some("Request") => return body_bytes(v),
481 Some("TypedArray") | Some("Buffer") => {
482 if let Some(e) = super::typedarray::elems_of(v) {
483 return e.iter().map(|x| *x as u8).collect();
484 }
485 }
486 _ => {}
487 }
488 if super::native_tag(v).as_deref() == Some("URLSearchParams") {
489 return str_of(v).into_bytes();
490 }
491 str_of(v).into_bytes()
492}
493
494pub fn body_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
497 match method {
498 "text" => {
499 set_prop(recv, "bodyUsed", Value::Bool(true));
500 let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
501 Ok(settled(Ok(new_str(s))))
502 }
503 "json" => {
504 set_prop(recv, "bodyUsed", Value::Bool(true));
505 let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
506 let parsed = crate::builtins::call_builtin_function("JSON.parse", vec![new_str(s)]);
507 Ok(settled(parsed))
508 }
509 "bytes" => {
510 set_prop(recv, "bodyUsed", Value::Bool(true));
511 Ok(settled(to_uint8array(&body_bytes(recv))))
512 }
513 "arrayBuffer" => {
514 set_prop(recv, "bodyUsed", Value::Bool(true));
515 let bytes = body_bytes(recv);
516 let buf = with_host(|h| {
517 let mut m = IndexMap::new();
518 m.insert("@@native".into(), h.new_str("ArrayBuffer"));
519 m.insert("byteLength".into(), Value::Float(bytes.len() as f64));
520 let arr = h.new_array(bytes.iter().map(|b| Value::Float(*b as f64)).collect());
521 m.insert("@@bytes".into(), arr);
522 h.new_object(m)
523 });
524 with_host(|h| h.hide_prop(&buf, "@@bytes"));
525 Ok(settled(Ok(buf)))
526 }
527 "blob" => {
528 set_prop(recv, "bodyUsed", Value::Bool(true));
529 let bytes = body_bytes(recv);
530 let ct = prop(recv, "headers")
531 .and_then(|h| get_header(&h, "content-type"))
532 .unwrap_or_default();
533 Ok(settled(Ok(new_blob(&bytes, &ct, None))))
534 }
535 "formData" => {
536 set_prop(recv, "bodyUsed", Value::Bool(true));
537 let s = String::from_utf8_lossy(&body_bytes(recv)).into_owned();
538 let fd = construct_form_data(&[])?;
539 for pair in s.split('&').filter(|p| !p.is_empty()) {
542 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
543 form_append(&fd, &percent_decode(k), &new_str(percent_decode(v)));
544 }
545 Ok(settled(Ok(fd)))
546 }
547 "clone" => {
548 let bytes = body_bytes(recv);
549 let clone = crate::builtins::deep_clone(recv)?;
550 set_prop(&clone, "@@body", store_body(&bytes));
551 set_prop(&clone, "bodyUsed", Value::Bool(false));
552 Ok(clone)
553 }
554 "slice" => {
555 let bytes = body_bytes(recv);
556 let start = args
557 .first()
558 .map(|v| with_host(|h| h.to_number(v)) as usize)
559 .unwrap_or(0)
560 .min(bytes.len());
561 let end = args
562 .get(1)
563 .filter(|v| !matches!(v, Value::Undef))
564 .map(|v| with_host(|h| h.to_number(v)) as usize)
565 .unwrap_or(bytes.len())
566 .clamp(start, bytes.len());
567 let ct = args.get(2).map(str_of).unwrap_or_default();
568 Ok(new_blob(&bytes[start..end], &ct, None))
569 }
570 "stream" => Err(crate::host::type_error(
571 "Response.body streaming is not implemented on this build",
572 )),
573 _ => Err(crate::host::type_error(&format!(
574 "{method} is not a function"
575 ))),
576 }
577}
578
579fn throw_js(v: Value) -> String {
582 let msg = with_host(|h| h.str_of(&v));
583 with_host(|h| h.exc = Some(v));
584 msg
585}
586
587fn percent_decode(s: &str) -> String {
588 let bytes = s.replace('+', " ").into_bytes();
589 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
590 let mut i = 0;
591 while i < bytes.len() {
592 if bytes[i] == b'%' && i + 2 < bytes.len() {
593 if let Ok(b) = u8::from_str_radix(&String::from_utf8_lossy(&bytes[i + 1..i + 3]), 16) {
594 out.push(b);
595 i += 3;
596 continue;
597 }
598 }
599 out.push(bytes[i]);
600 i += 1;
601 }
602 String::from_utf8_lossy(&out).into_owned()
603}
604
605fn new_blob(bytes: &[u8], content_type: &str, file_name: Option<&str>) -> Value {
608 let body = store_body(bytes);
609 let obj = with_host(|h| {
610 let mut m = IndexMap::new();
611 m.insert(
612 "@@native".into(),
613 h.new_str(if file_name.is_some() { "File" } else { "Blob" }),
614 );
615 m.insert("size".into(), Value::Float(bytes.len() as f64));
616 m.insert("type".into(), h.new_str(content_type.to_ascii_lowercase()));
617 if let Some(n) = file_name {
618 m.insert("name".into(), h.new_str(n.to_string()));
619 m.insert("lastModified".into(), Value::Float(0.0));
620 }
621 m.insert("@@body".into(), body);
622 h.new_object(m)
623 });
624 with_host(|h| h.hide_prop(&obj, "@@body"));
625 obj
626}
627
628pub fn construct_blob(args: &[Value]) -> Result<Value, String> {
629 let mut bytes = Vec::new();
630 if let Some(parts) = args.first() {
631 for p in crate::host::iter_all(parts).unwrap_or_default() {
632 bytes.extend(body_init_bytes(Some(&p)));
633 }
634 }
635 let ct = args
636 .get(1)
637 .and_then(|o| prop(o, "type"))
638 .map(|v| str_of(&v))
639 .unwrap_or_default();
640 Ok(new_blob(&bytes, &ct, None))
641}
642
643pub fn construct_file(args: &[Value]) -> Result<Value, String> {
645 let mut bytes = Vec::new();
646 if let Some(parts) = args.first() {
647 for p in crate::host::iter_all(parts).unwrap_or_default() {
648 bytes.extend(body_init_bytes(Some(&p)));
649 }
650 }
651 let name = super::arg_str(args, 1);
652 let ct = args
653 .get(2)
654 .and_then(|o| prop(o, "type"))
655 .map(|v| str_of(&v))
656 .unwrap_or_default();
657 Ok(new_blob(&bytes, &ct, Some(&name)))
658}
659
660pub fn construct_form_data(_args: &[Value]) -> Result<Value, String> {
663 let obj = with_host(|h| {
664 let mut m = IndexMap::new();
665 m.insert("@@native".into(), h.new_str("FormData"));
666 let empty = h.new_array(Vec::new());
667 m.insert("@@entries".into(), empty);
668 h.new_object(m)
669 });
670 with_host(|h| h.hide_prop(&obj, "@@entries"));
671 Ok(obj)
672}
673
674fn form_entries(recv: &Value) -> Vec<(String, Value)> {
675 let Some(arr) = prop(recv, "@@entries") else {
676 return Vec::new();
677 };
678 with_host(|h| match h.get(&arr) {
679 Some(JsObj::Array(items)) => items
680 .iter()
681 .filter_map(|pair| match h.get(pair) {
682 Some(JsObj::Array(kv)) if kv.len() == 2 => Some((h.str_of(&kv[0]), kv[1].clone())),
683 _ => None,
684 })
685 .collect(),
686 _ => Vec::new(),
687 })
688}
689
690fn set_form_entries(recv: &Value, entries: &[(String, Value)]) {
691 let arr = with_host(|h| {
692 let pairs: Vec<Value> = entries
693 .iter()
694 .map(|(k, v)| {
695 let k = h.new_str(k.clone());
696 h.new_array(vec![k, v.clone()])
697 })
698 .collect();
699 h.new_array(pairs)
700 });
701 set_prop(recv, "@@entries", arr);
702}
703
704fn form_append(recv: &Value, name: &str, value: &Value) {
705 let mut e = form_entries(recv);
706 e.push((name.to_string(), value.clone()));
707 set_form_entries(recv, &e);
708}
709
710pub fn form_data_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
711 let name = super::arg_str(args, 0);
712 match method {
713 "append" => {
714 form_append(recv, &name, args.get(1).unwrap_or(&Value::Undef));
715 Ok(Value::Undef)
716 }
717 "set" => {
718 let mut e = form_entries(recv);
719 e.retain(|(k, _)| *k != name);
720 e.push((name, args.get(1).cloned().unwrap_or(Value::Undef)));
721 set_form_entries(recv, &e);
722 Ok(Value::Undef)
723 }
724 "delete" => {
725 let mut e = form_entries(recv);
726 e.retain(|(k, _)| *k != name);
727 set_form_entries(recv, &e);
728 Ok(Value::Undef)
729 }
730 "get" => Ok(form_entries(recv)
731 .into_iter()
732 .find(|(k, _)| *k == name)
733 .map(|(_, v)| v)
734 .unwrap_or_else(|| with_host(|h| h.null()))),
735 "getAll" => {
736 let vals: Vec<Value> = form_entries(recv)
737 .into_iter()
738 .filter(|(k, _)| *k == name)
739 .map(|(_, v)| v)
740 .collect();
741 Ok(with_host(|h| h.new_array(vals)))
742 }
743 "has" => Ok(Value::Bool(
744 form_entries(recv).iter().any(|(k, _)| *k == name),
745 )),
746 "forEach" => {
747 let cb = args.first().cloned().unwrap_or(Value::Undef);
748 for (k, v) in form_entries(recv) {
749 let kv = new_str(k);
750 crate::host::invoke(&cb, vec![v, kv, recv.clone()], None)?;
751 }
752 Ok(Value::Undef)
753 }
754 "keys" | "values" | "entries" | "@@iterator" => {
755 let items: Vec<Value> = form_entries(recv)
756 .into_iter()
757 .map(|(k, v)| match method {
758 "keys" => new_str(k),
759 "values" => v,
760 _ => {
761 let kv = new_str(k);
762 with_host(|h| h.new_array(vec![kv, v]))
763 }
764 })
765 .collect();
766 Ok(with_host(|h| {
767 h.alloc(JsObj::Iter {
768 items,
769 idx: 0,
770 array: None,
771 })
772 }))
773 }
774 _ => Err(crate::host::type_error(&format!(
775 "{method} is not a function"
776 ))),
777 }
778}
779
780pub fn new_abort_signal() -> Value {
783 with_host(|h| {
784 let mut m = IndexMap::new();
785 m.insert("@@native".into(), h.new_str("AbortSignal"));
786 m.insert("@@aborted".into(), Value::Bool(false));
787 m.insert("@@reason".into(), Value::Undef);
788 m.insert("@@onabort".into(), h.null());
789 h.new_object(m)
790 })
791}
792
793pub fn construct_abort_controller(_args: &[Value]) -> Result<Value, String> {
794 let signal = new_abort_signal();
795 Ok(with_host(|h| {
796 let mut m = IndexMap::new();
797 m.insert("@@native".into(), h.new_str("AbortController"));
798 m.insert("@@signal".into(), signal);
799 h.new_object(m)
800 }))
801}
802
803fn dom_exception(name: &str, message: &str) -> Value {
808 let msg = with_host(|h| h.new_str(message.to_string()));
809 let nm = with_host(|h| h.new_str(name.to_string()));
810 crate::builtins::dom_exception(&[msg, nm])
811}
812
813pub fn fire_timeout_abort(idx: u32) -> Result<Value, String> {
816 let signal = Value::Obj(idx);
817 let e = dom_exception("TimeoutError", "The operation was aborted due to timeout");
818 abort_signal(&signal, e)?;
819 Ok(Value::Undef)
820}
821
822fn abort_signal(signal: &Value, reason: Value) -> Result<(), String> {
824 if matches!(prop(signal, "@@aborted"), Some(Value::Bool(true))) {
825 return Ok(());
826 }
827 let reason = if matches!(reason, Value::Undef) {
828 dom_exception("AbortError", "This operation was aborted")
829 } else {
830 reason
831 };
832 set_prop(signal, "@@aborted", Value::Bool(true));
833 set_prop(signal, "@@reason", reason);
834 if let Some(cb) = prop(signal, "@@onabort") {
835 if with_host(|h| crate::host::is_callable(h, &cb)) {
836 crate::host::invoke(&cb, vec![Value::Undef], Some(signal.clone()))?;
837 }
838 }
839 if let Some(list) = prop(signal, "@@abortListeners") {
840 for cb in crate::host::iter_all(&list).unwrap_or_default() {
841 crate::host::invoke(&cb, vec![Value::Undef], Some(signal.clone()))?;
842 }
843 }
844 Ok(())
845}
846
847pub fn abort_controller_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
848 match method {
849 "abort" => {
850 let signal = prop(recv, "@@signal").unwrap_or(Value::Undef);
851 abort_signal(&signal, args.first().cloned().unwrap_or(Value::Undef))?;
852 Ok(Value::Undef)
853 }
854 _ => Err(crate::host::type_error(&format!(
855 "{method} is not a function"
856 ))),
857 }
858}
859
860pub fn abort_signal_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
861 match method {
862 "throwIfAborted" => {
863 if matches!(prop(recv, "@@aborted"), Some(Value::Bool(true))) {
864 let reason = prop(recv, "@@reason").unwrap_or(Value::Undef);
865 return Err(throw_js(reason));
866 }
867 Ok(Value::Undef)
868 }
869 "addEventListener" => {
870 if super::arg_str(args, 0) == "abort" {
871 let cb = args.get(1).cloned().unwrap_or(Value::Undef);
872 let existing = prop(recv, "@@abortListeners");
873 let list = match existing {
874 Some(l) => l,
875 None => {
876 let l = with_host(|h| h.new_array(Vec::new()));
877 set_prop(recv, "@@abortListeners", l.clone());
878 with_host(|h| h.hide_prop(recv, "@@abortListeners"));
879 l
880 }
881 };
882 with_host(|h| {
883 if let Some(JsObj::Array(items)) = h.get_mut(&list) {
884 items.push(cb);
885 }
886 });
887 }
888 Ok(Value::Undef)
889 }
890 "removeEventListener" => Ok(Value::Undef),
891 _ => Err(crate::host::type_error(&format!(
892 "{method} is not a function"
893 ))),
894 }
895}
896
897pub fn abort_signal_static(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
899 match method {
900 "abort" => {
901 let s = new_abort_signal();
902 let r = abort_signal(&s, args.first().cloned().unwrap_or(Value::Undef));
903 Some(r.map(|_| s))
904 }
905 "timeout" => {
906 let s = new_abort_signal();
907 let ms = super::arg_num(args, 0);
911 let Value::Obj(idx) = s else {
912 return Some(Ok(s));
913 };
914 let cb = with_host(|h| h.alloc(JsObj::Builtin(format!("@@aborttimeout:{idx}"))));
915 with_host(|h| h.add_timer(ms, cb, Vec::new(), None));
916 Some(Ok(s))
917 }
918 _ => None,
919 }
920}
921
922pub fn fetch(args: &[Value]) -> Result<Value, String> {
926 let promise = with_host(|h| h.new_promise());
927 let id = with_host(|h| h.promise_id(&promise).unwrap_or(0));
928
929 let input = args.first().cloned().unwrap_or(Value::Undef);
930 let init = args.get(1).cloned().unwrap_or(Value::Undef);
931
932 let from_request = super::native_tag(&input).as_deref() == Some("Request");
935 let url = if from_request {
936 prop(&input, "url").map(|v| str_of(&v)).unwrap_or_default()
937 } else {
938 str_of(&input)
939 };
940
941 let mut method = if from_request {
942 prop(&input, "method")
943 .map(|v| str_of(&v))
944 .unwrap_or_else(|| "GET".into())
945 } else {
946 "GET".into()
947 };
948 if let Some(m) = prop(&init, "method") {
949 method = str_of(&m);
950 }
951 method = method.to_ascii_uppercase();
952
953 let mut headers: Vec<(String, String)> = Vec::new();
954 if from_request {
955 if let Some(h) = prop(&input, "headers") {
956 headers = headers_entries(&h);
957 }
958 }
959 if let Some(hv) = prop(&init, "headers") {
960 headers.extend(init_header_entries(&hv));
961 }
962
963 let body = match prop(&init, "body") {
964 Some(b) => body_init_bytes(Some(&b)),
965 None if from_request => body_bytes(&input),
966 None => Vec::new(),
967 };
968
969 if let Some(sig) = prop(&init, "signal") {
971 if matches!(prop(&sig, "@@aborted"), Some(Value::Bool(true))) {
972 let reason = prop(&sig, "@@reason")
973 .unwrap_or_else(|| dom_exception("AbortError", "This operation was aborted"));
974 crate::host::reject_promise_val(id, reason);
975 return Ok(promise);
976 }
977 }
978
979 let Some(target) = parse_target(&url) else {
980 crate::host::reject_promise_val(id, fetch_failed("unknown scheme"));
981 return Ok(promise);
982 };
983
984 let wire = build_request_bytes(&target, &method, &headers, &body);
985 let io_tx = with_host(|h| h.io_sender());
986 with_host(|h| h.incr_handle());
987 let url_for_response = url.clone();
988 std::thread::spawn(move || {
989 let raw = if target.tls {
990 let config = crate::stdlib::tls::client_config(true);
991 crate::stdlib::https::exchange(&target.host, target.port, &target.host, config, &wire)
992 } else {
993 crate::stdlib::http::exchange(&target.host, target.port, &wire)
994 };
995 let _ = io_tx.send(Box::new(move || {
996 with_host(|h| h.decr_handle());
997 match raw {
998 Ok(raw) => {
999 let parsed = crate::stdlib::http::parse_raw_response(&raw);
1000 let (status, message, headers, body) =
1001 (parsed.status, parsed.message, parsed.headers, parsed.body);
1002 let resp = build_response(status, &message, &headers, &body, &url_for_response);
1003 crate::host::resolve_promise_val(id, resp);
1004 }
1005 Err(msg) => crate::host::reject_promise_val(id, fetch_failed(&msg)),
1006 }
1007 Ok(())
1008 }));
1009 });
1010 Ok(promise)
1011}
1012
1013fn fetch_failed(cause: &str) -> Value {
1017 with_host(|h| {
1018 let e = crate::builtins::synth_error(h, "TypeError: fetch failed");
1019 let c = crate::builtins::synth_error(h, &format!("Error: {cause}"));
1020 if let Some(JsObj::Object(p)) = h.get_mut(&e) {
1021 p.insert("cause".into(), c);
1022 }
1023 e
1024 })
1025}
1026
1027struct Target {
1028 host: String,
1029 port: u16,
1030 path: String,
1031 tls: bool,
1032}
1033
1034fn parse_target(url: &str) -> Option<Target> {
1035 let (tls, rest) = match url.strip_prefix("https://") {
1038 Some(r) => (true, r),
1039 None => (false, url.strip_prefix("http://")?),
1040 };
1041 let (authority, path) = match rest.find('/') {
1042 Some(i) => (&rest[..i], rest[i..].to_string()),
1043 None => (rest, "/".to_string()),
1044 };
1045 let authority = authority.split('@').next_back().unwrap_or(authority);
1046 let (host, port) = match authority.rsplit_once(':') {
1047 Some((h, p)) => match p.parse::<u16>() {
1048 Ok(n) => (h.to_string(), n),
1049 Err(_) => (authority.to_string(), if tls { 443 } else { 80 }),
1050 },
1051 None => (authority.to_string(), if tls { 443 } else { 80 }),
1052 };
1053 if host.is_empty() {
1054 return None;
1055 }
1056 Some(Target {
1057 host,
1058 port,
1059 path,
1060 tls,
1061 })
1062}
1063
1064fn build_request_bytes(
1065 target: &Target,
1066 method: &str,
1067 headers: &[(String, String)],
1068 body: &[u8],
1069) -> Vec<u8> {
1070 let mut has_host = false;
1071 let mut has_len = false;
1072 let mut block = String::new();
1073 for (k, v) in headers {
1074 if k.eq_ignore_ascii_case("host") {
1075 has_host = true;
1076 }
1077 if k.eq_ignore_ascii_case("content-length") {
1078 has_len = true;
1079 }
1080 if k.eq_ignore_ascii_case("connection") {
1081 continue;
1082 }
1083 block.push_str(&format!("{k}: {v}\r\n"));
1084 }
1085 let default_port = if target.tls { 443 } else { 80 };
1086 let host_header = if target.port == default_port {
1087 target.host.clone()
1088 } else {
1089 format!("{}:{}", target.host, target.port)
1090 };
1091 let mut req = format!("{method} {} HTTP/1.1\r\n", target.path);
1092 if !has_host {
1093 req.push_str(&format!("Host: {host_header}\r\n"));
1094 }
1095 req.push_str(&block);
1096 if !has_len && !body.is_empty() {
1097 req.push_str(&format!("Content-Length: {}\r\n", body.len()));
1098 }
1099 req.push_str("Connection: close\r\n\r\n");
1101 let mut wire = req.into_bytes();
1102 wire.extend_from_slice(body);
1103 wire
1104}
1105
1106pub fn instance_call(
1109 tag: &str,
1110 recv: &Value,
1111 method: &str,
1112 args: &[Value],
1113) -> Result<Value, String> {
1114 match tag {
1115 "Headers" => headers_call(recv, method, args),
1116 "FormData" => form_data_call(recv, method, args),
1117 "AbortController" => abort_controller_call(recv, method, args),
1118 "AbortSignal" => abort_signal_call(recv, method, args),
1119 _ => body_call(recv, method, args),
1120 }
1121}
1122
1123pub fn construct(name: &str, args: &[Value]) -> Option<Result<Value, String>> {
1124 Some(match name {
1125 "Headers" => construct_headers(args),
1126 "Request" => construct_request(args),
1127 "Response" => construct_response(args),
1128 "Blob" => construct_blob(args),
1129 "File" => construct_file(args),
1130 "FormData" => construct_form_data(args),
1131 "AbortController" => construct_abort_controller(args),
1132 "AbortSignal" => Err(crate::host::type_error(
1133 "Illegal constructor: use AbortSignal.abort() or AbortSignal.timeout()",
1134 )),
1135 _ => return None,
1136 })
1137}
1138
1139pub fn static_call(ns: &str, method: &str, args: &[Value]) -> Option<Result<Value, String>> {
1141 match ns {
1142 "AbortSignal" => abort_signal_static(method, args),
1143 "Response" => match method {
1144 "json" => {
1145 let body = crate::builtins::call_builtin_function(
1146 "JSON.stringify",
1147 vec![args.first()?.clone()],
1148 )
1149 .map(|v| str_of(&v))
1150 .unwrap_or_default();
1151 let init = args.get(1).cloned().unwrap_or(Value::Undef);
1152 let status = match prop(&init, "status") {
1153 Some(v) => with_host(|h| h.to_number(&v)) as u16,
1154 None => 200,
1155 };
1156 let mut headers =
1157 vec![("content-type".to_string(), "application/json".to_string())];
1158 if let Some(hv) = prop(&init, "headers") {
1159 headers.extend(init_header_entries(&hv));
1160 }
1161 Some(Ok(build_response(
1162 status,
1163 "",
1164 &headers,
1165 body.as_bytes(),
1166 "",
1167 )))
1168 }
1169 "error" => Some(Ok(build_response(0, "", &[], &[], ""))),
1170 "redirect" => {
1171 let loc = super::arg_str(args, 0);
1172 let status = match args.get(1) {
1173 Some(v) => with_host(|h| h.to_number(v)) as u16,
1174 None => 302,
1175 };
1176 Some(Ok(build_response(
1177 status,
1178 "",
1179 &[("location".into(), loc)],
1180 &[],
1181 "",
1182 )))
1183 }
1184 _ => None,
1185 },
1186 _ => None,
1187 }
1188}
1189
1190pub const RESPONSE_STATICS: &[&str] = &["json", "error", "redirect"];
1191pub const ABORT_SIGNAL_STATICS: &[&str] = &["abort", "timeout"];