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 |_| {
334 if port.bytes().all(|b| b == b'0') {
335 Some("0".to_string())
336 } else {
337 None
338 }
339 },
340 |n| Some(n.to_string()),
341 )?
342 } else {
343 return None;
344 };
345
346 let hash = match tail.find('#') {
347 Some(i) => {
348 let h = tail[i..].to_string();
349 tail = &tail[..i];
350 h
351 }
352 None => String::new(),
353 };
354 let search = match tail.find('?') {
355 Some(i) => {
356 let s = tail[i..].to_string();
357 tail = &tail[..i];
358 s
359 }
360 None => String::new(),
361 };
362 let scheme = scheme.to_ascii_lowercase();
364 let default_port = special_port(&scheme);
365 let pathname = if tail.is_empty() {
366 "/".to_string()
367 } else {
368 normalize_path(tail)
369 };
370 let port = if default_port == Some(port.as_str()) {
372 String::new()
373 } else {
374 port
375 };
376
377 Some(Parts {
378 protocol: format!("{scheme}:"),
379 username,
380 password,
381 hostname,
382 port,
383 pathname,
384 search,
385 hash,
386 })
387}
388
389fn normalize_path(path: &str) -> String {
394 if !path.contains('.') {
395 return path.to_string();
396 }
397 let rooted = path.starts_with('/');
398 let mut out: Vec<&str> = Vec::new();
399 let mut trailing_slash = false;
400 for seg in path.split('/') {
401 match seg {
402 "." => trailing_slash = true,
403 ".." => {
404 out.pop();
405 trailing_slash = true;
406 }
407 _ => {
408 out.push(seg);
409 trailing_slash = false;
410 }
411 }
412 }
413 if rooted && out.first() != Some(&"") {
416 out.insert(0, "");
417 }
418 let mut joined = out.join("/");
419 if trailing_slash && !joined.ends_with('/') {
420 joined.push('/');
421 }
422 if joined.is_empty() {
423 joined.push('/');
424 }
425 joined
426}
427
428pub fn construct(args: &[Value]) -> Result<Value, String> {
430 let to_str = |v: &Value| {
433 crate::host::to_string_value(v).map(|s| crate::host::with_host(|h| h.str_of(&s)))
434 };
435 let input = match args.first() {
436 Some(v) => to_str(v)?,
437 None => "undefined".to_string(),
438 };
439 let base = match args.get(1) {
441 Some(Value::Undef) | None => None,
442 Some(v) => Some(to_str(v)?),
443 };
444 let parts = parse_absolute(&input)
445 .or_else(|| {
446 if let Some(base) = &base {
448 parse_absolute(base).map(|mut b| {
449 let mut rest = input.as_str();
452 let hash = match rest.find('#') {
453 Some(i) => {
454 let h = rest[i..].to_string();
455 rest = &rest[..i];
456 h
457 }
458 None => String::new(),
459 };
460 let search = match rest.find('?') {
461 Some(i) => {
462 let q = rest[i..].to_string();
463 rest = &rest[..i];
464 q
465 }
466 None => String::new(),
467 };
468 let merged = if rest.starts_with('/') {
471 rest.to_string()
472 } else if rest.is_empty() {
473 b.pathname.clone()
474 } else {
475 let dir = match b.pathname.rfind('/') {
476 Some(i) => &b.pathname[..=i],
477 None => "/",
478 };
479 format!("{dir}{rest}")
480 };
481 b.pathname = normalize_path(&merged);
482 b.search = search;
483 b.hash = hash;
484 b
485 })
486 } else {
487 None
488 }
489 })
490 .ok_or_else(|| {
497 let mut fields = vec![("input", input.as_str())];
498 if let Some(b) = &base {
499 fields.push(("base", b.as_str()));
500 }
501 crate::host::plain_coded_error_with(
502 "TypeError",
503 "ERR_INVALID_URL",
504 "Invalid URL",
505 &fields,
506 )
507 })?;
508 Ok(build(&parts))
509}
510
511fn percent_encode(s: &str, extra: &str) -> String {
523 let bytes = s.as_bytes();
524 let mut out = String::with_capacity(s.len());
525 let mut i = 0;
526 while i < bytes.len() {
527 let b = bytes[i];
528 if b == b'%' && i + 2 < bytes.len() + 1 {
530 let hex = bytes.get(i + 1..i + 3);
531 if hex.is_some_and(|h| h.iter().all(|c| c.is_ascii_hexdigit())) {
532 out.push('%');
533 out.push(bytes[i + 1] as char);
534 out.push(bytes[i + 2] as char);
535 i += 3;
536 continue;
537 }
538 }
539 if b < 0x20 || b == 0x7f || b >= 0x80 || extra.as_bytes().contains(&b) {
540 out.push_str(&format!("%{b:02X}"));
541 } else {
542 out.push(b as char);
543 }
544 i += 1;
545 }
546 out
547}
548
549const PATH_SET: &str = " \"<>^`{}";
551const QUERY_SET: &str = " \"'<>";
552const FRAGMENT_SET: &str = " \"<>`";
553const USERINFO_SET: &str = " \";<=>@[]^`{|}";
554
555fn build(p: &Parts) -> Value {
556 let p = &Parts {
562 protocol: p.protocol.clone(),
563 username: percent_encode(&p.username, USERINFO_SET),
564 password: percent_encode(&p.password, USERINFO_SET),
565 hostname: p.hostname.clone(),
566 port: p.port.clone(),
567 pathname: percent_encode(&p.pathname, PATH_SET),
568 search: percent_encode(&p.search, QUERY_SET),
569 hash: percent_encode(&p.hash, FRAGMENT_SET),
570 };
571 let query = p.search.strip_prefix('?').unwrap_or(&p.search);
577 let search_params = make_search_params(&parse_query(query));
578 with_host(|h| {
579 let mut m = IndexMap::new();
580 m.insert("@@native".into(), h.new_str("URL"));
581 m.insert("@@href".into(), h.new_str(p.href()));
582 m.insert("@@origin".into(), h.new_str(p.origin()));
583 m.insert("@@protocol".into(), h.new_str(p.protocol.clone()));
584 m.insert("@@username".into(), h.new_str(p.username.clone()));
585 m.insert("@@password".into(), h.new_str(p.password.clone()));
586 m.insert("@@host".into(), h.new_str(p.host()));
587 m.insert("@@hostname".into(), h.new_str(p.hostname.clone()));
588 m.insert("@@port".into(), h.new_str(p.port.clone()));
589 m.insert("@@pathname".into(), h.new_str(p.pathname.clone()));
590 m.insert("@@search".into(), h.new_str(p.search.clone()));
591 m.insert("@@searchParams".into(), search_params.clone());
592 m.insert("@@hash".into(), h.new_str(p.hash.clone()));
593 let obj = h.new_object(m);
594 if let Some(JsObj::Object(sp)) = h.get_mut(&search_params) {
596 sp.insert("@@ownerUrl".into(), obj.clone());
597 }
598 obj
599 })
600}
601
602pub const STATIC_METHODS: &[&str] = &["canParse", "parse"];
608
609pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
615 let parsed = construct(args);
616 Some(match method {
617 "canParse" => Ok(Value::Bool(parsed.is_ok())),
618 "parse" => Ok(parsed.unwrap_or_else(|_| with_host(|h| h.null()))),
619 _ => return None,
620 })
621}
622
623pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
624 Some(match method {
625 "parse" => legacy_parse(args).map(|u| super::url_legacy::to_js(&u)),
626 "format" => super::url_legacy::format_value(&args.first().cloned().unwrap_or(Value::Undef)),
627 "fileURLToPath" => file_url_to_path(args).map(|s| with_host(|h| h.new_str(s))),
631 "fileURLToPathBuffer" => {
633 file_url_to_path(args).map(|s| super::buffer::from_bytes(s.as_bytes()))
634 }
635 "pathToFileURL" => Ok(path_to_file_url(&arg_str(args, 0))),
637 "domainToASCII" => Ok(punycode_domain(args, true)),
641 "domainToUnicode" => Ok(punycode_domain(args, false)),
642 "urlToHttpOptions" => Ok(url_to_http_options(
644 &args.first().cloned().unwrap_or(Value::Undef),
645 )),
646 "resolve" => legacy_resolve_object(args)
650 .map(|u| with_host(|h| h.new_str(u.href.unwrap_or_default()))),
651 "resolveObject" => {
654 if !args.first().is_some_and(|v| with_host(|h| h.truthy(v))) {
655 return Some(Ok(args.get(1).cloned().unwrap_or(Value::Undef)));
656 }
657 legacy_resolve_object(args).map(|u| super::url_legacy::to_js(&u))
658 }
659 _ => return None,
660 })
661}
662
663fn legacy_parse(args: &[Value]) -> Result<super::url_legacy::Url, String> {
667 emit_url_parse_deprecation();
668 let input = arg_str(args, 0);
669 let truthy = |i: usize| {
670 args.get(i)
671 .map(|v| with_host(|h| h.truthy(v)))
672 .unwrap_or(false)
673 };
674 super::url_legacy::parse(&input, truthy(1), truthy(2))
675}
676
677fn emit_url_parse_deprecation() {
680 super::process::emit_deprecation_warning(
681 "DEP0169",
682 "`url.parse()` behavior is not standardized and prone to errors that \
683 have security implications. Use the WHATWG URL API instead. CVEs are \
684 not issued for `url.parse()` vulnerabilities.",
685 );
686}
687
688fn legacy_resolve_object(args: &[Value]) -> Result<super::url_legacy::Url, String> {
691 emit_url_parse_deprecation();
692 let source = super::url_legacy::parse(&arg_str(args, 0), false, true)?;
693 let relative = super::url_legacy::parse(&arg_str(args, 1), false, true)?;
694 Ok(super::url_legacy::resolve_object(&source, relative))
695}
696
697pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
699 match method {
700 "toString" | "toJSON" => Ok(with_host(|h| match h.get(recv) {
701 Some(JsObj::Object(p)) => p.get("@@href").cloned().unwrap_or(Value::Undef),
702 _ => Value::Undef,
703 })),
704 _ => Err(crate::host::type_error(&format!(
705 "url.{method} is not a function"
706 ))),
707 }
708}
709
710fn url_href(v: &Value) -> String {
715 with_host(|h| match h.get(v) {
716 Some(JsObj::Object(p)) => match p.get("@@native").map(|x| h.str_of(x)).as_deref() {
717 Some("URL") => p.get("@@href").map(|x| h.str_of(x)).unwrap_or_default(),
718 _ => h.str_of(v),
719 },
720 _ => h.str_of(v),
721 })
722}
723
724fn file_url_to_path(args: &[Value]) -> Result<String, String> {
726 let v = args.first().cloned().unwrap_or(Value::Undef);
727 let href = url_href(&v);
728 let rest = href.strip_prefix("file://").ok_or_else(|| {
729 crate::host::plain_coded_error(
730 "TypeError",
731 "ERR_INVALID_URL_SCHEME",
732 "The URL must be of scheme file",
733 )
734 })?;
735 let path = match rest.find('/') {
737 Some(0) => rest,
738 Some(i) => &rest[i..],
739 None => "/",
740 };
741 Ok(percent_decode(path))
742}
743
744fn path_to_file_url(path: &str) -> Value {
747 let enc = encode_path_component(path);
748 let pathname = if enc.starts_with('/') {
749 enc
750 } else {
751 format!("/{enc}")
752 };
753 let parts = Parts {
754 protocol: "file:".into(),
755 username: String::new(),
756 password: String::new(),
757 hostname: String::new(),
758 port: String::new(),
759 pathname,
760 search: String::new(),
761 hash: String::new(),
762 };
763 build(&parts)
764}
765
766fn punycode_domain(args: &[Value], ascii: bool) -> Value {
768 let method = if ascii { "toASCII" } else { "toUnicode" };
769 match super::punycode::call(method, args) {
770 Some(Ok(v)) => v,
771 _ => with_host(|h| h.new_str("")),
772 }
773}
774
775fn url_to_http_options(v: &Value) -> Value {
778 let get = |key: &str| -> String {
779 with_host(|h| match h.get(v) {
780 Some(JsObj::Object(p)) => p.get(key).map(|x| h.str_of(x)).unwrap_or_default(),
781 _ => String::new(),
782 })
783 };
784 let protocol = get("@@protocol");
785 let mut hostname = get("@@hostname");
786 if hostname.starts_with('[') && hostname.ends_with(']') && hostname.len() >= 2 {
787 hostname = hostname[1..hostname.len() - 1].to_string();
788 }
789 let hash = get("@@hash");
790 let search = get("@@search");
791 let pathname = get("@@pathname");
792 let href = get("@@href");
793 let port = get("@@port");
794 let username = get("@@username");
795 let password = get("@@password");
796 let path = format!("{pathname}{search}");
797 let auth = if username.is_empty() && password.is_empty() {
798 None
799 } else {
800 Some(format!(
801 "{}:{}",
802 percent_decode(&username),
803 percent_decode(&password)
804 ))
805 };
806 let port_num = if port.is_empty() {
807 None
808 } else {
809 port.parse::<f64>().ok()
810 };
811 with_host(|h| {
812 let mut m = IndexMap::new();
813 m.insert("protocol".into(), h.new_str(protocol));
814 m.insert("hostname".into(), h.new_str(hostname));
815 m.insert("hash".into(), h.new_str(hash));
816 m.insert("search".into(), h.new_str(search));
817 m.insert("pathname".into(), h.new_str(pathname));
818 m.insert("path".into(), h.new_str(path));
819 m.insert("href".into(), h.new_str(href));
820 if let Some(n) = port_num {
821 m.insert("port".into(), Value::Float(n));
822 }
823 if let Some(a) = auth {
824 m.insert("auth".into(), h.new_str(a));
825 }
826 h.new_object(m)
827 })
828}
829
830pub(crate) fn percent_decode(s: &str) -> String {
833 let b = s.as_bytes();
834 let mut out: Vec<u8> = Vec::with_capacity(b.len());
835 let mut i = 0;
836 while i < b.len() {
837 if b[i] == b'%' && i + 2 < b.len() {
838 if let (Some(hi), Some(lo)) = (hex_val(b[i + 1]), hex_val(b[i + 2])) {
839 out.push((hi << 4) | lo);
840 i += 3;
841 continue;
842 }
843 }
844 out.push(b[i]);
845 i += 1;
846 }
847 String::from_utf8_lossy(&out).into_owned()
848}
849
850fn encode_path_component(s: &str) -> String {
853 let mut out = String::with_capacity(s.len());
854 for &b in s.as_bytes() {
855 let keep = b.is_ascii_alphanumeric()
856 || matches!(
857 b,
858 b'/' | b'-'
859 | b'.'
860 | b'_'
861 | b'~'
862 | b'!'
863 | b'$'
864 | b'&'
865 | b'\''
866 | b'('
867 | b')'
868 | b'*'
869 | b'+'
870 | b','
871 | b';'
872 | b'='
873 | b':'
874 | b'@'
875 );
876 if keep {
877 out.push(b as char);
878 } else {
879 out.push('%');
880 out.push(hex_upper(b >> 4));
881 out.push(hex_upper(b & 0x0f));
882 }
883 }
884 out
885}
886
887pub const SEARCH_PARAMS_METHODS: &[&str] = &[
897 "get",
898 "getAll",
899 "has",
900 "set",
901 "append",
902 "delete",
903 "keys",
904 "values",
905 "entries",
906 "forEach",
907 "toString",
908 "sort",
909 "@@iterator",
910];
911
912fn make_search_params(pairs: &[(String, String)]) -> Value {
914 with_host(|h| {
915 let items: Vec<Value> = pairs
916 .iter()
917 .map(|(k, v)| {
918 let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
919 h.new_array(kv)
920 })
921 .collect();
922 let arr = h.new_array(items);
923 let mut m = IndexMap::new();
924 m.insert("@@native".into(), h.new_str("URLSearchParams"));
925 m.insert("@@pairs".into(), arr);
926 m.insert("size".into(), Value::Float(pairs.len() as f64));
930 let obj = h.new_object(m);
931 h.hide_prop(&obj, "size");
932 obj
933 })
934}
935
936fn encode_query(pairs: &[(String, String)]) -> String {
939 pairs
940 .iter()
941 .map(|(k, v)| format!("{}={}", form_encode(k), form_encode(v)))
942 .collect::<Vec<_>>()
943 .join("&")
944}
945
946fn pairs_of(recv: &Value) -> Vec<(String, String)> {
948 with_host(|h| {
949 let items: Vec<Value> = match h.get(recv) {
950 Some(JsObj::Object(p)) => match p.get("@@pairs").and_then(|a| h.get(a)) {
951 Some(JsObj::Array(items)) => items.clone(),
952 _ => Vec::new(),
953 },
954 _ => Vec::new(),
955 };
956 items
957 .iter()
958 .map(|it| match h.get(it) {
959 Some(JsObj::Array(kv)) => {
960 let kv = kv.clone();
961 let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
962 let v = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
963 (k, v)
964 }
965 _ => (h.str_of(it), String::new()),
966 })
967 .collect()
968 })
969}
970
971fn set_pairs(recv: &Value, pairs: &[(String, String)]) {
978 write_pairs(recv, pairs);
979 let owner = with_host(|h| match h.get(recv) {
980 Some(JsObj::Object(p)) => p.get("@@ownerUrl").cloned(),
981 _ => None,
982 });
983 if let Some(owner) = owner {
984 let query = encode_query(pairs);
985 with_host(|h| {
986 let s = h.new_str(if query.is_empty() {
987 String::new()
988 } else {
989 format!("?{query}")
990 });
991 if let Some(JsObj::Object(p)) = h.get_mut(&owner) {
992 p.insert("@@search".into(), s);
993 }
994 });
995 recompute(&owner, false);
996 }
997}
998
999fn write_pairs(recv: &Value, pairs: &[(String, String)]) {
1001 with_host(|h| {
1002 let items: Vec<Value> = pairs
1003 .iter()
1004 .map(|(k, v)| {
1005 let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
1006 h.new_array(kv)
1007 })
1008 .collect();
1009 let arr = h.new_array(items);
1010 let n = Value::Float(pairs.len() as f64);
1011 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
1012 p.insert("@@pairs".into(), arr);
1013 p.insert("size".into(), n);
1014 }
1015 h.hide_prop(recv, "size");
1016 });
1017}
1018
1019pub fn construct_search_params(args: &[Value]) -> Result<Value, String> {
1022 let pairs = match args.first() {
1023 None => Vec::new(),
1024 Some(v) if matches!(v, Value::Undef) || with_host(|h| h.is_null(v)) => Vec::new(),
1025 Some(v) => pairs_from_init(v),
1026 };
1027 Ok(make_search_params(&pairs))
1028}
1029
1030fn pairs_from_init(v: &Value) -> Vec<(String, String)> {
1031 if super::native_tag(v).as_deref() == Some("URLSearchParams") {
1033 return pairs_of(v);
1034 }
1035 if let Some(s) = with_host(|h| h.as_str(v)) {
1037 return parse_query(s.strip_prefix('?').unwrap_or(&s));
1038 }
1039 with_host(|h| match h.get(v) {
1040 Some(JsObj::Array(items)) => {
1042 let items = items.clone();
1043 items
1044 .iter()
1045 .map(|it| match h.get(it) {
1046 Some(JsObj::Array(kv)) => {
1047 let kv = kv.clone();
1048 let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
1049 let val = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
1050 (k, val)
1051 }
1052 _ => (h.str_of(it), String::new()),
1053 })
1054 .collect()
1055 }
1056 Some(JsObj::Object(p)) => {
1058 let entries: Vec<(String, Value)> = p
1059 .iter()
1060 .filter(|(k, _)| !k.starts_with("@@"))
1061 .map(|(k, val)| (k.clone(), val.clone()))
1062 .collect();
1063 entries
1064 .into_iter()
1065 .map(|(k, val)| (k, h.str_of(&val)))
1066 .collect()
1067 }
1068 _ => Vec::new(),
1069 })
1070}
1071
1072pub fn search_params_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
1074 match method {
1075 "get" => {
1076 let name = arg_str(args, 0);
1077 match pairs_of(recv).into_iter().find(|(k, _)| *k == name) {
1078 Some((_, v)) => Ok(with_host(|h| h.new_str(v))),
1079 None => Ok(with_host(|h| h.null())),
1080 }
1081 }
1082 "getAll" => {
1083 let name = arg_str(args, 0);
1084 let vals: Vec<String> = pairs_of(recv)
1085 .into_iter()
1086 .filter(|(k, _)| *k == name)
1087 .map(|(_, v)| v)
1088 .collect();
1089 Ok(with_host(|h| {
1090 let items = vals.into_iter().map(|v| h.new_str(v)).collect();
1091 h.new_array(items)
1092 }))
1093 }
1094 "has" => {
1095 let name = arg_str(args, 0);
1096 let pairs = pairs_of(recv);
1097 let found = if args.len() > 1 {
1098 let val = arg_str(args, 1);
1099 pairs.iter().any(|(k, v)| *k == name && *v == val)
1100 } else {
1101 pairs.iter().any(|(k, _)| *k == name)
1102 };
1103 Ok(Value::Bool(found))
1104 }
1105 "append" => {
1106 let mut pairs = pairs_of(recv);
1107 pairs.push((arg_str(args, 0), arg_str(args, 1)));
1108 set_pairs(recv, &pairs);
1109 Ok(Value::Undef)
1110 }
1111 "set" => {
1112 let name = arg_str(args, 0);
1113 let val = arg_str(args, 1);
1114 let mut pairs = pairs_of(recv);
1115 let mut seen = false;
1118 pairs.retain_mut(|(k, v)| {
1119 if *k == name {
1120 if seen {
1121 false
1122 } else {
1123 *v = val.clone();
1124 seen = true;
1125 true
1126 }
1127 } else {
1128 true
1129 }
1130 });
1131 if !seen {
1132 pairs.push((name, val));
1133 }
1134 set_pairs(recv, &pairs);
1135 Ok(Value::Undef)
1136 }
1137 "delete" => {
1138 let name = arg_str(args, 0);
1139 let mut pairs = pairs_of(recv);
1140 if args.len() > 1 {
1141 let val = arg_str(args, 1);
1142 pairs.retain(|(k, v)| !(*k == name && *v == val));
1143 } else {
1144 pairs.retain(|(k, _)| *k != name);
1145 }
1146 set_pairs(recv, &pairs);
1147 Ok(Value::Undef)
1148 }
1149 "sort" => {
1150 let mut pairs = pairs_of(recv);
1151 pairs.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
1153 set_pairs(recv, &pairs);
1154 Ok(Value::Undef)
1155 }
1156 "toString" => {
1157 let s = encode_query(&pairs_of(recv));
1158 Ok(with_host(|h| h.new_str(s)))
1159 }
1160 "keys" => {
1161 let pairs = pairs_of(recv);
1162 Ok(with_host(|h| {
1163 let items = pairs.into_iter().map(|(k, _)| h.new_str(k)).collect();
1164 h.alloc(JsObj::Iter {
1165 items,
1166 idx: 0,
1167 array: None,
1168 })
1169 }))
1170 }
1171 "values" => {
1172 let pairs = pairs_of(recv);
1173 Ok(with_host(|h| {
1174 let items = pairs.into_iter().map(|(_, v)| h.new_str(v)).collect();
1175 h.alloc(JsObj::Iter {
1176 items,
1177 idx: 0,
1178 array: None,
1179 })
1180 }))
1181 }
1182 "entries" | "@@iterator" => {
1183 let pairs = pairs_of(recv);
1184 Ok(with_host(|h| {
1185 let items = pairs
1186 .into_iter()
1187 .map(|(k, v)| {
1188 let kv = vec![h.new_str(k), h.new_str(v)];
1189 h.new_array(kv)
1190 })
1191 .collect();
1192 h.alloc(JsObj::Iter {
1193 items,
1194 idx: 0,
1195 array: None,
1196 })
1197 }))
1198 }
1199 "forEach" => {
1200 let cb = args.first().cloned().unwrap_or(Value::Undef);
1201 let this_arg = args.get(1).cloned();
1202 for (k, v) in pairs_of(recv) {
1204 let (value, name) = with_host(|h| (h.new_str(v), h.new_str(k)));
1205 crate::host::invoke(&cb, vec![value, name, recv.clone()], this_arg.clone())?;
1206 }
1207 Ok(Value::Undef)
1208 }
1209 _ => Err(crate::host::type_error(&format!(
1210 "urlSearchParams.{method} is not a function"
1211 ))),
1212 }
1213}
1214
1215fn parse_query(q: &str) -> Vec<(String, String)> {
1217 q.split('&')
1218 .filter(|s| !s.is_empty())
1219 .map(|seg| match seg.split_once('=') {
1220 Some((k, v)) => (form_decode(k), form_decode(v)),
1221 None => (form_decode(seg), String::new()),
1222 })
1223 .collect()
1224}
1225
1226fn form_decode(s: &str) -> String {
1229 let b = s.as_bytes();
1230 let mut out: Vec<u8> = Vec::with_capacity(b.len());
1231 let mut i = 0;
1232 while i < b.len() {
1233 match b[i] {
1234 b'+' => {
1235 out.push(b' ');
1236 i += 1;
1237 }
1238 b'%' if i + 2 < b.len() => match (hex_val(b[i + 1]), hex_val(b[i + 2])) {
1239 (Some(hi), Some(lo)) => {
1240 out.push((hi << 4) | lo);
1241 i += 3;
1242 }
1243 _ => {
1244 out.push(b'%');
1245 i += 1;
1246 }
1247 },
1248 c => {
1249 out.push(c);
1250 i += 1;
1251 }
1252 }
1253 }
1254 String::from_utf8_lossy(&out).into_owned()
1255}
1256
1257fn form_encode(s: &str) -> String {
1260 let mut out = String::with_capacity(s.len());
1261 for &b in s.as_bytes() {
1262 match b {
1263 b' ' => out.push('+'),
1264 b'*' | b'-' | b'.' | b'_' => out.push(b as char),
1265 _ if b.is_ascii_alphanumeric() => out.push(b as char),
1266 _ => {
1267 out.push('%');
1268 out.push(hex_upper(b >> 4));
1269 out.push(hex_upper(b & 0x0f));
1270 }
1271 }
1272 }
1273 out
1274}
1275
1276fn hex_val(c: u8) -> Option<u8> {
1277 match c {
1278 b'0'..=b'9' => Some(c - b'0'),
1279 b'a'..=b'f' => Some(c - b'a' + 10),
1280 b'A'..=b'F' => Some(c - b'A' + 10),
1281 _ => None,
1282 }
1283}
1284
1285fn hex_upper(n: u8) -> char {
1286 char::from_digit(n as u32, 16).unwrap().to_ascii_uppercase()
1287}