1use super::arg_str;
14use crate::host::{with_host, JsObj};
15use fusevm::Value;
16use indexmap::IndexMap;
17
18pub const MODULE_METHODS: &[&str] = &[
19 "parse",
20 "format",
21 "fileURLToPath",
22 "fileURLToPathBuffer",
23 "pathToFileURL",
24 "domainToASCII",
25 "domainToUnicode",
26 "urlToHttpOptions",
27 "resolve",
28 "resolveObject",
29];
30
31pub const COMPONENTS: &[&str] = &[
44 "protocol", "username", "password", "host", "hostname", "port", "pathname", "search", "hash",
45 "href",
46];
47
48pub fn is_component(name: &str) -> bool {
51 COMPONENTS.contains(&name)
52}
53
54fn recompute(url: &Value, sync_params: bool) {
61 let read = |k: &str| {
62 with_host(|h| match h.get(url) {
63 Some(JsObj::Object(p)) => p.get(k).map(|v| h.str_of(v)).unwrap_or_default(),
64 _ => String::new(),
65 })
66 };
67 let mut protocol = read("@@protocol");
68 if !protocol.is_empty() && !protocol.ends_with(':') {
69 protocol.push(':');
70 }
71 let delimited = |s: String, lead: char| {
74 if s.is_empty() || s.starts_with(lead) {
75 s
76 } else {
77 format!("{lead}{s}")
78 }
79 };
80 let parts = Parts {
81 protocol,
82 username: read("@@username"),
83 password: read("@@password"),
84 hostname: read("@@hostname"),
85 port: read("@@port"),
86 pathname: read("@@pathname"),
87 search: delimited(read("@@search"), '?'),
88 hash: delimited(read("@@hash"), '#'),
89 };
90 let (href, host, origin) = (parts.href(), parts.host(), parts.origin());
91 let search = parts.search.clone();
92 if sync_params {
93 let query = search.strip_prefix('?').unwrap_or(&search).to_string();
97 let params = with_host(|h| match h.get(url) {
98 Some(JsObj::Object(p)) => p.get("@@searchParams").cloned(),
99 _ => None,
100 });
101 if let Some(params) = params {
102 write_pairs(¶ms, &parse_query(&query));
103 }
104 }
105 with_host(|h| {
106 let vals = [
107 ("@@href", h.new_str(href)),
108 ("@@host", h.new_str(host)),
109 ("@@origin", h.new_str(origin)),
110 ("@@protocol", h.new_str(parts.protocol.clone())),
111 ("@@search", h.new_str(search)),
112 ("@@hash", h.new_str(parts.hash.clone())),
113 ];
114 if let Some(JsObj::Object(p)) = h.get_mut(url) {
115 for (k, v) in vals {
116 p.insert(k.to_string(), v);
117 }
118 }
119 });
120}
121
122pub fn refresh(url: &Value) {
124 recompute(url, true);
125}
126
127pub fn split_host(url: &Value) {
134 let host = with_host(|h| match h.get(url) {
135 Some(JsObj::Object(p)) => p.get("@@host").map(|v| h.str_of(v)).unwrap_or_default(),
136 _ => String::new(),
137 });
138 let split = match host.rfind(']') {
141 Some(i) => host[i..].find(':').map(|j| i + j),
142 None => host.rfind(':'),
143 };
144 let (hostname, port) = match split {
145 Some(i) => (host[..i].to_string(), host[i + 1..].to_string()),
146 None => (host.clone(), String::new()),
147 };
148 with_host(|h| {
149 let (hn, pt) = (h.new_str(hostname), h.new_str(port));
150 if let Some(JsObj::Object(p)) = h.get_mut(url) {
151 p.insert("@@hostname".into(), hn);
152 p.insert("@@port".into(), pt);
153 }
154 });
155 refresh(url);
156}
157
158pub fn reparse(url: &Value) {
167 let href = with_host(|h| match h.get(url) {
168 Some(JsObj::Object(p)) => p.get("@@href").map(|v| h.str_of(v)).unwrap_or_default(),
169 _ => String::new(),
170 });
171 let Some(parts) = parse_absolute(&href) else {
172 return;
173 };
174 let fresh = build(&parts);
175 let props = with_host(|h| match h.get(&fresh) {
176 Some(JsObj::Object(p)) => p.clone(),
177 _ => IndexMap::new(),
178 });
179 with_host(|h| {
180 if let Some(JsObj::Object(p)) = h.get_mut(url) {
181 for (k, v) in props {
182 p.insert(k, v);
183 }
184 }
185 });
186}
187
188struct Parts {
189 protocol: String,
190 username: String,
191 password: String,
192 hostname: String,
193 port: String,
194 pathname: String,
195 search: String,
196 hash: String,
197}
198
199impl Parts {
200 fn host(&self) -> String {
201 if self.port.is_empty() {
202 self.hostname.clone()
203 } else {
204 format!("{}:{}", self.hostname, self.port)
205 }
206 }
207 fn origin(&self) -> String {
208 let scheme = self.protocol.strip_suffix(':').unwrap_or(&self.protocol);
211 if self.hostname.is_empty() || special_port(scheme).is_none() {
212 "null".into()
213 } else {
214 format!("{}//{}", self.protocol, self.host())
215 }
216 }
217 fn href(&self) -> String {
218 let auth = if self.username.is_empty() {
219 String::new()
220 } else if self.password.is_empty() {
221 format!("{}@", self.username)
222 } else {
223 format!("{}:{}@", self.username, self.password)
224 };
225 format!(
226 "{}//{auth}{}{}{}{}",
227 self.protocol,
228 self.host(),
229 self.pathname,
230 self.search,
231 self.hash
232 )
233 }
234}
235
236fn special_port(scheme: &str) -> Option<&'static str> {
239 match scheme {
240 "http" | "ws" => Some("80"),
241 "https" | "wss" => Some("443"),
242 "ftp" => Some("21"),
243 _ => None,
244 }
245}
246
247fn parse_absolute(input: &str) -> Option<Parts> {
249 let stripped: String;
253 let input = if input.contains(['\t', '\n', '\r']) {
254 stripped = input.replace(['\t', '\n', '\r'], "");
255 stripped.as_str()
256 } else {
257 input
258 };
259 let (scheme, rest) = input.split_once("://")?;
260 let backslashed: String;
265 let rest = if special_port(&scheme.to_ascii_lowercase()).is_some() && rest.contains('\\') {
266 let cut = rest.find(['?', '#']).unwrap_or(rest.len());
267 backslashed = format!("{}{}", rest[..cut].replace('\\', "/"), &rest[cut..]);
268 backslashed.as_str()
269 } else {
270 rest
271 };
272 if scheme.is_empty()
273 || !scheme
274 .chars()
275 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
276 {
277 return None;
278 }
279 let rest = if special_port(&scheme.to_ascii_lowercase()).is_some() {
282 rest.trim_start_matches('/')
283 } else {
284 rest
285 };
286 let auth_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
288 let authority = &rest[..auth_end];
289 let mut tail = &rest[auth_end..];
290
291 let (userinfo, hostport) = match authority.rsplit_once('@') {
292 Some((u, h)) => (u, h),
293 None => ("", authority),
294 };
295 let (username, password) = match userinfo.split_once(':') {
296 Some((u, p)) => (u.to_string(), p.to_string()),
297 None => (userinfo.to_string(), String::new()),
298 };
299 let (hostname, port) = if hostport.starts_with('[') {
302 let close = hostport.find(']')?;
303 match &hostport[close + 1..] {
304 "" => (&hostport[..=close], ""),
305 p => (&hostport[..=close], p.strip_prefix(':')?),
306 }
307 } else {
308 hostport.split_once(':').unwrap_or((hostport, ""))
309 };
310 let lower_scheme = scheme.to_ascii_lowercase();
311 let special = special_port(&lower_scheme).is_some();
312 let hostname = if special {
318 if hostname.is_empty() {
319 return None;
320 }
321 url::Host::parse(hostname).ok()?.to_string()
322 } else if hostname.is_empty() {
323 String::new()
324 } else {
325 url::Host::parse_opaque(hostname).ok()?.to_string()
326 };
327 let port = if port.is_empty() {
330 String::new()
331 } else if port.bytes().all(|b| b.is_ascii_digit()) {
332 port.trim_start_matches('0').parse::<u16>().map_or_else(
333 |_| if port.bytes().all(|b| b == b'0') { Some("0".to_string()) } else { None },
334 |n| Some(n.to_string()),
335 )?
336 } else {
337 return None;
338 };
339
340 let hash = match tail.find('#') {
341 Some(i) => {
342 let h = tail[i..].to_string();
343 tail = &tail[..i];
344 h
345 }
346 None => String::new(),
347 };
348 let search = match tail.find('?') {
349 Some(i) => {
350 let s = tail[i..].to_string();
351 tail = &tail[..i];
352 s
353 }
354 None => String::new(),
355 };
356 let scheme = scheme.to_ascii_lowercase();
358 let default_port = special_port(&scheme);
359 let pathname = if tail.is_empty() {
360 "/".to_string()
361 } else {
362 normalize_path(tail)
363 };
364 let port = if default_port == Some(port.as_str()) {
366 String::new()
367 } else {
368 port
369 };
370
371 Some(Parts {
372 protocol: format!("{scheme}:"),
373 username,
374 password,
375 hostname,
376 port,
377 pathname,
378 search,
379 hash,
380 })
381}
382
383fn normalize_path(path: &str) -> String {
388 if !path.contains('.') {
389 return path.to_string();
390 }
391 let rooted = path.starts_with('/');
392 let mut out: Vec<&str> = Vec::new();
393 let mut trailing_slash = false;
394 for seg in path.split('/') {
395 match seg {
396 "." => trailing_slash = true,
397 ".." => {
398 out.pop();
399 trailing_slash = true;
400 }
401 _ => {
402 out.push(seg);
403 trailing_slash = false;
404 }
405 }
406 }
407 if rooted && out.first() != Some(&"") {
410 out.insert(0, "");
411 }
412 let mut joined = out.join("/");
413 if trailing_slash && !joined.ends_with('/') {
414 joined.push('/');
415 }
416 if joined.is_empty() {
417 joined.push('/');
418 }
419 joined
420}
421
422pub fn construct(args: &[Value]) -> Result<Value, String> {
424 let to_str = |v: &Value| {
427 crate::host::to_string_value(v).map(|s| crate::host::with_host(|h| h.str_of(&s)))
428 };
429 let input = match args.first() {
430 Some(v) => to_str(v)?,
431 None => "undefined".to_string(),
432 };
433 let base = match args.get(1) {
435 Some(Value::Undef) | None => None,
436 Some(v) => Some(to_str(v)?),
437 };
438 let parts = parse_absolute(&input)
439 .or_else(|| {
440 if let Some(base) = &base {
442 parse_absolute(base).map(|mut b| {
443 let mut rest = input.as_str();
446 let hash = match rest.find('#') {
447 Some(i) => {
448 let h = rest[i..].to_string();
449 rest = &rest[..i];
450 h
451 }
452 None => String::new(),
453 };
454 let search = match rest.find('?') {
455 Some(i) => {
456 let q = rest[i..].to_string();
457 rest = &rest[..i];
458 q
459 }
460 None => String::new(),
461 };
462 let merged = if rest.starts_with('/') {
465 rest.to_string()
466 } else if rest.is_empty() {
467 b.pathname.clone()
468 } else {
469 let dir = match b.pathname.rfind('/') {
470 Some(i) => &b.pathname[..=i],
471 None => "/",
472 };
473 format!("{dir}{rest}")
474 };
475 b.pathname = normalize_path(&merged);
476 b.search = search;
477 b.hash = hash;
478 b
479 })
480 } else {
481 None
482 }
483 })
484 .ok_or_else(|| {
491 let mut fields = vec![("input", input.as_str())];
492 if let Some(b) = &base {
493 fields.push(("base", b.as_str()));
494 }
495 crate::host::plain_coded_error_with("TypeError", "ERR_INVALID_URL", "Invalid URL", &fields)
496 })?;
497 Ok(build(&parts))
498}
499
500fn percent_encode(s: &str, extra: &str) -> String {
512 let bytes = s.as_bytes();
513 let mut out = String::with_capacity(s.len());
514 let mut i = 0;
515 while i < bytes.len() {
516 let b = bytes[i];
517 if b == b'%' && i + 2 < bytes.len() + 1 {
519 let hex = bytes.get(i + 1..i + 3);
520 if hex.is_some_and(|h| h.iter().all(|c| c.is_ascii_hexdigit())) {
521 out.push('%');
522 out.push(bytes[i + 1] as char);
523 out.push(bytes[i + 2] as char);
524 i += 3;
525 continue;
526 }
527 }
528 if b < 0x20 || b == 0x7f || b >= 0x80 || extra.as_bytes().contains(&b) {
529 out.push_str(&format!("%{b:02X}"));
530 } else {
531 out.push(b as char);
532 }
533 i += 1;
534 }
535 out
536}
537
538const PATH_SET: &str = " \"<>^`{}";
540const QUERY_SET: &str = " \"'<>";
541const FRAGMENT_SET: &str = " \"<>`";
542const USERINFO_SET: &str = " \";<=>@[]^`{|}";
543
544fn build(p: &Parts) -> Value {
545 let p = &Parts {
551 protocol: p.protocol.clone(),
552 username: percent_encode(&p.username, USERINFO_SET),
553 password: percent_encode(&p.password, USERINFO_SET),
554 hostname: p.hostname.clone(),
555 port: p.port.clone(),
556 pathname: percent_encode(&p.pathname, PATH_SET),
557 search: percent_encode(&p.search, QUERY_SET),
558 hash: percent_encode(&p.hash, FRAGMENT_SET),
559 };
560 let query = p.search.strip_prefix('?').unwrap_or(&p.search);
566 let search_params = make_search_params(&parse_query(query));
567 with_host(|h| {
568 let mut m = IndexMap::new();
569 m.insert("@@native".into(), h.new_str("URL"));
570 m.insert("@@href".into(), h.new_str(p.href()));
571 m.insert("@@origin".into(), h.new_str(p.origin()));
572 m.insert("@@protocol".into(), h.new_str(p.protocol.clone()));
573 m.insert("@@username".into(), h.new_str(p.username.clone()));
574 m.insert("@@password".into(), h.new_str(p.password.clone()));
575 m.insert("@@host".into(), h.new_str(p.host()));
576 m.insert("@@hostname".into(), h.new_str(p.hostname.clone()));
577 m.insert("@@port".into(), h.new_str(p.port.clone()));
578 m.insert("@@pathname".into(), h.new_str(p.pathname.clone()));
579 m.insert("@@search".into(), h.new_str(p.search.clone()));
580 m.insert("@@searchParams".into(), search_params.clone());
581 m.insert("@@hash".into(), h.new_str(p.hash.clone()));
582 let obj = h.new_object(m);
583 if let Some(JsObj::Object(sp)) = h.get_mut(&search_params) {
585 sp.insert("@@ownerUrl".into(), obj.clone());
586 }
587 obj
588 })
589}
590
591pub const STATIC_METHODS: &[&str] = &["canParse", "parse"];
597
598pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
604 let parsed = construct(args);
605 Some(match method {
606 "canParse" => Ok(Value::Bool(parsed.is_ok())),
607 "parse" => Ok(parsed.unwrap_or_else(|_| with_host(|h| h.null()))),
608 _ => return None,
609 })
610}
611
612pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
613 Some(match method {
614 "parse" => legacy_parse(args).map(|u| super::url_legacy::to_js(&u)),
615 "format" => super::url_legacy::format_value(&args.first().cloned().unwrap_or(Value::Undef)),
616 "fileURLToPath" => file_url_to_path(args).map(|s| with_host(|h| h.new_str(s))),
620 "fileURLToPathBuffer" => {
622 file_url_to_path(args).map(|s| super::buffer::from_bytes(s.as_bytes()))
623 }
624 "pathToFileURL" => Ok(path_to_file_url(&arg_str(args, 0))),
626 "domainToASCII" => Ok(punycode_domain(args, true)),
630 "domainToUnicode" => Ok(punycode_domain(args, false)),
631 "urlToHttpOptions" => Ok(url_to_http_options(
633 &args.first().cloned().unwrap_or(Value::Undef),
634 )),
635 "resolve" => legacy_resolve_object(args)
639 .map(|u| with_host(|h| h.new_str(u.href.unwrap_or_default()))),
640 "resolveObject" => {
643 if !args.first().is_some_and(|v| with_host(|h| h.truthy(v))) {
644 return Some(Ok(args.get(1).cloned().unwrap_or(Value::Undef)));
645 }
646 legacy_resolve_object(args).map(|u| super::url_legacy::to_js(&u))
647 }
648 _ => return None,
649 })
650}
651
652fn legacy_parse(args: &[Value]) -> Result<super::url_legacy::Url, String> {
656 emit_url_parse_deprecation();
657 let input = arg_str(args, 0);
658 let truthy = |i: usize| {
659 args.get(i)
660 .map(|v| with_host(|h| h.truthy(v)))
661 .unwrap_or(false)
662 };
663 super::url_legacy::parse(&input, truthy(1), truthy(2))
664}
665
666fn emit_url_parse_deprecation() {
669 super::process::emit_deprecation_warning(
670 "DEP0169",
671 "`url.parse()` behavior is not standardized and prone to errors that \
672 have security implications. Use the WHATWG URL API instead. CVEs are \
673 not issued for `url.parse()` vulnerabilities.",
674 );
675}
676
677fn legacy_resolve_object(args: &[Value]) -> Result<super::url_legacy::Url, String> {
680 emit_url_parse_deprecation();
681 let source = super::url_legacy::parse(&arg_str(args, 0), false, true)?;
682 let relative = super::url_legacy::parse(&arg_str(args, 1), false, true)?;
683 Ok(super::url_legacy::resolve_object(&source, relative))
684}
685
686pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
688 match method {
689 "toString" | "toJSON" => Ok(with_host(|h| match h.get(recv) {
690 Some(JsObj::Object(p)) => p.get("@@href").cloned().unwrap_or(Value::Undef),
691 _ => Value::Undef,
692 })),
693 _ => Err(crate::host::type_error(&format!(
694 "url.{method} is not a function"
695 ))),
696 }
697}
698
699fn url_href(v: &Value) -> String {
704 with_host(|h| match h.get(v) {
705 Some(JsObj::Object(p)) => match p.get("@@native").map(|x| h.str_of(x)).as_deref() {
706 Some("URL") => p.get("@@href").map(|x| h.str_of(x)).unwrap_or_default(),
707 _ => h.str_of(v),
708 },
709 _ => h.str_of(v),
710 })
711}
712
713fn file_url_to_path(args: &[Value]) -> Result<String, String> {
715 let v = args.first().cloned().unwrap_or(Value::Undef);
716 let href = url_href(&v);
717 let rest = href.strip_prefix("file://").ok_or_else(|| {
718 crate::host::plain_coded_error(
719 "TypeError",
720 "ERR_INVALID_URL_SCHEME",
721 "The URL must be of scheme file",
722 )
723 })?;
724 let path = match rest.find('/') {
726 Some(0) => rest,
727 Some(i) => &rest[i..],
728 None => "/",
729 };
730 Ok(percent_decode(path))
731}
732
733fn path_to_file_url(path: &str) -> Value {
736 let enc = encode_path_component(path);
737 let pathname = if enc.starts_with('/') {
738 enc
739 } else {
740 format!("/{enc}")
741 };
742 let parts = Parts {
743 protocol: "file:".into(),
744 username: String::new(),
745 password: String::new(),
746 hostname: String::new(),
747 port: String::new(),
748 pathname,
749 search: String::new(),
750 hash: String::new(),
751 };
752 build(&parts)
753}
754
755fn punycode_domain(args: &[Value], ascii: bool) -> Value {
757 let method = if ascii { "toASCII" } else { "toUnicode" };
758 match super::punycode::call(method, args) {
759 Some(Ok(v)) => v,
760 _ => with_host(|h| h.new_str("")),
761 }
762}
763
764fn url_to_http_options(v: &Value) -> Value {
767 let get = |key: &str| -> String {
768 with_host(|h| match h.get(v) {
769 Some(JsObj::Object(p)) => p.get(key).map(|x| h.str_of(x)).unwrap_or_default(),
770 _ => String::new(),
771 })
772 };
773 let protocol = get("@@protocol");
774 let mut hostname = get("@@hostname");
775 if hostname.starts_with('[') && hostname.ends_with(']') && hostname.len() >= 2 {
776 hostname = hostname[1..hostname.len() - 1].to_string();
777 }
778 let hash = get("@@hash");
779 let search = get("@@search");
780 let pathname = get("@@pathname");
781 let href = get("@@href");
782 let port = get("@@port");
783 let username = get("@@username");
784 let password = get("@@password");
785 let path = format!("{pathname}{search}");
786 let auth = if username.is_empty() && password.is_empty() {
787 None
788 } else {
789 Some(format!(
790 "{}:{}",
791 percent_decode(&username),
792 percent_decode(&password)
793 ))
794 };
795 let port_num = if port.is_empty() {
796 None
797 } else {
798 port.parse::<f64>().ok()
799 };
800 with_host(|h| {
801 let mut m = IndexMap::new();
802 m.insert("protocol".into(), h.new_str(protocol));
803 m.insert("hostname".into(), h.new_str(hostname));
804 m.insert("hash".into(), h.new_str(hash));
805 m.insert("search".into(), h.new_str(search));
806 m.insert("pathname".into(), h.new_str(pathname));
807 m.insert("path".into(), h.new_str(path));
808 m.insert("href".into(), h.new_str(href));
809 if let Some(n) = port_num {
810 m.insert("port".into(), Value::Float(n));
811 }
812 if let Some(a) = auth {
813 m.insert("auth".into(), h.new_str(a));
814 }
815 h.new_object(m)
816 })
817}
818
819pub(crate) fn percent_decode(s: &str) -> String {
822 let b = s.as_bytes();
823 let mut out: Vec<u8> = Vec::with_capacity(b.len());
824 let mut i = 0;
825 while i < b.len() {
826 if b[i] == b'%' && i + 2 < b.len() {
827 if let (Some(hi), Some(lo)) = (hex_val(b[i + 1]), hex_val(b[i + 2])) {
828 out.push((hi << 4) | lo);
829 i += 3;
830 continue;
831 }
832 }
833 out.push(b[i]);
834 i += 1;
835 }
836 String::from_utf8_lossy(&out).into_owned()
837}
838
839fn encode_path_component(s: &str) -> String {
842 let mut out = String::with_capacity(s.len());
843 for &b in s.as_bytes() {
844 let keep = b.is_ascii_alphanumeric()
845 || matches!(
846 b,
847 b'/' | b'-'
848 | b'.'
849 | b'_'
850 | b'~'
851 | b'!'
852 | b'$'
853 | b'&'
854 | b'\''
855 | b'('
856 | b')'
857 | b'*'
858 | b'+'
859 | b','
860 | b';'
861 | b'='
862 | b':'
863 | b'@'
864 );
865 if keep {
866 out.push(b as char);
867 } else {
868 out.push('%');
869 out.push(hex_upper(b >> 4));
870 out.push(hex_upper(b & 0x0f));
871 }
872 }
873 out
874}
875
876pub const SEARCH_PARAMS_METHODS: &[&str] = &[
886 "get",
887 "getAll",
888 "has",
889 "set",
890 "append",
891 "delete",
892 "keys",
893 "values",
894 "entries",
895 "forEach",
896 "toString",
897 "sort",
898 "@@iterator",
899];
900
901fn make_search_params(pairs: &[(String, String)]) -> Value {
903 with_host(|h| {
904 let items: Vec<Value> = pairs
905 .iter()
906 .map(|(k, v)| {
907 let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
908 h.new_array(kv)
909 })
910 .collect();
911 let arr = h.new_array(items);
912 let mut m = IndexMap::new();
913 m.insert("@@native".into(), h.new_str("URLSearchParams"));
914 m.insert("@@pairs".into(), arr);
915 m.insert("size".into(), Value::Float(pairs.len() as f64));
919 let obj = h.new_object(m);
920 h.hide_prop(&obj, "size");
921 obj
922 })
923}
924
925fn encode_query(pairs: &[(String, String)]) -> String {
928 pairs
929 .iter()
930 .map(|(k, v)| format!("{}={}", form_encode(k), form_encode(v)))
931 .collect::<Vec<_>>()
932 .join("&")
933}
934
935fn pairs_of(recv: &Value) -> Vec<(String, String)> {
937 with_host(|h| {
938 let items: Vec<Value> = match h.get(recv) {
939 Some(JsObj::Object(p)) => match p.get("@@pairs").and_then(|a| h.get(a)) {
940 Some(JsObj::Array(items)) => items.clone(),
941 _ => Vec::new(),
942 },
943 _ => Vec::new(),
944 };
945 items
946 .iter()
947 .map(|it| match h.get(it) {
948 Some(JsObj::Array(kv)) => {
949 let kv = kv.clone();
950 let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
951 let v = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
952 (k, v)
953 }
954 _ => (h.str_of(it), String::new()),
955 })
956 .collect()
957 })
958}
959
960fn set_pairs(recv: &Value, pairs: &[(String, String)]) {
967 write_pairs(recv, pairs);
968 let owner = with_host(|h| match h.get(recv) {
969 Some(JsObj::Object(p)) => p.get("@@ownerUrl").cloned(),
970 _ => None,
971 });
972 if let Some(owner) = owner {
973 let query = encode_query(pairs);
974 with_host(|h| {
975 let s = h.new_str(if query.is_empty() {
976 String::new()
977 } else {
978 format!("?{query}")
979 });
980 if let Some(JsObj::Object(p)) = h.get_mut(&owner) {
981 p.insert("@@search".into(), s);
982 }
983 });
984 recompute(&owner, false);
985 }
986}
987
988fn write_pairs(recv: &Value, pairs: &[(String, String)]) {
990 with_host(|h| {
991 let items: Vec<Value> = pairs
992 .iter()
993 .map(|(k, v)| {
994 let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
995 h.new_array(kv)
996 })
997 .collect();
998 let arr = h.new_array(items);
999 let n = Value::Float(pairs.len() as f64);
1000 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1001 p.insert("@@pairs".into(), arr);
1002 p.insert("size".into(), n);
1003 }
1004 h.hide_prop(recv, "size");
1005 });
1006}
1007
1008pub fn construct_search_params(args: &[Value]) -> Result<Value, String> {
1011 let pairs = match args.first() {
1012 None => Vec::new(),
1013 Some(v) if matches!(v, Value::Undef) || with_host(|h| h.is_null(v)) => Vec::new(),
1014 Some(v) => pairs_from_init(v),
1015 };
1016 Ok(make_search_params(&pairs))
1017}
1018
1019fn pairs_from_init(v: &Value) -> Vec<(String, String)> {
1020 if super::native_tag(v).as_deref() == Some("URLSearchParams") {
1022 return pairs_of(v);
1023 }
1024 if let Some(s) = with_host(|h| h.as_str(v)) {
1026 return parse_query(s.strip_prefix('?').unwrap_or(&s));
1027 }
1028 with_host(|h| match h.get(v) {
1029 Some(JsObj::Array(items)) => {
1031 let items = items.clone();
1032 items
1033 .iter()
1034 .map(|it| match h.get(it) {
1035 Some(JsObj::Array(kv)) => {
1036 let kv = kv.clone();
1037 let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
1038 let val = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
1039 (k, val)
1040 }
1041 _ => (h.str_of(it), String::new()),
1042 })
1043 .collect()
1044 }
1045 Some(JsObj::Object(p)) => {
1047 let entries: Vec<(String, Value)> = p
1048 .iter()
1049 .filter(|(k, _)| !k.starts_with("@@"))
1050 .map(|(k, val)| (k.clone(), val.clone()))
1051 .collect();
1052 entries
1053 .into_iter()
1054 .map(|(k, val)| (k, h.str_of(&val)))
1055 .collect()
1056 }
1057 _ => Vec::new(),
1058 })
1059}
1060
1061pub fn search_params_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
1063 match method {
1064 "get" => {
1065 let name = arg_str(args, 0);
1066 match pairs_of(recv).into_iter().find(|(k, _)| *k == name) {
1067 Some((_, v)) => Ok(with_host(|h| h.new_str(v))),
1068 None => Ok(with_host(|h| h.null())),
1069 }
1070 }
1071 "getAll" => {
1072 let name = arg_str(args, 0);
1073 let vals: Vec<String> = pairs_of(recv)
1074 .into_iter()
1075 .filter(|(k, _)| *k == name)
1076 .map(|(_, v)| v)
1077 .collect();
1078 Ok(with_host(|h| {
1079 let items = vals.into_iter().map(|v| h.new_str(v)).collect();
1080 h.new_array(items)
1081 }))
1082 }
1083 "has" => {
1084 let name = arg_str(args, 0);
1085 let pairs = pairs_of(recv);
1086 let found = if args.len() > 1 {
1087 let val = arg_str(args, 1);
1088 pairs.iter().any(|(k, v)| *k == name && *v == val)
1089 } else {
1090 pairs.iter().any(|(k, _)| *k == name)
1091 };
1092 Ok(Value::Bool(found))
1093 }
1094 "append" => {
1095 let mut pairs = pairs_of(recv);
1096 pairs.push((arg_str(args, 0), arg_str(args, 1)));
1097 set_pairs(recv, &pairs);
1098 Ok(Value::Undef)
1099 }
1100 "set" => {
1101 let name = arg_str(args, 0);
1102 let val = arg_str(args, 1);
1103 let mut pairs = pairs_of(recv);
1104 let mut seen = false;
1107 pairs.retain_mut(|(k, v)| {
1108 if *k == name {
1109 if seen {
1110 false
1111 } else {
1112 *v = val.clone();
1113 seen = true;
1114 true
1115 }
1116 } else {
1117 true
1118 }
1119 });
1120 if !seen {
1121 pairs.push((name, val));
1122 }
1123 set_pairs(recv, &pairs);
1124 Ok(Value::Undef)
1125 }
1126 "delete" => {
1127 let name = arg_str(args, 0);
1128 let mut pairs = pairs_of(recv);
1129 if args.len() > 1 {
1130 let val = arg_str(args, 1);
1131 pairs.retain(|(k, v)| !(*k == name && *v == val));
1132 } else {
1133 pairs.retain(|(k, _)| *k != name);
1134 }
1135 set_pairs(recv, &pairs);
1136 Ok(Value::Undef)
1137 }
1138 "sort" => {
1139 let mut pairs = pairs_of(recv);
1140 pairs.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
1142 set_pairs(recv, &pairs);
1143 Ok(Value::Undef)
1144 }
1145 "toString" => {
1146 let s = encode_query(&pairs_of(recv));
1147 Ok(with_host(|h| h.new_str(s)))
1148 }
1149 "keys" => {
1150 let pairs = pairs_of(recv);
1151 Ok(with_host(|h| {
1152 let items = pairs.into_iter().map(|(k, _)| h.new_str(k)).collect();
1153 h.alloc(JsObj::Iter { items, idx: 0 })
1154 }))
1155 }
1156 "values" => {
1157 let pairs = pairs_of(recv);
1158 Ok(with_host(|h| {
1159 let items = pairs.into_iter().map(|(_, v)| h.new_str(v)).collect();
1160 h.alloc(JsObj::Iter { items, idx: 0 })
1161 }))
1162 }
1163 "entries" | "@@iterator" => {
1164 let pairs = pairs_of(recv);
1165 Ok(with_host(|h| {
1166 let items = pairs
1167 .into_iter()
1168 .map(|(k, v)| {
1169 let kv = vec![h.new_str(k), h.new_str(v)];
1170 h.new_array(kv)
1171 })
1172 .collect();
1173 h.alloc(JsObj::Iter { items, idx: 0 })
1174 }))
1175 }
1176 "forEach" => {
1177 let cb = args.first().cloned().unwrap_or(Value::Undef);
1178 let this_arg = args.get(1).cloned();
1179 for (k, v) in pairs_of(recv) {
1181 let (value, name) = with_host(|h| (h.new_str(v), h.new_str(k)));
1182 crate::host::invoke(&cb, vec![value, name, recv.clone()], this_arg.clone())?;
1183 }
1184 Ok(Value::Undef)
1185 }
1186 _ => Err(crate::host::type_error(&format!(
1187 "urlSearchParams.{method} is not a function"
1188 ))),
1189 }
1190}
1191
1192fn parse_query(q: &str) -> Vec<(String, String)> {
1194 q.split('&')
1195 .filter(|s| !s.is_empty())
1196 .map(|seg| match seg.split_once('=') {
1197 Some((k, v)) => (form_decode(k), form_decode(v)),
1198 None => (form_decode(seg), String::new()),
1199 })
1200 .collect()
1201}
1202
1203fn form_decode(s: &str) -> String {
1206 let b = s.as_bytes();
1207 let mut out: Vec<u8> = Vec::with_capacity(b.len());
1208 let mut i = 0;
1209 while i < b.len() {
1210 match b[i] {
1211 b'+' => {
1212 out.push(b' ');
1213 i += 1;
1214 }
1215 b'%' if i + 2 < b.len() => match (hex_val(b[i + 1]), hex_val(b[i + 2])) {
1216 (Some(hi), Some(lo)) => {
1217 out.push((hi << 4) | lo);
1218 i += 3;
1219 }
1220 _ => {
1221 out.push(b'%');
1222 i += 1;
1223 }
1224 },
1225 c => {
1226 out.push(c);
1227 i += 1;
1228 }
1229 }
1230 }
1231 String::from_utf8_lossy(&out).into_owned()
1232}
1233
1234fn form_encode(s: &str) -> String {
1237 let mut out = String::with_capacity(s.len());
1238 for &b in s.as_bytes() {
1239 match b {
1240 b' ' => out.push('+'),
1241 b'*' | b'-' | b'.' | b'_' => out.push(b as char),
1242 _ if b.is_ascii_alphanumeric() => out.push(b as char),
1243 _ => {
1244 out.push('%');
1245 out.push(hex_upper(b >> 4));
1246 out.push(hex_upper(b & 0x0f));
1247 }
1248 }
1249 }
1250 out
1251}
1252
1253fn hex_val(c: u8) -> Option<u8> {
1254 match c {
1255 b'0'..=b'9' => Some(c - b'0'),
1256 b'a'..=b'f' => Some(c - b'a' + 10),
1257 b'A'..=b'F' => Some(c - b'A' + 10),
1258 _ => None,
1259 }
1260}
1261
1262fn hex_upper(n: u8) -> char {
1263 char::from_digit(n as u32, 16).unwrap().to_ascii_uppercase()
1264}