1use super::arg_str;
7use crate::host::{with_host, JsObj};
8use fusevm::Value;
9use indexmap::IndexMap;
10
11pub const MODULE_METHODS: &[&str] = &[
12 "parse",
13 "format",
14 "fileURLToPath",
15 "fileURLToPathBuffer",
16 "pathToFileURL",
17 "domainToASCII",
18 "domainToUnicode",
19 "urlToHttpOptions",
20 "resolve",
21 "resolveObject",
22];
23
24struct Parts {
26 protocol: String,
27 username: String,
28 password: String,
29 hostname: String,
30 port: String,
31 pathname: String,
32 search: String,
33 hash: String,
34}
35
36impl Parts {
37 fn host(&self) -> String {
38 if self.port.is_empty() {
39 self.hostname.clone()
40 } else {
41 format!("{}:{}", self.hostname, self.port)
42 }
43 }
44 fn origin(&self) -> String {
45 if self.hostname.is_empty() {
46 "null".into()
47 } else {
48 format!("{}//{}", self.protocol, self.host())
49 }
50 }
51 fn href(&self) -> String {
52 let auth = if self.username.is_empty() {
53 String::new()
54 } else if self.password.is_empty() {
55 format!("{}@", self.username)
56 } else {
57 format!("{}:{}@", self.username, self.password)
58 };
59 format!(
60 "{}//{auth}{}{}{}{}",
61 self.protocol,
62 self.host(),
63 self.pathname,
64 self.search,
65 self.hash
66 )
67 }
68}
69
70fn parse_absolute(input: &str) -> Option<Parts> {
72 let (scheme, rest) = input.split_once("://")?;
73 if scheme.is_empty()
74 || !scheme
75 .chars()
76 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
77 {
78 return None;
79 }
80 let auth_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
82 let authority = &rest[..auth_end];
83 let mut tail = &rest[auth_end..];
84
85 let (userinfo, hostport) = match authority.rsplit_once('@') {
86 Some((u, h)) => (u, h),
87 None => ("", authority),
88 };
89 let (username, password) = match userinfo.split_once(':') {
90 Some((u, p)) => (u.to_string(), p.to_string()),
91 None => (userinfo.to_string(), String::new()),
92 };
93 let (hostname, port) = match hostport.split_once(':') {
94 Some((h, p)) => (h.to_string(), p.to_string()),
95 None => (hostport.to_string(), String::new()),
96 };
97
98 let hash = match tail.find('#') {
99 Some(i) => {
100 let h = tail[i..].to_string();
101 tail = &tail[..i];
102 h
103 }
104 None => String::new(),
105 };
106 let search = match tail.find('?') {
107 Some(i) => {
108 let s = tail[i..].to_string();
109 tail = &tail[..i];
110 s
111 }
112 None => String::new(),
113 };
114 let pathname = if tail.is_empty() {
115 "/".to_string()
116 } else {
117 tail.to_string()
118 };
119
120 Some(Parts {
121 protocol: format!("{scheme}:"),
122 username,
123 password,
124 hostname,
125 port,
126 pathname,
127 search,
128 hash,
129 })
130}
131
132pub fn construct(args: &[Value]) -> Result<Value, String> {
134 let input = arg_str(args, 0);
135 let parts = parse_absolute(&input)
136 .or_else(|| {
137 if args.len() > 1 {
139 let base = arg_str(args, 1);
140 parse_absolute(&base).map(|mut b| {
141 if input.starts_with('/') {
142 b.pathname = input.clone();
143 } else {
144 b.pathname = format!("/{input}");
145 }
146 b.search.clear();
147 b.hash.clear();
148 b
149 })
150 } else {
151 None
152 }
153 })
154 .ok_or_else(|| format!("TypeError: Invalid URL: {input}"))?;
155 Ok(build(&parts))
156}
157
158fn build(p: &Parts) -> Value {
159 let query = p.search.strip_prefix('?').unwrap_or(&p.search);
164 let search_params = make_search_params(&parse_query(query));
165 with_host(|h| {
166 let mut m = IndexMap::new();
167 m.insert("@@native".into(), h.new_str("URL"));
168 m.insert("href".into(), h.new_str(p.href()));
169 m.insert("origin".into(), h.new_str(p.origin()));
170 m.insert("protocol".into(), h.new_str(p.protocol.clone()));
171 m.insert("username".into(), h.new_str(p.username.clone()));
172 m.insert("password".into(), h.new_str(p.password.clone()));
173 m.insert("host".into(), h.new_str(p.host()));
174 m.insert("hostname".into(), h.new_str(p.hostname.clone()));
175 m.insert("port".into(), h.new_str(p.port.clone()));
176 m.insert("pathname".into(), h.new_str(p.pathname.clone()));
177 m.insert("search".into(), h.new_str(p.search.clone()));
178 m.insert("searchParams".into(), search_params);
179 m.insert("hash".into(), h.new_str(p.hash.clone()));
180 h.new_object(m)
181 })
182}
183
184pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
185 Some(match method {
186 "parse" => Ok(legacy_parse(&arg_str(args, 0))),
187 "format" => Ok(with_host(|h| {
188 let v = args.first().cloned().unwrap_or(Value::Undef);
190 let s = match h.get(&v) {
191 Some(JsObj::Object(p)) => p.get("href").map(|x| h.str_of(x)).unwrap_or_default(),
192 _ => h.str_of(&v),
193 };
194 h.new_str(s)
195 })),
196 "fileURLToPath" => file_url_to_path(args).map(|s| with_host(|h| h.new_str(s))),
200 "fileURLToPathBuffer" => {
202 file_url_to_path(args).map(|s| super::buffer::from_bytes(s.as_bytes()))
203 }
204 "pathToFileURL" => Ok(path_to_file_url(&arg_str(args, 0))),
206 "domainToASCII" => Ok(punycode_domain(args, true)),
210 "domainToUnicode" => Ok(punycode_domain(args, false)),
211 "urlToHttpOptions" => Ok(url_to_http_options(
213 &args.first().cloned().unwrap_or(Value::Undef),
214 )),
215 "resolve" => {
217 let from = arg_str(args, 0);
218 let to = arg_str(args, 1);
219 Ok(with_host(|h| h.new_str(legacy_resolve(&from, &to))))
220 }
221 "resolveObject" => {
223 let from = arg_str(args, 0);
224 let to = arg_str(args, 1);
225 Ok(legacy_parse(&legacy_resolve(&from, &to)))
226 }
227 _ => return None,
228 })
229}
230
231fn legacy_parse(input: &str) -> Value {
234 let p = parse_absolute(input);
235 with_host(|h| {
236 let mut m = IndexMap::new();
237 let null = h.null();
238 let opt = |h: &mut crate::host::JsHost, s: &str| {
239 if s.is_empty() {
240 h.null()
241 } else {
242 h.new_str(s)
243 }
244 };
245 match p {
246 Some(p) => {
247 let auth = if p.username.is_empty() {
248 String::new()
249 } else if p.password.is_empty() {
250 p.username.clone()
251 } else {
252 format!("{}:{}", p.username, p.password)
253 };
254 m.insert("protocol".into(), h.new_str(p.protocol.clone()));
255 m.insert("slashes".into(), Value::Bool(true));
256 m.insert("auth".into(), opt(h, &auth));
257 m.insert("host".into(), h.new_str(p.host()));
258 m.insert("port".into(), opt(h, &p.port));
259 m.insert("hostname".into(), h.new_str(p.hostname.clone()));
260 m.insert("hash".into(), opt(h, &p.hash));
261 m.insert("search".into(), opt(h, &p.search));
262 m.insert("query".into(), opt(h, p.search.trim_start_matches('?')));
263 m.insert("pathname".into(), h.new_str(p.pathname.clone()));
264 m.insert(
265 "path".into(),
266 h.new_str(format!("{}{}", p.pathname, p.search)),
267 );
268 m.insert("href".into(), h.new_str(p.href()));
269 }
270 None => {
271 m.insert("protocol".into(), null.clone());
272 m.insert("slashes".into(), null.clone());
273 m.insert("auth".into(), null.clone());
274 m.insert("host".into(), null.clone());
275 m.insert("port".into(), null.clone());
276 m.insert("hostname".into(), null.clone());
277 m.insert("hash".into(), null.clone());
278 m.insert("search".into(), null.clone());
279 m.insert("query".into(), null.clone());
280 m.insert("pathname".into(), h.new_str(input));
281 m.insert("path".into(), h.new_str(input));
282 m.insert("href".into(), h.new_str(input));
283 }
284 }
285 h.new_object(m)
286 })
287}
288
289pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
291 match method {
292 "toString" | "toJSON" => Ok(with_host(|h| match h.get(recv) {
293 Some(JsObj::Object(p)) => p.get("href").cloned().unwrap_or(Value::Undef),
294 _ => Value::Undef,
295 })),
296 _ => Err(crate::host::type_error(&format!(
297 "url.{method} is not a function"
298 ))),
299 }
300}
301
302fn url_href(v: &Value) -> String {
307 with_host(|h| match h.get(v) {
308 Some(JsObj::Object(p)) => match p.get("@@native").map(|x| h.str_of(x)).as_deref() {
309 Some("URL") => p.get("href").map(|x| h.str_of(x)).unwrap_or_default(),
310 _ => h.str_of(v),
311 },
312 _ => h.str_of(v),
313 })
314}
315
316fn file_url_to_path(args: &[Value]) -> Result<String, String> {
318 let v = args.first().cloned().unwrap_or(Value::Undef);
319 let href = url_href(&v);
320 let rest = href
321 .strip_prefix("file://")
322 .ok_or_else(|| crate::host::type_error("The URL must be of scheme file"))?;
323 let path = match rest.find('/') {
325 Some(0) => rest,
326 Some(i) => &rest[i..],
327 None => "/",
328 };
329 Ok(percent_decode(path))
330}
331
332fn path_to_file_url(path: &str) -> Value {
335 let enc = encode_path_component(path);
336 let pathname = if enc.starts_with('/') {
337 enc
338 } else {
339 format!("/{enc}")
340 };
341 let parts = Parts {
342 protocol: "file:".into(),
343 username: String::new(),
344 password: String::new(),
345 hostname: String::new(),
346 port: String::new(),
347 pathname,
348 search: String::new(),
349 hash: String::new(),
350 };
351 build(&parts)
352}
353
354fn punycode_domain(args: &[Value], ascii: bool) -> Value {
356 let method = if ascii { "toASCII" } else { "toUnicode" };
357 match super::punycode::call(method, args) {
358 Some(Ok(v)) => v,
359 _ => with_host(|h| h.new_str("")),
360 }
361}
362
363fn url_to_http_options(v: &Value) -> Value {
366 let get = |key: &str| -> String {
367 with_host(|h| match h.get(v) {
368 Some(JsObj::Object(p)) => p.get(key).map(|x| h.str_of(x)).unwrap_or_default(),
369 _ => String::new(),
370 })
371 };
372 let protocol = get("protocol");
373 let mut hostname = get("hostname");
374 if hostname.starts_with('[') && hostname.ends_with(']') && hostname.len() >= 2 {
375 hostname = hostname[1..hostname.len() - 1].to_string();
376 }
377 let hash = get("hash");
378 let search = get("search");
379 let pathname = get("pathname");
380 let href = get("href");
381 let port = get("port");
382 let username = get("username");
383 let password = get("password");
384 let path = format!("{pathname}{search}");
385 let auth = if username.is_empty() && password.is_empty() {
386 None
387 } else {
388 Some(format!(
389 "{}:{}",
390 percent_decode(&username),
391 percent_decode(&password)
392 ))
393 };
394 let port_num = if port.is_empty() {
395 None
396 } else {
397 port.parse::<f64>().ok()
398 };
399 with_host(|h| {
400 let mut m = IndexMap::new();
401 m.insert("protocol".into(), h.new_str(protocol));
402 m.insert("hostname".into(), h.new_str(hostname));
403 m.insert("hash".into(), h.new_str(hash));
404 m.insert("search".into(), h.new_str(search));
405 m.insert("pathname".into(), h.new_str(pathname));
406 m.insert("path".into(), h.new_str(path));
407 m.insert("href".into(), h.new_str(href));
408 if let Some(n) = port_num {
409 m.insert("port".into(), Value::Float(n));
410 }
411 if let Some(a) = auth {
412 m.insert("auth".into(), h.new_str(a));
413 }
414 h.new_object(m)
415 })
416}
417
418fn percent_decode(s: &str) -> String {
421 let b = s.as_bytes();
422 let mut out: Vec<u8> = Vec::with_capacity(b.len());
423 let mut i = 0;
424 while i < b.len() {
425 if b[i] == b'%' && i + 2 < b.len() {
426 if let (Some(hi), Some(lo)) = (hex_val(b[i + 1]), hex_val(b[i + 2])) {
427 out.push((hi << 4) | lo);
428 i += 3;
429 continue;
430 }
431 }
432 out.push(b[i]);
433 i += 1;
434 }
435 String::from_utf8_lossy(&out).into_owned()
436}
437
438fn encode_path_component(s: &str) -> String {
441 let mut out = String::with_capacity(s.len());
442 for &b in s.as_bytes() {
443 let keep = b.is_ascii_alphanumeric()
444 || matches!(
445 b,
446 b'/' | b'-'
447 | b'.'
448 | b'_'
449 | b'~'
450 | b'!'
451 | b'$'
452 | b'&'
453 | b'\''
454 | b'('
455 | b')'
456 | b'*'
457 | b'+'
458 | b','
459 | b';'
460 | b'='
461 | b':'
462 | b'@'
463 );
464 if keep {
465 out.push(b as char);
466 } else {
467 out.push('%');
468 out.push(hex_upper(b >> 4));
469 out.push(hex_upper(b & 0x0f));
470 }
471 }
472 out
473}
474
475struct UriRef {
479 scheme: Option<String>,
480 authority: Option<String>,
481 path: String,
482 query: Option<String>,
483 fragment: Option<String>,
484}
485
486fn split_uri(input: &str) -> UriRef {
488 let mut rest = input;
489 let mut scheme = None;
492 if let Some(colon) = rest.find(':') {
493 let cand = &rest[..colon];
494 let scheme_ok = !cand.is_empty()
495 && cand.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
496 && cand
497 .chars()
498 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
499 && cand.find(['/', '?', '#']).is_none();
500 if scheme_ok {
501 scheme = Some(cand.to_string());
502 rest = &rest[colon + 1..];
503 }
504 }
505 let mut fragment = None;
506 if let Some(h) = rest.find('#') {
507 fragment = Some(rest[h + 1..].to_string());
508 rest = &rest[..h];
509 }
510 let mut query = None;
511 if let Some(q) = rest.find('?') {
512 query = Some(rest[q + 1..].to_string());
513 rest = &rest[..q];
514 }
515 let mut authority = None;
516 if let Some(r) = rest.strip_prefix("//") {
517 let end = r.find('/').unwrap_or(r.len());
518 authority = Some(r[..end].to_string());
519 rest = &r[end..];
520 }
521 UriRef {
522 scheme,
523 authority,
524 path: rest.to_string(),
525 query,
526 fragment,
527 }
528}
529
530fn merge_paths(base: &UriRef, ref_path: &str) -> String {
532 if base.authority.is_some() && base.path.is_empty() {
533 format!("/{ref_path}")
534 } else {
535 match base.path.rfind('/') {
536 Some(i) => format!("{}{ref_path}", &base.path[..=i]),
537 None => ref_path.to_string(),
538 }
539 }
540}
541
542fn remove_last_segment(output: &mut String) {
544 match output.rfind('/') {
545 Some(pos) => output.truncate(pos),
546 None => output.clear(),
547 }
548}
549
550fn remove_dot_segments(path: &str) -> String {
552 let mut input = path.to_string();
553 let mut output = String::new();
554 while !input.is_empty() {
555 if let Some(r) = input.strip_prefix("../") {
556 input = r.to_string();
557 } else if let Some(r) = input.strip_prefix("./") {
558 input = r.to_string();
559 } else if let Some(r) = input.strip_prefix("/./") {
560 input = format!("/{r}");
561 } else if input == "/." {
562 input = "/".to_string();
563 } else if let Some(r) = input.strip_prefix("/../") {
564 input = format!("/{r}");
565 remove_last_segment(&mut output);
566 } else if input == "/.." {
567 input = "/".to_string();
568 remove_last_segment(&mut output);
569 } else if input == "." || input == ".." {
570 input.clear();
571 } else {
572 let start = usize::from(input.starts_with('/'));
573 let end = input[start..]
574 .find('/')
575 .map(|i| start + i)
576 .unwrap_or(input.len());
577 output.push_str(&input[..end]);
578 input.drain(..end);
579 }
580 }
581 output
582}
583
584fn resolve_ref(base: &UriRef, r: &UriRef) -> UriRef {
586 if r.scheme.is_some() {
587 return UriRef {
588 scheme: r.scheme.clone(),
589 authority: r.authority.clone(),
590 path: remove_dot_segments(&r.path),
591 query: r.query.clone(),
592 fragment: r.fragment.clone(),
593 };
594 }
595 let (authority, path, query) = if r.authority.is_some() {
596 (
597 r.authority.clone(),
598 remove_dot_segments(&r.path),
599 r.query.clone(),
600 )
601 } else if r.path.is_empty() {
602 let q = if r.query.is_some() {
603 r.query.clone()
604 } else {
605 base.query.clone()
606 };
607 (base.authority.clone(), base.path.clone(), q)
608 } else if r.path.starts_with('/') {
609 (
610 base.authority.clone(),
611 remove_dot_segments(&r.path),
612 r.query.clone(),
613 )
614 } else {
615 (
616 base.authority.clone(),
617 remove_dot_segments(&merge_paths(base, &r.path)),
618 r.query.clone(),
619 )
620 };
621 UriRef {
622 scheme: base.scheme.clone(),
623 authority,
624 path,
625 query,
626 fragment: r.fragment.clone(),
627 }
628}
629
630fn recompose(u: &UriRef) -> String {
632 let mut s = String::new();
633 if let Some(sc) = &u.scheme {
634 s.push_str(sc);
635 s.push(':');
636 }
637 if let Some(a) = &u.authority {
638 s.push_str("//");
639 s.push_str(a);
640 }
641 s.push_str(&u.path);
642 if let Some(q) = &u.query {
643 s.push('?');
644 s.push_str(q);
645 }
646 if let Some(f) = &u.fragment {
647 s.push('#');
648 s.push_str(f);
649 }
650 s
651}
652
653fn legacy_resolve(from: &str, to: &str) -> String {
655 recompose(&resolve_ref(&split_uri(from), &split_uri(to)))
656}
657
658pub const SEARCH_PARAMS_METHODS: &[&str] = &[
668 "get",
669 "getAll",
670 "has",
671 "set",
672 "append",
673 "delete",
674 "keys",
675 "values",
676 "entries",
677 "forEach",
678 "toString",
679 "sort",
680 "@@iterator",
681];
682
683fn make_search_params(pairs: &[(String, String)]) -> Value {
685 with_host(|h| {
686 let items: Vec<Value> = pairs
687 .iter()
688 .map(|(k, v)| {
689 let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
690 h.new_array(kv)
691 })
692 .collect();
693 let arr = h.new_array(items);
694 let mut m = IndexMap::new();
695 m.insert("@@native".into(), h.new_str("URLSearchParams"));
696 m.insert("@@pairs".into(), arr);
697 h.new_object(m)
698 })
699}
700
701fn pairs_of(recv: &Value) -> Vec<(String, String)> {
703 with_host(|h| {
704 let items: Vec<Value> = match h.get(recv) {
705 Some(JsObj::Object(p)) => match p.get("@@pairs").and_then(|a| h.get(a)) {
706 Some(JsObj::Array(items)) => items.clone(),
707 _ => Vec::new(),
708 },
709 _ => Vec::new(),
710 };
711 items
712 .iter()
713 .map(|it| match h.get(it) {
714 Some(JsObj::Array(kv)) => {
715 let kv = kv.clone();
716 let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
717 let v = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
718 (k, v)
719 }
720 _ => (h.str_of(it), String::new()),
721 })
722 .collect()
723 })
724}
725
726fn set_pairs(recv: &Value, pairs: &[(String, String)]) {
728 with_host(|h| {
729 let items: Vec<Value> = pairs
730 .iter()
731 .map(|(k, v)| {
732 let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
733 h.new_array(kv)
734 })
735 .collect();
736 let arr = h.new_array(items);
737 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
738 p.insert("@@pairs".into(), arr);
739 }
740 });
741}
742
743pub fn construct_search_params(args: &[Value]) -> Result<Value, String> {
746 let pairs = match args.first() {
747 None => Vec::new(),
748 Some(v) if matches!(v, Value::Undef) || with_host(|h| h.is_null(v)) => Vec::new(),
749 Some(v) => pairs_from_init(v),
750 };
751 Ok(make_search_params(&pairs))
752}
753
754fn pairs_from_init(v: &Value) -> Vec<(String, String)> {
755 if super::native_tag(v).as_deref() == Some("URLSearchParams") {
757 return pairs_of(v);
758 }
759 if let Some(s) = with_host(|h| h.as_str(v)) {
761 return parse_query(s.strip_prefix('?').unwrap_or(&s));
762 }
763 with_host(|h| match h.get(v) {
764 Some(JsObj::Array(items)) => {
766 let items = items.clone();
767 items
768 .iter()
769 .map(|it| match h.get(it) {
770 Some(JsObj::Array(kv)) => {
771 let kv = kv.clone();
772 let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
773 let val = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
774 (k, val)
775 }
776 _ => (h.str_of(it), String::new()),
777 })
778 .collect()
779 }
780 Some(JsObj::Object(p)) => {
782 let entries: Vec<(String, Value)> = p
783 .iter()
784 .filter(|(k, _)| !k.starts_with("@@"))
785 .map(|(k, val)| (k.clone(), val.clone()))
786 .collect();
787 entries
788 .into_iter()
789 .map(|(k, val)| (k, h.str_of(&val)))
790 .collect()
791 }
792 _ => Vec::new(),
793 })
794}
795
796pub fn search_params_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
798 match method {
799 "get" => {
800 let name = arg_str(args, 0);
801 match pairs_of(recv).into_iter().find(|(k, _)| *k == name) {
802 Some((_, v)) => Ok(with_host(|h| h.new_str(v))),
803 None => Ok(with_host(|h| h.null())),
804 }
805 }
806 "getAll" => {
807 let name = arg_str(args, 0);
808 let vals: Vec<String> = pairs_of(recv)
809 .into_iter()
810 .filter(|(k, _)| *k == name)
811 .map(|(_, v)| v)
812 .collect();
813 Ok(with_host(|h| {
814 let items = vals.into_iter().map(|v| h.new_str(v)).collect();
815 h.new_array(items)
816 }))
817 }
818 "has" => {
819 let name = arg_str(args, 0);
820 let pairs = pairs_of(recv);
821 let found = if args.len() > 1 {
822 let val = arg_str(args, 1);
823 pairs.iter().any(|(k, v)| *k == name && *v == val)
824 } else {
825 pairs.iter().any(|(k, _)| *k == name)
826 };
827 Ok(Value::Bool(found))
828 }
829 "append" => {
830 let mut pairs = pairs_of(recv);
831 pairs.push((arg_str(args, 0), arg_str(args, 1)));
832 set_pairs(recv, &pairs);
833 Ok(Value::Undef)
834 }
835 "set" => {
836 let name = arg_str(args, 0);
837 let val = arg_str(args, 1);
838 let mut pairs = pairs_of(recv);
839 let mut seen = false;
842 pairs.retain_mut(|(k, v)| {
843 if *k == name {
844 if seen {
845 false
846 } else {
847 *v = val.clone();
848 seen = true;
849 true
850 }
851 } else {
852 true
853 }
854 });
855 if !seen {
856 pairs.push((name, val));
857 }
858 set_pairs(recv, &pairs);
859 Ok(Value::Undef)
860 }
861 "delete" => {
862 let name = arg_str(args, 0);
863 let mut pairs = pairs_of(recv);
864 if args.len() > 1 {
865 let val = arg_str(args, 1);
866 pairs.retain(|(k, v)| !(*k == name && *v == val));
867 } else {
868 pairs.retain(|(k, _)| *k != name);
869 }
870 set_pairs(recv, &pairs);
871 Ok(Value::Undef)
872 }
873 "sort" => {
874 let mut pairs = pairs_of(recv);
875 pairs.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
877 set_pairs(recv, &pairs);
878 Ok(Value::Undef)
879 }
880 "toString" => {
881 let s = pairs_of(recv)
882 .iter()
883 .map(|(k, v)| format!("{}={}", form_encode(k), form_encode(v)))
884 .collect::<Vec<_>>()
885 .join("&");
886 Ok(with_host(|h| h.new_str(s)))
887 }
888 "keys" => {
889 let pairs = pairs_of(recv);
890 Ok(with_host(|h| {
891 let items = pairs.into_iter().map(|(k, _)| h.new_str(k)).collect();
892 h.alloc(JsObj::Iter { items, idx: 0 })
893 }))
894 }
895 "values" => {
896 let pairs = pairs_of(recv);
897 Ok(with_host(|h| {
898 let items = pairs.into_iter().map(|(_, v)| h.new_str(v)).collect();
899 h.alloc(JsObj::Iter { items, idx: 0 })
900 }))
901 }
902 "entries" | "@@iterator" => {
903 let pairs = pairs_of(recv);
904 Ok(with_host(|h| {
905 let items = pairs
906 .into_iter()
907 .map(|(k, v)| {
908 let kv = vec![h.new_str(k), h.new_str(v)];
909 h.new_array(kv)
910 })
911 .collect();
912 h.alloc(JsObj::Iter { items, idx: 0 })
913 }))
914 }
915 "forEach" => {
916 let cb = args.first().cloned().unwrap_or(Value::Undef);
917 let this_arg = args.get(1).cloned();
918 for (k, v) in pairs_of(recv) {
920 let (value, name) = with_host(|h| (h.new_str(v), h.new_str(k)));
921 crate::host::invoke(&cb, vec![value, name, recv.clone()], this_arg.clone())?;
922 }
923 Ok(Value::Undef)
924 }
925 _ => Err(crate::host::type_error(&format!(
926 "urlSearchParams.{method} is not a function"
927 ))),
928 }
929}
930
931fn parse_query(q: &str) -> Vec<(String, String)> {
933 q.split('&')
934 .filter(|s| !s.is_empty())
935 .map(|seg| match seg.split_once('=') {
936 Some((k, v)) => (form_decode(k), form_decode(v)),
937 None => (form_decode(seg), String::new()),
938 })
939 .collect()
940}
941
942fn form_decode(s: &str) -> String {
945 let b = s.as_bytes();
946 let mut out: Vec<u8> = Vec::with_capacity(b.len());
947 let mut i = 0;
948 while i < b.len() {
949 match b[i] {
950 b'+' => {
951 out.push(b' ');
952 i += 1;
953 }
954 b'%' if i + 2 < b.len() => match (hex_val(b[i + 1]), hex_val(b[i + 2])) {
955 (Some(hi), Some(lo)) => {
956 out.push((hi << 4) | lo);
957 i += 3;
958 }
959 _ => {
960 out.push(b'%');
961 i += 1;
962 }
963 },
964 c => {
965 out.push(c);
966 i += 1;
967 }
968 }
969 }
970 String::from_utf8_lossy(&out).into_owned()
971}
972
973fn form_encode(s: &str) -> String {
976 let mut out = String::with_capacity(s.len());
977 for &b in s.as_bytes() {
978 match b {
979 b' ' => out.push('+'),
980 b'*' | b'-' | b'.' | b'_' => out.push(b as char),
981 _ if b.is_ascii_alphanumeric() => out.push(b as char),
982 _ => {
983 out.push('%');
984 out.push(hex_upper(b >> 4));
985 out.push(hex_upper(b & 0x0f));
986 }
987 }
988 }
989 out
990}
991
992fn hex_val(c: u8) -> Option<u8> {
993 match c {
994 b'0'..=b'9' => Some(c - b'0'),
995 b'a'..=b'f' => Some(c - b'a' + 10),
996 b'A'..=b'F' => Some(c - b'A' + 10),
997 _ => None,
998 }
999}
1000
1001fn hex_upper(n: u8) -> char {
1002 char::from_digit(n as u32, 16).unwrap().to_ascii_uppercase()
1003}