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 normalize_path(tail)
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
132fn normalize_path(path: &str) -> String {
137 if !path.contains('.') {
138 return path.to_string();
139 }
140 let rooted = path.starts_with('/');
141 let mut out: Vec<&str> = Vec::new();
142 let mut trailing_slash = false;
143 for seg in path.split('/') {
144 match seg {
145 "." => trailing_slash = true,
146 ".." => {
147 out.pop();
148 trailing_slash = true;
149 }
150 _ => {
151 out.push(seg);
152 trailing_slash = false;
153 }
154 }
155 }
156 if rooted && out.first() != Some(&"") {
159 out.insert(0, "");
160 }
161 let mut joined = out.join("/");
162 if trailing_slash && !joined.ends_with('/') {
163 joined.push('/');
164 }
165 if joined.is_empty() {
166 joined.push('/');
167 }
168 joined
169}
170
171pub fn construct(args: &[Value]) -> Result<Value, String> {
173 let input = arg_str(args, 0);
174 let parts = parse_absolute(&input)
175 .or_else(|| {
176 if args.len() > 1 {
178 let base = arg_str(args, 1);
179 parse_absolute(&base).map(|mut b| {
180 let mut rest = input.as_str();
183 let hash = match rest.find('#') {
184 Some(i) => {
185 let h = rest[i..].to_string();
186 rest = &rest[..i];
187 h
188 }
189 None => String::new(),
190 };
191 let search = match rest.find('?') {
192 Some(i) => {
193 let q = rest[i..].to_string();
194 rest = &rest[..i];
195 q
196 }
197 None => String::new(),
198 };
199 let merged = if rest.starts_with('/') {
202 rest.to_string()
203 } else if rest.is_empty() {
204 b.pathname.clone()
205 } else {
206 let dir = match b.pathname.rfind('/') {
207 Some(i) => &b.pathname[..=i],
208 None => "/",
209 };
210 format!("{dir}{rest}")
211 };
212 b.pathname = normalize_path(&merged);
213 b.search = search;
214 b.hash = hash;
215 b
216 })
217 } else {
218 None
219 }
220 })
221 .ok_or_else(|| {
226 crate::host::plain_coded_error("TypeError", "ERR_INVALID_URL", "Invalid URL")
227 })?;
228 Ok(build(&parts))
229}
230
231fn build(p: &Parts) -> Value {
232 let query = p.search.strip_prefix('?').unwrap_or(&p.search);
237 let search_params = make_search_params(&parse_query(query));
238 with_host(|h| {
239 let mut m = IndexMap::new();
240 m.insert("@@native".into(), h.new_str("URL"));
241 m.insert("href".into(), h.new_str(p.href()));
242 m.insert("origin".into(), h.new_str(p.origin()));
243 m.insert("protocol".into(), h.new_str(p.protocol.clone()));
244 m.insert("username".into(), h.new_str(p.username.clone()));
245 m.insert("password".into(), h.new_str(p.password.clone()));
246 m.insert("host".into(), h.new_str(p.host()));
247 m.insert("hostname".into(), h.new_str(p.hostname.clone()));
248 m.insert("port".into(), h.new_str(p.port.clone()));
249 m.insert("pathname".into(), h.new_str(p.pathname.clone()));
250 m.insert("search".into(), h.new_str(p.search.clone()));
251 m.insert("searchParams".into(), search_params);
252 m.insert("hash".into(), h.new_str(p.hash.clone()));
253 h.new_object(m)
254 })
255}
256
257pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
258 Some(match method {
259 "parse" => legacy_parse(args).map(|u| super::url_legacy::to_js(&u)),
260 "format" => super::url_legacy::format_value(&args.first().cloned().unwrap_or(Value::Undef)),
261 "fileURLToPath" => file_url_to_path(args).map(|s| with_host(|h| h.new_str(s))),
265 "fileURLToPathBuffer" => {
267 file_url_to_path(args).map(|s| super::buffer::from_bytes(s.as_bytes()))
268 }
269 "pathToFileURL" => Ok(path_to_file_url(&arg_str(args, 0))),
271 "domainToASCII" => Ok(punycode_domain(args, true)),
275 "domainToUnicode" => Ok(punycode_domain(args, false)),
276 "urlToHttpOptions" => Ok(url_to_http_options(
278 &args.first().cloned().unwrap_or(Value::Undef),
279 )),
280 "resolve" => {
282 let from = arg_str(args, 0);
283 let to = arg_str(args, 1);
284 Ok(with_host(|h| h.new_str(legacy_resolve(&from, &to))))
285 }
286 "resolveObject" => {
288 let from = arg_str(args, 0);
289 let to = arg_str(args, 1);
290 let resolved = legacy_resolve(&from, &to);
291 super::url_legacy::parse(&resolved, false, false).map(|u| super::url_legacy::to_js(&u))
292 }
293 _ => return None,
294 })
295}
296
297fn legacy_parse(args: &[Value]) -> Result<super::url_legacy::Url, String> {
301 super::process::emit_deprecation_warning(
302 "DEP0169",
303 "`url.parse()` behavior is not standardized and prone to errors that \
304 have security implications. Use the WHATWG URL API instead. CVEs are \
305 not issued for `url.parse()` vulnerabilities.",
306 );
307 let input = arg_str(args, 0);
308 let truthy = |i: usize| {
309 args.get(i)
310 .map(|v| with_host(|h| h.truthy(v)))
311 .unwrap_or(false)
312 };
313 super::url_legacy::parse(&input, truthy(1), truthy(2))
314}
315
316pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
318 match method {
319 "toString" | "toJSON" => Ok(with_host(|h| match h.get(recv) {
320 Some(JsObj::Object(p)) => p.get("href").cloned().unwrap_or(Value::Undef),
321 _ => Value::Undef,
322 })),
323 _ => Err(crate::host::type_error(&format!(
324 "url.{method} is not a function"
325 ))),
326 }
327}
328
329fn url_href(v: &Value) -> String {
334 with_host(|h| match h.get(v) {
335 Some(JsObj::Object(p)) => match p.get("@@native").map(|x| h.str_of(x)).as_deref() {
336 Some("URL") => p.get("href").map(|x| h.str_of(x)).unwrap_or_default(),
337 _ => h.str_of(v),
338 },
339 _ => h.str_of(v),
340 })
341}
342
343fn file_url_to_path(args: &[Value]) -> Result<String, String> {
345 let v = args.first().cloned().unwrap_or(Value::Undef);
346 let href = url_href(&v);
347 let rest = href.strip_prefix("file://").ok_or_else(|| {
348 crate::host::plain_coded_error(
349 "TypeError",
350 "ERR_INVALID_URL_SCHEME",
351 "The URL must be of scheme file",
352 )
353 })?;
354 let path = match rest.find('/') {
356 Some(0) => rest,
357 Some(i) => &rest[i..],
358 None => "/",
359 };
360 Ok(percent_decode(path))
361}
362
363fn path_to_file_url(path: &str) -> Value {
366 let enc = encode_path_component(path);
367 let pathname = if enc.starts_with('/') {
368 enc
369 } else {
370 format!("/{enc}")
371 };
372 let parts = Parts {
373 protocol: "file:".into(),
374 username: String::new(),
375 password: String::new(),
376 hostname: String::new(),
377 port: String::new(),
378 pathname,
379 search: String::new(),
380 hash: String::new(),
381 };
382 build(&parts)
383}
384
385fn punycode_domain(args: &[Value], ascii: bool) -> Value {
387 let method = if ascii { "toASCII" } else { "toUnicode" };
388 match super::punycode::call(method, args) {
389 Some(Ok(v)) => v,
390 _ => with_host(|h| h.new_str("")),
391 }
392}
393
394fn url_to_http_options(v: &Value) -> Value {
397 let get = |key: &str| -> String {
398 with_host(|h| match h.get(v) {
399 Some(JsObj::Object(p)) => p.get(key).map(|x| h.str_of(x)).unwrap_or_default(),
400 _ => String::new(),
401 })
402 };
403 let protocol = get("protocol");
404 let mut hostname = get("hostname");
405 if hostname.starts_with('[') && hostname.ends_with(']') && hostname.len() >= 2 {
406 hostname = hostname[1..hostname.len() - 1].to_string();
407 }
408 let hash = get("hash");
409 let search = get("search");
410 let pathname = get("pathname");
411 let href = get("href");
412 let port = get("port");
413 let username = get("username");
414 let password = get("password");
415 let path = format!("{pathname}{search}");
416 let auth = if username.is_empty() && password.is_empty() {
417 None
418 } else {
419 Some(format!(
420 "{}:{}",
421 percent_decode(&username),
422 percent_decode(&password)
423 ))
424 };
425 let port_num = if port.is_empty() {
426 None
427 } else {
428 port.parse::<f64>().ok()
429 };
430 with_host(|h| {
431 let mut m = IndexMap::new();
432 m.insert("protocol".into(), h.new_str(protocol));
433 m.insert("hostname".into(), h.new_str(hostname));
434 m.insert("hash".into(), h.new_str(hash));
435 m.insert("search".into(), h.new_str(search));
436 m.insert("pathname".into(), h.new_str(pathname));
437 m.insert("path".into(), h.new_str(path));
438 m.insert("href".into(), h.new_str(href));
439 if let Some(n) = port_num {
440 m.insert("port".into(), Value::Float(n));
441 }
442 if let Some(a) = auth {
443 m.insert("auth".into(), h.new_str(a));
444 }
445 h.new_object(m)
446 })
447}
448
449pub(crate) fn percent_decode(s: &str) -> String {
452 let b = s.as_bytes();
453 let mut out: Vec<u8> = Vec::with_capacity(b.len());
454 let mut i = 0;
455 while i < b.len() {
456 if b[i] == b'%' && i + 2 < b.len() {
457 if let (Some(hi), Some(lo)) = (hex_val(b[i + 1]), hex_val(b[i + 2])) {
458 out.push((hi << 4) | lo);
459 i += 3;
460 continue;
461 }
462 }
463 out.push(b[i]);
464 i += 1;
465 }
466 String::from_utf8_lossy(&out).into_owned()
467}
468
469fn encode_path_component(s: &str) -> String {
472 let mut out = String::with_capacity(s.len());
473 for &b in s.as_bytes() {
474 let keep = b.is_ascii_alphanumeric()
475 || matches!(
476 b,
477 b'/' | b'-'
478 | b'.'
479 | b'_'
480 | b'~'
481 | b'!'
482 | b'$'
483 | b'&'
484 | b'\''
485 | b'('
486 | b')'
487 | b'*'
488 | b'+'
489 | b','
490 | b';'
491 | b'='
492 | b':'
493 | b'@'
494 );
495 if keep {
496 out.push(b as char);
497 } else {
498 out.push('%');
499 out.push(hex_upper(b >> 4));
500 out.push(hex_upper(b & 0x0f));
501 }
502 }
503 out
504}
505
506struct UriRef {
510 scheme: Option<String>,
511 authority: Option<String>,
512 path: String,
513 query: Option<String>,
514 fragment: Option<String>,
515}
516
517fn split_uri(input: &str) -> UriRef {
519 let mut rest = input;
520 let mut scheme = None;
523 if let Some(colon) = rest.find(':') {
524 let cand = &rest[..colon];
525 let scheme_ok = !cand.is_empty()
526 && cand.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
527 && cand
528 .chars()
529 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
530 && cand.find(['/', '?', '#']).is_none();
531 if scheme_ok {
532 scheme = Some(cand.to_string());
533 rest = &rest[colon + 1..];
534 }
535 }
536 let mut fragment = None;
537 if let Some(h) = rest.find('#') {
538 fragment = Some(rest[h + 1..].to_string());
539 rest = &rest[..h];
540 }
541 let mut query = None;
542 if let Some(q) = rest.find('?') {
543 query = Some(rest[q + 1..].to_string());
544 rest = &rest[..q];
545 }
546 let mut authority = None;
547 if let Some(r) = rest.strip_prefix("//") {
548 let end = r.find('/').unwrap_or(r.len());
549 authority = Some(r[..end].to_string());
550 rest = &r[end..];
551 }
552 UriRef {
553 scheme,
554 authority,
555 path: rest.to_string(),
556 query,
557 fragment,
558 }
559}
560
561fn merge_paths(base: &UriRef, ref_path: &str) -> String {
563 if base.authority.is_some() && base.path.is_empty() {
564 format!("/{ref_path}")
565 } else {
566 match base.path.rfind('/') {
567 Some(i) => format!("{}{ref_path}", &base.path[..=i]),
568 None => ref_path.to_string(),
569 }
570 }
571}
572
573fn remove_last_segment(output: &mut String) {
575 match output.rfind('/') {
576 Some(pos) => output.truncate(pos),
577 None => output.clear(),
578 }
579}
580
581fn remove_dot_segments(path: &str) -> String {
583 let mut input = path.to_string();
584 let mut output = String::new();
585 while !input.is_empty() {
586 if let Some(r) = input.strip_prefix("../") {
587 input = r.to_string();
588 } else if let Some(r) = input.strip_prefix("./") {
589 input = r.to_string();
590 } else if let Some(r) = input.strip_prefix("/./") {
591 input = format!("/{r}");
592 } else if input == "/." {
593 input = "/".to_string();
594 } else if let Some(r) = input.strip_prefix("/../") {
595 input = format!("/{r}");
596 remove_last_segment(&mut output);
597 } else if input == "/.." {
598 input = "/".to_string();
599 remove_last_segment(&mut output);
600 } else if input == "." || input == ".." {
601 input.clear();
602 } else {
603 let start = usize::from(input.starts_with('/'));
604 let end = input[start..]
605 .find('/')
606 .map(|i| start + i)
607 .unwrap_or(input.len());
608 output.push_str(&input[..end]);
609 input.drain(..end);
610 }
611 }
612 output
613}
614
615fn resolve_ref(base: &UriRef, r: &UriRef) -> UriRef {
617 if r.scheme.is_some() {
618 return UriRef {
619 scheme: r.scheme.clone(),
620 authority: r.authority.clone(),
621 path: remove_dot_segments(&r.path),
622 query: r.query.clone(),
623 fragment: r.fragment.clone(),
624 };
625 }
626 let (authority, path, query) = if r.authority.is_some() {
627 (
628 r.authority.clone(),
629 remove_dot_segments(&r.path),
630 r.query.clone(),
631 )
632 } else if r.path.is_empty() {
633 let q = if r.query.is_some() {
634 r.query.clone()
635 } else {
636 base.query.clone()
637 };
638 (base.authority.clone(), base.path.clone(), q)
639 } else if r.path.starts_with('/') {
640 (
641 base.authority.clone(),
642 remove_dot_segments(&r.path),
643 r.query.clone(),
644 )
645 } else {
646 (
647 base.authority.clone(),
648 remove_dot_segments(&merge_paths(base, &r.path)),
649 r.query.clone(),
650 )
651 };
652 UriRef {
653 scheme: base.scheme.clone(),
654 authority,
655 path,
656 query,
657 fragment: r.fragment.clone(),
658 }
659}
660
661fn recompose(u: &UriRef) -> String {
663 let mut s = String::new();
664 if let Some(sc) = &u.scheme {
665 s.push_str(sc);
666 s.push(':');
667 }
668 if let Some(a) = &u.authority {
669 s.push_str("//");
670 s.push_str(a);
671 }
672 s.push_str(&u.path);
673 if let Some(q) = &u.query {
674 s.push('?');
675 s.push_str(q);
676 }
677 if let Some(f) = &u.fragment {
678 s.push('#');
679 s.push_str(f);
680 }
681 s
682}
683
684fn legacy_resolve(from: &str, to: &str) -> String {
686 recompose(&resolve_ref(&split_uri(from), &split_uri(to)))
687}
688
689pub const SEARCH_PARAMS_METHODS: &[&str] = &[
699 "get",
700 "getAll",
701 "has",
702 "set",
703 "append",
704 "delete",
705 "keys",
706 "values",
707 "entries",
708 "forEach",
709 "toString",
710 "sort",
711 "@@iterator",
712];
713
714fn make_search_params(pairs: &[(String, String)]) -> Value {
716 with_host(|h| {
717 let items: Vec<Value> = pairs
718 .iter()
719 .map(|(k, v)| {
720 let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
721 h.new_array(kv)
722 })
723 .collect();
724 let arr = h.new_array(items);
725 let mut m = IndexMap::new();
726 m.insert("@@native".into(), h.new_str("URLSearchParams"));
727 m.insert("@@pairs".into(), arr);
728 m.insert("size".into(), Value::Float(pairs.len() as f64));
732 let obj = h.new_object(m);
733 h.hide_prop(&obj, "size");
734 obj
735 })
736}
737
738fn pairs_of(recv: &Value) -> Vec<(String, String)> {
740 with_host(|h| {
741 let items: Vec<Value> = match h.get(recv) {
742 Some(JsObj::Object(p)) => match p.get("@@pairs").and_then(|a| h.get(a)) {
743 Some(JsObj::Array(items)) => items.clone(),
744 _ => Vec::new(),
745 },
746 _ => Vec::new(),
747 };
748 items
749 .iter()
750 .map(|it| match h.get(it) {
751 Some(JsObj::Array(kv)) => {
752 let kv = kv.clone();
753 let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
754 let v = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
755 (k, v)
756 }
757 _ => (h.str_of(it), String::new()),
758 })
759 .collect()
760 })
761}
762
763fn set_pairs(recv: &Value, pairs: &[(String, String)]) {
765 with_host(|h| {
766 let items: Vec<Value> = pairs
767 .iter()
768 .map(|(k, v)| {
769 let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
770 h.new_array(kv)
771 })
772 .collect();
773 let arr = h.new_array(items);
774 let n = Value::Float(pairs.len() as f64);
775 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
776 p.insert("@@pairs".into(), arr);
777 p.insert("size".into(), n);
778 }
779 h.hide_prop(recv, "size");
780 });
781}
782
783pub fn construct_search_params(args: &[Value]) -> Result<Value, String> {
786 let pairs = match args.first() {
787 None => Vec::new(),
788 Some(v) if matches!(v, Value::Undef) || with_host(|h| h.is_null(v)) => Vec::new(),
789 Some(v) => pairs_from_init(v),
790 };
791 Ok(make_search_params(&pairs))
792}
793
794fn pairs_from_init(v: &Value) -> Vec<(String, String)> {
795 if super::native_tag(v).as_deref() == Some("URLSearchParams") {
797 return pairs_of(v);
798 }
799 if let Some(s) = with_host(|h| h.as_str(v)) {
801 return parse_query(s.strip_prefix('?').unwrap_or(&s));
802 }
803 with_host(|h| match h.get(v) {
804 Some(JsObj::Array(items)) => {
806 let items = items.clone();
807 items
808 .iter()
809 .map(|it| match h.get(it) {
810 Some(JsObj::Array(kv)) => {
811 let kv = kv.clone();
812 let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
813 let val = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
814 (k, val)
815 }
816 _ => (h.str_of(it), String::new()),
817 })
818 .collect()
819 }
820 Some(JsObj::Object(p)) => {
822 let entries: Vec<(String, Value)> = p
823 .iter()
824 .filter(|(k, _)| !k.starts_with("@@"))
825 .map(|(k, val)| (k.clone(), val.clone()))
826 .collect();
827 entries
828 .into_iter()
829 .map(|(k, val)| (k, h.str_of(&val)))
830 .collect()
831 }
832 _ => Vec::new(),
833 })
834}
835
836pub fn search_params_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
838 match method {
839 "get" => {
840 let name = arg_str(args, 0);
841 match pairs_of(recv).into_iter().find(|(k, _)| *k == name) {
842 Some((_, v)) => Ok(with_host(|h| h.new_str(v))),
843 None => Ok(with_host(|h| h.null())),
844 }
845 }
846 "getAll" => {
847 let name = arg_str(args, 0);
848 let vals: Vec<String> = pairs_of(recv)
849 .into_iter()
850 .filter(|(k, _)| *k == name)
851 .map(|(_, v)| v)
852 .collect();
853 Ok(with_host(|h| {
854 let items = vals.into_iter().map(|v| h.new_str(v)).collect();
855 h.new_array(items)
856 }))
857 }
858 "has" => {
859 let name = arg_str(args, 0);
860 let pairs = pairs_of(recv);
861 let found = if args.len() > 1 {
862 let val = arg_str(args, 1);
863 pairs.iter().any(|(k, v)| *k == name && *v == val)
864 } else {
865 pairs.iter().any(|(k, _)| *k == name)
866 };
867 Ok(Value::Bool(found))
868 }
869 "append" => {
870 let mut pairs = pairs_of(recv);
871 pairs.push((arg_str(args, 0), arg_str(args, 1)));
872 set_pairs(recv, &pairs);
873 Ok(Value::Undef)
874 }
875 "set" => {
876 let name = arg_str(args, 0);
877 let val = arg_str(args, 1);
878 let mut pairs = pairs_of(recv);
879 let mut seen = false;
882 pairs.retain_mut(|(k, v)| {
883 if *k == name {
884 if seen {
885 false
886 } else {
887 *v = val.clone();
888 seen = true;
889 true
890 }
891 } else {
892 true
893 }
894 });
895 if !seen {
896 pairs.push((name, val));
897 }
898 set_pairs(recv, &pairs);
899 Ok(Value::Undef)
900 }
901 "delete" => {
902 let name = arg_str(args, 0);
903 let mut pairs = pairs_of(recv);
904 if args.len() > 1 {
905 let val = arg_str(args, 1);
906 pairs.retain(|(k, v)| !(*k == name && *v == val));
907 } else {
908 pairs.retain(|(k, _)| *k != name);
909 }
910 set_pairs(recv, &pairs);
911 Ok(Value::Undef)
912 }
913 "sort" => {
914 let mut pairs = pairs_of(recv);
915 pairs.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
917 set_pairs(recv, &pairs);
918 Ok(Value::Undef)
919 }
920 "toString" => {
921 let s = pairs_of(recv)
922 .iter()
923 .map(|(k, v)| format!("{}={}", form_encode(k), form_encode(v)))
924 .collect::<Vec<_>>()
925 .join("&");
926 Ok(with_host(|h| h.new_str(s)))
927 }
928 "keys" => {
929 let pairs = pairs_of(recv);
930 Ok(with_host(|h| {
931 let items = pairs.into_iter().map(|(k, _)| h.new_str(k)).collect();
932 h.alloc(JsObj::Iter { items, idx: 0 })
933 }))
934 }
935 "values" => {
936 let pairs = pairs_of(recv);
937 Ok(with_host(|h| {
938 let items = pairs.into_iter().map(|(_, v)| h.new_str(v)).collect();
939 h.alloc(JsObj::Iter { items, idx: 0 })
940 }))
941 }
942 "entries" | "@@iterator" => {
943 let pairs = pairs_of(recv);
944 Ok(with_host(|h| {
945 let items = pairs
946 .into_iter()
947 .map(|(k, v)| {
948 let kv = vec![h.new_str(k), h.new_str(v)];
949 h.new_array(kv)
950 })
951 .collect();
952 h.alloc(JsObj::Iter { items, idx: 0 })
953 }))
954 }
955 "forEach" => {
956 let cb = args.first().cloned().unwrap_or(Value::Undef);
957 let this_arg = args.get(1).cloned();
958 for (k, v) in pairs_of(recv) {
960 let (value, name) = with_host(|h| (h.new_str(v), h.new_str(k)));
961 crate::host::invoke(&cb, vec![value, name, recv.clone()], this_arg.clone())?;
962 }
963 Ok(Value::Undef)
964 }
965 _ => Err(crate::host::type_error(&format!(
966 "urlSearchParams.{method} is not a function"
967 ))),
968 }
969}
970
971fn parse_query(q: &str) -> Vec<(String, String)> {
973 q.split('&')
974 .filter(|s| !s.is_empty())
975 .map(|seg| match seg.split_once('=') {
976 Some((k, v)) => (form_decode(k), form_decode(v)),
977 None => (form_decode(seg), String::new()),
978 })
979 .collect()
980}
981
982fn form_decode(s: &str) -> String {
985 let b = s.as_bytes();
986 let mut out: Vec<u8> = Vec::with_capacity(b.len());
987 let mut i = 0;
988 while i < b.len() {
989 match b[i] {
990 b'+' => {
991 out.push(b' ');
992 i += 1;
993 }
994 b'%' if i + 2 < b.len() => match (hex_val(b[i + 1]), hex_val(b[i + 2])) {
995 (Some(hi), Some(lo)) => {
996 out.push((hi << 4) | lo);
997 i += 3;
998 }
999 _ => {
1000 out.push(b'%');
1001 i += 1;
1002 }
1003 },
1004 c => {
1005 out.push(c);
1006 i += 1;
1007 }
1008 }
1009 }
1010 String::from_utf8_lossy(&out).into_owned()
1011}
1012
1013fn form_encode(s: &str) -> String {
1016 let mut out = String::with_capacity(s.len());
1017 for &b in s.as_bytes() {
1018 match b {
1019 b' ' => out.push('+'),
1020 b'*' | b'-' | b'.' | b'_' => out.push(b as char),
1021 _ if b.is_ascii_alphanumeric() => out.push(b as char),
1022 _ => {
1023 out.push('%');
1024 out.push(hex_upper(b >> 4));
1025 out.push(hex_upper(b & 0x0f));
1026 }
1027 }
1028 }
1029 out
1030}
1031
1032fn hex_val(c: u8) -> Option<u8> {
1033 match c {
1034 b'0'..=b'9' => Some(c - b'0'),
1035 b'a'..=b'f' => Some(c - b'a' + 10),
1036 b'A'..=b'F' => Some(c - b'A' + 10),
1037 _ => None,
1038 }
1039}
1040
1041fn hex_upper(n: u8) -> char {
1042 char::from_digit(n as u32, 16).unwrap().to_ascii_uppercase()
1043}