1use crate::host::{with_host, JsObj};
17use fusevm::Value;
18use indexmap::IndexMap;
19
20fn is_unsafe_protocol(lower_proto: &str) -> bool {
22 matches!(lower_proto, "javascript" | "javascript:")
23}
24
25fn is_hostless_protocol(lower_proto: &str) -> bool {
27 matches!(lower_proto, "javascript" | "javascript:")
28}
29
30fn is_slashed_protocol(p: &str) -> bool {
32 matches!(
33 p,
34 "http"
35 | "http:"
36 | "https"
37 | "https:"
38 | "ftp"
39 | "ftp:"
40 | "gopher"
41 | "gopher:"
42 | "file"
43 | "file:"
44 | "ws"
45 | "ws:"
46 | "wss"
47 | "wss:"
48 )
49}
50
51fn escaped_code(c: char) -> Option<&'static str> {
54 Some(match c {
55 '\t' => "%09",
56 '\n' => "%0A",
57 '\r' => "%0D",
58 ' ' => "%20",
59 '"' => "%22",
60 '\'' => "%27",
61 '<' => "%3C",
62 '>' => "%3E",
63 '\\' => "%5C",
64 '^' => "%5E",
65 '`' => "%60",
66 '{' => "%7B",
67 '|' => "%7C",
68 '}' => "%7D",
69 _ => return None,
70 })
71}
72
73fn auto_escape_str(rest: &str) -> String {
74 let mut out = String::with_capacity(rest.len());
75 let mut escaped_any = false;
76 for c in rest.chars() {
77 match escaped_code(c) {
78 Some(e) => {
79 out.push_str(e);
80 escaped_any = true;
81 }
82 None => out.push(c),
83 }
84 }
85 if escaped_any {
86 out
87 } else {
88 rest.to_string()
89 }
90}
91
92fn is_js_space(c: char) -> bool {
94 c.is_whitespace() || c == '\u{feff}'
95}
96
97fn is_trim_ws(c: char) -> bool {
99 (c as u32) < 33 || c == '\u{a0}' || c == '\u{feff}'
100}
101
102#[derive(Default, Clone)]
105pub struct Url {
106 pub protocol: Option<String>,
107 pub slashes: Option<bool>,
108 pub auth: Option<String>,
109 pub host: Option<String>,
110 pub port: Option<String>,
111 pub hostname: Option<String>,
112 pub hash: Option<String>,
113 pub search: Option<String>,
114 pub query: Option<Result<String, String>>,
117 pub pathname: Option<String>,
118 pub path: Option<String>,
119 pub href: Option<String>,
120}
121
122fn parse_host(u: &mut Url) {
124 let Some(host) = u.host.clone() else { return };
125 let hc: Vec<char> = host.chars().collect();
126 let mut i = hc.len();
128 while i > 0 && hc[i - 1].is_ascii_digit() {
129 i -= 1;
130 }
131 let mut host = host;
132 if i > 0 && hc[i - 1] == ':' {
133 let port: String = hc[i..].iter().collect();
134 if !port.is_empty() {
135 u.port = Some(port);
136 }
137 host = hc[..i - 1].iter().collect();
138 }
139 if !host.is_empty() {
140 u.hostname = Some(host);
141 }
142}
143
144fn match_protocol(rest: &[char]) -> Option<String> {
146 let mut i = 0;
147 while i < rest.len() && (rest[i].is_ascii_alphanumeric() || matches!(rest[i], '.' | '+' | '-'))
148 {
149 i += 1;
150 }
151 if i > 0 && rest.get(i) == Some(&':') {
152 Some(rest[..=i].iter().collect())
153 } else {
154 None
155 }
156}
157
158fn matches_host_pattern(rest: &[char]) -> bool {
160 if rest.len() < 2 || rest[0] != '/' || rest[1] != '/' {
161 return false;
162 }
163 let mut i = 2;
164 let start = i;
165 while i < rest.len() && rest[i] != '@' && rest[i] != '/' {
166 i += 1;
167 }
168 if i == start || rest.get(i) != Some(&'@') {
169 return false;
170 }
171 i += 1;
172 let start = i;
173 while i < rest.len() && rest[i] != '@' && rest[i] != '/' {
174 i += 1;
175 }
176 i > start
177}
178
179fn match_simple_path(rest: &[char]) -> Option<(String, Option<String>)> {
181 if rest.first() != Some(&'/') {
182 return None;
183 }
184 let mut i = 1;
187 if rest.get(1) == Some(&'/') {
188 i = 2;
189 }
190 if rest.get(i) == Some(&'/') {
191 return None;
192 }
193 let mut j = i;
194 while j < rest.len() && rest[j] != '?' && !is_js_space(rest[j]) {
195 j += 1;
196 }
197 let group1: String = rest[..j].iter().collect();
198 if j == rest.len() {
199 return Some((group1, None));
200 }
201 if rest[j] != '?' {
202 return None;
204 }
205 if rest[j + 1..].iter().any(|&c| is_js_space(c)) {
206 return None;
207 }
208 let group2: String = rest[j..].iter().collect();
209 Some((group1, Some(group2)))
210}
211
212fn get_hostname(u: &mut Url, rest: &str, hostname: &str, url: &str) -> Result<String, String> {
216 for (i, c) in hostname.chars().enumerate() {
217 if matches!(c, '/' | '\\' | '#' | '?' | ':') {
218 if c == ':' {
219 return Err(std::format!(
223 "TypeError [ERR_INVALID_ARG_VALUE]: The argument 'url' {url}. \
224 Received 'Invalid port in url'"
225 ));
226 }
227 let head: String = hostname.chars().take(i).collect();
228 let tail: String = hostname.chars().skip(i).collect();
229 u.hostname = Some(head);
230 return Ok(std::format!("/{tail}{rest}"));
231 }
232 }
233 Ok(rest.to_string())
234}
235
236fn is_ipv6_hostname(h: &str) -> bool {
237 h.starts_with('[') && h.ends_with(']') && h.len() >= 2
238}
239
240fn has_forbidden_host_char(h: &str, ipv6: bool) -> bool {
241 h.chars().any(|c| {
242 matches!(
243 c,
244 '\0' | '\t'
245 | '\n'
246 | '\r'
247 | ' '
248 | '#'
249 | '%'
250 | '/'
251 | '<'
252 | '>'
253 | '?'
254 | '@'
255 | '\\'
256 | '^'
257 | '|'
258 ) || (!ipv6 && matches!(c, ':' | '[' | ']'))
259 })
260}
261
262pub fn parse(
265 url: &str,
266 parse_query_string: bool,
267 slashes_denote_host: bool,
268) -> Result<Url, String> {
269 let mut u = Url::default();
270 let uc: Vec<char> = url.chars().collect();
271
272 let mut has_hash = false;
275 let mut has_at = false;
276 let mut start: isize = -1;
277 let mut end: isize = -1;
278 let mut rest = String::new();
279 let mut last_pos: usize = 0;
280 let mut in_ws = false;
281 let mut split = false;
282 for (i, &code) in uc.iter().enumerate() {
283 let is_ws = is_trim_ws(code);
284 if start == -1 {
285 if is_ws {
286 continue;
287 }
288 last_pos = i;
289 start = i as isize;
290 } else if in_ws {
291 if !is_ws {
292 end = -1;
293 in_ws = false;
294 }
295 } else if is_ws {
296 end = i as isize;
297 in_ws = true;
298 }
299
300 if !split {
301 match code {
302 '@' => has_at = true,
303 '#' => {
304 has_hash = true;
305 split = true;
306 }
307 '?' => split = true,
308 '\\' => {
309 if i > last_pos {
310 rest.extend(&uc[last_pos..i]);
311 }
312 rest.push('/');
313 last_pos = i + 1;
314 }
315 _ => {}
316 }
317 } else if !has_hash && code == '#' {
318 has_hash = true;
319 }
320 }
321
322 if start != -1 {
323 let s = start as usize;
324 if last_pos == s {
325 rest = if end == -1 {
326 uc[s..].iter().collect()
327 } else {
328 uc[s..end as usize].iter().collect()
329 };
330 } else if end == -1 && last_pos < uc.len() {
331 rest.extend(&uc[last_pos..]);
332 } else if end != -1 && (last_pos as isize) < end {
333 rest.extend(&uc[last_pos..end as usize]);
334 }
335 }
336
337 let set_query = |u: &mut Url, raw: String| {
338 u.query = Some(if parse_query_string {
339 Err(raw)
340 } else {
341 Ok(raw)
342 });
343 };
344
345 if !slashes_denote_host && !has_hash && !has_at {
346 let rc: Vec<char> = rest.chars().collect();
347 if let Some((g1, g2)) = match_simple_path(&rc) {
348 u.path = Some(rest.clone());
349 u.href = Some(rest.clone());
350 u.pathname = Some(g1);
351 match g2 {
352 Some(q) => {
353 let raw: String = q.chars().skip(1).collect();
354 u.search = Some(q);
355 set_query(&mut u, raw);
356 }
357 None if parse_query_string => {
358 u.search = None;
359 u.query = Some(Err(String::new()));
360 }
361 None => {}
362 }
363 return Ok(u);
364 }
365 }
366
367 let mut rc: Vec<char> = rest.chars().collect();
368 let proto = match_protocol(&rc);
369 let mut lower_proto = String::new();
370 if let Some(p) = &proto {
371 lower_proto = p.to_lowercase();
372 u.protocol = Some(lower_proto.clone());
373 rc = rc[p.chars().count()..].to_vec();
374 }
375
376 let mut slashes = false;
379 if slashes_denote_host || proto.is_some() || matches_host_pattern(&rc) {
380 slashes = rc.first() == Some(&'/') && rc.get(1) == Some(&'/');
381 if slashes && !(proto.is_some() && is_hostless_protocol(&lower_proto)) {
382 rc = rc[2..].to_vec();
383 u.slashes = Some(true);
384 }
385 }
386
387 if !is_hostless_protocol(&lower_proto)
388 && (slashes || (proto.is_some() && !is_slashed_protocol(proto.as_deref().unwrap_or(""))))
389 {
390 let mut host_end: isize = -1;
395 let mut at_sign: isize = -1;
396 let mut non_host: isize = -1;
397 let mut i = 0usize;
398 while i < rc.len() {
399 match rc[i] {
400 '\t' | '\n' | '\r' => {
401 rc.remove(i);
403 continue;
404 }
405 ' ' | '"' | '%' | '\'' | ';' | '<' | '>' | '\\' | '^' | '`' | '{' | '|' | '}' => {
406 if non_host == -1 {
407 non_host = i as isize;
408 }
409 }
410 '#' | '/' | '?' => {
411 if non_host == -1 {
412 non_host = i as isize;
413 }
414 host_end = i as isize;
415 }
416 '@' => {
417 at_sign = i as isize;
418 non_host = -1;
419 }
420 _ => {}
421 }
422 if host_end != -1 {
423 break;
424 }
425 i += 1;
426 }
427 let mut start = 0usize;
428 if at_sign != -1 {
429 u.auth = Some(super::url::percent_decode(
430 &rc[..at_sign as usize].iter().collect::<String>(),
431 ));
432 start = at_sign as usize + 1;
433 }
434 if non_host == -1 {
435 u.host = Some(rc[start..].iter().collect());
436 rc = Vec::new();
437 } else {
438 u.host = Some(rc[start..non_host as usize].iter().collect());
439 rc = rc[non_host as usize..].to_vec();
440 }
441
442 parse_host(&mut u);
443
444 if u.hostname.is_none() {
447 u.hostname = Some(String::new());
448 }
449 let hostname = u.hostname.clone().unwrap_or_default();
450 let ipv6 = is_ipv6_hostname(&hostname);
451 if !ipv6 {
452 let rest_s: String = rc.iter().collect();
453 rc = get_hostname(&mut u, &rest_s, &hostname, url)?
454 .chars()
455 .collect();
456 }
457
458 let hn = u.hostname.clone().unwrap_or_default();
459 u.hostname = Some(if hn.chars().count() > 255 {
460 String::new()
461 } else {
462 hn.to_lowercase()
463 });
464
465 let hn = u.hostname.clone().unwrap_or_default();
466 if !hn.is_empty() {
467 if ipv6 {
468 if has_forbidden_host_char(&hn, true) {
469 return Err(invalid_url(url));
470 }
471 } else {
472 let ascii = super::punycode::to_ascii(&hn);
474 u.hostname = Some(ascii.clone());
475 if ascii.is_empty() || has_forbidden_host_char(&ascii, false) {
479 return Err(invalid_url(url));
480 }
481 }
482 }
483
484 let p = match &u.port {
485 Some(p) => std::format!(":{p}"),
486 None => String::new(),
487 };
488 let h = u.hostname.clone().unwrap_or_default();
489 u.host = Some(std::format!("{h}{p}"));
490
491 if ipv6 {
493 let hn = u.hostname.clone().unwrap_or_default();
494 let inner: String = {
495 let c: Vec<char> = hn.chars().collect();
496 if c.len() >= 2 {
497 c[1..c.len() - 1].iter().collect()
498 } else {
499 String::new()
500 }
501 };
502 u.hostname = Some(inner);
503 if rc.first() != Some(&'/') {
504 rc.insert(0, '/');
505 }
506 }
507 }
508
509 if !is_unsafe_protocol(&lower_proto) {
510 rc = auto_escape_str(&rc.iter().collect::<String>())
511 .chars()
512 .collect();
513 }
514
515 let mut question_idx: isize = -1;
516 let mut hash_idx: isize = -1;
517 for (i, &c) in rc.iter().enumerate() {
518 if c == '#' {
519 u.hash = Some(rc[i..].iter().collect());
520 hash_idx = i as isize;
521 break;
522 } else if c == '?' && question_idx == -1 {
523 question_idx = i as isize;
524 }
525 }
526
527 if question_idx != -1 {
528 let q = question_idx as usize;
529 if hash_idx == -1 {
530 u.search = Some(rc[q..].iter().collect());
531 set_query(&mut u, rc[q + 1..].iter().collect());
532 } else {
533 let h = hash_idx as usize;
534 u.search = Some(rc[q..h].iter().collect());
535 set_query(&mut u, rc[q + 1..h].iter().collect());
536 }
537 } else if parse_query_string {
538 u.search = None;
539 u.query = Some(Err(String::new()));
540 }
541
542 let use_question = question_idx != -1 && (hash_idx == -1 || question_idx < hash_idx);
543 let first_idx = if use_question { question_idx } else { hash_idx };
544 if first_idx == -1 {
545 if !rc.is_empty() {
546 u.pathname = Some(rc.iter().collect());
547 }
548 } else if first_idx > 0 {
549 u.pathname = Some(rc[..first_idx as usize].iter().collect());
550 }
551 if is_slashed_protocol(&lower_proto)
554 && !u.hostname.as_deref().unwrap_or("").is_empty()
555 && u.pathname.as_deref().unwrap_or("").is_empty()
556 {
557 u.pathname = Some("/".into());
558 }
559
560 if u.pathname.is_some() || u.search.is_some() {
562 let p = u.pathname.clone().unwrap_or_default();
563 let s = u.search.clone().unwrap_or_default();
564 u.path = Some(std::format!("{p}{s}"));
565 }
566
567 u.href = Some(format_url(&u, None));
568 Ok(u)
569}
570
571fn invalid_url(_url: &str) -> String {
572 "TypeError [ERR_INVALID_URL]: Invalid URL".into()
573}
574
575fn auth_needs_escape(c: char) -> bool {
578 !(c.is_ascii_alphanumeric()
579 || matches!(
580 c,
581 '!' | '-' | '.' | '_' | '~' | '\'' | '(' | ')' | '*' | ':'
582 ))
583}
584
585fn encode_auth(auth: &str) -> String {
586 let mut out = String::with_capacity(auth.len());
587 for c in auth.chars() {
588 if auth_needs_escape(c) {
589 let mut buf = [0u8; 4];
590 for b in c.encode_utf8(&mut buf).as_bytes() {
591 out.push_str(&std::format!("%{b:02X}"));
592 }
593 } else {
594 out.push(c);
595 }
596 }
597 out
598}
599
600pub fn format_url(u: &Url, query_string: Option<&str>) -> String {
604 let mut auth = u.auth.clone().unwrap_or_default();
605 if !auth.is_empty() {
606 auth = std::format!("{}@", encode_auth(&auth));
607 }
608
609 let mut protocol = u.protocol.clone().unwrap_or_default();
610 if !protocol.is_empty() && !protocol.ends_with(':') {
611 protocol.push(':');
612 }
613
614 let mut pathname = u.pathname.clone().unwrap_or_default();
615 let mut hash = u.hash.clone().unwrap_or_default();
616 let mut host = String::new();
617
618 if let Some(h) = u.host.as_ref().filter(|h| !h.is_empty()) {
619 host = std::format!("{auth}{h}");
620 } else if let Some(hn) = u.hostname.as_ref().filter(|h| !h.is_empty()) {
621 let bracketed = if hn.contains(':') && !is_ipv6_hostname(hn) {
622 std::format!("[{hn}]")
623 } else {
624 hn.clone()
625 };
626 host = std::format!("{auth}{bracketed}");
627 if let Some(p) = u.port.as_ref().filter(|p| !p.is_empty()) {
628 host.push(':');
629 host.push_str(p);
630 }
631 }
632
633 let query = query_string.unwrap_or("");
634 let mut search = u.search.clone().unwrap_or_default();
635 if search.is_empty() && !query.is_empty() {
636 search = std::format!("?{query}");
637 }
638
639 if pathname.contains('#') || pathname.contains('?') {
640 pathname = pathname
641 .chars()
642 .map(|c| match c {
643 '#' => "%23".to_string(),
644 '?' => "%3F".to_string(),
645 c => c.to_string(),
646 })
647 .collect();
648 }
649
650 if u.slashes == Some(true) || is_slashed_protocol(&protocol) {
653 if u.slashes == Some(true) || !host.is_empty() {
654 if !pathname.is_empty() && !pathname.starts_with('/') {
655 pathname = std::format!("/{pathname}");
656 }
657 host = std::format!("//{host}");
658 } else if protocol.starts_with("file") {
659 host = "//".into();
660 }
661 }
662
663 if search.contains('#') {
664 search = search.replace('#', "%23");
665 }
666 if !hash.is_empty() && !hash.starts_with('#') {
667 hash = std::format!("#{hash}");
668 }
669 if !search.is_empty() && !search.starts_with('?') {
670 search = std::format!("?{search}");
671 }
672
673 std::format!("{protocol}{host}{pathname}{search}{hash}")
674}
675
676pub fn to_js(u: &Url) -> Value {
680 let query_val: Option<Value> = u.query.as_ref().map(|q| match q {
683 Ok(raw) => with_host(|h| h.new_str(raw.clone())),
684 Err(raw) => {
685 let arg = with_host(|h| h.new_str(raw.clone()));
686 super::querystring::call("parse", &[arg])
687 .and_then(|r| r.ok())
688 .unwrap_or(Value::Undef)
689 }
690 });
691 with_host(|h| {
692 let mut m = IndexMap::new();
693 let opt = |h: &mut crate::host::JsHost, v: &Option<String>| match v {
694 Some(s) => h.new_str(s.clone()),
695 None => h.null(),
696 };
697 m.insert("protocol".into(), opt(h, &u.protocol));
698 m.insert(
699 "slashes".into(),
700 match u.slashes {
701 Some(b) => Value::Bool(b),
702 None => h.null(),
703 },
704 );
705 m.insert("auth".into(), opt(h, &u.auth));
706 m.insert("host".into(), opt(h, &u.host));
707 m.insert("port".into(), opt(h, &u.port));
708 m.insert("hostname".into(), opt(h, &u.hostname));
709 m.insert("hash".into(), opt(h, &u.hash));
710 m.insert("search".into(), opt(h, &u.search));
711 m.insert(
712 "query".into(),
713 query_val.clone().unwrap_or_else(|| h.null()),
714 );
715 m.insert("pathname".into(), opt(h, &u.pathname));
716 m.insert("path".into(), opt(h, &u.path));
717 m.insert("href".into(), opt(h, &u.href));
718 h.new_object(m)
719 })
720}
721
722fn from_js(v: &Value) -> (Url, Option<String>) {
726 let query_obj = with_host(|h| match h.get(v) {
728 Some(JsObj::Object(p)) => p.get("query").cloned(),
729 _ => None,
730 });
731 let query_string = match &query_obj {
732 Some(q) if with_host(|h| matches!(h.get(q), Some(JsObj::Object(_)))) => {
733 super::querystring::call("stringify", std::slice::from_ref(q))
734 .and_then(|r| r.ok())
735 .map(|s| with_host(|h| h.str_of(&s)))
736 }
737 _ => None,
738 };
739 let get = |k: &str| {
740 with_host(|h| match h.get(v) {
741 Some(JsObj::Object(p)) => match p.get(k) {
742 None | Some(Value::Undef) => None,
743 Some(x) if h.is_null(x) => None,
744 Some(x) => Some(h.str_of(x)),
745 },
746 _ => None,
747 })
748 };
749 let slashes = with_host(|h| match h.get(v) {
750 Some(JsObj::Object(p)) => p.get("slashes").map(|x| h.truthy(x)),
751 _ => None,
752 });
753 let u = Url {
754 protocol: get("protocol"),
755 slashes,
756 auth: get("auth"),
757 host: get("host"),
758 port: get("port"),
759 hostname: get("hostname"),
760 hash: get("hash"),
761 search: get("search"),
762 query: None,
763 pathname: get("pathname"),
764 path: get("path"),
765 href: get("href"),
766 };
767 (u, query_string)
768}
769
770pub fn format_value(v: &Value) -> Result<Value, String> {
773 if let Some(s) = with_host(|h| h.as_str(v)) {
774 let u = parse(&s, false, false)?;
775 let out = format_url(&u, None);
776 return Ok(with_host(|h| h.new_str(out)));
777 }
778 let href = with_host(|h| match h.get(v) {
780 Some(JsObj::Object(p)) if p.get("@@native").is_some() => p.get("href").map(|x| h.str_of(x)),
781 _ => None,
782 });
783 if let Some(href) = href {
784 return Ok(with_host(|h| h.new_str(href)));
785 }
786 let is_obj = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
787 if !is_obj {
788 let received = super::received_desc(v);
789 return Err(std::format!(
790 "TypeError [ERR_INVALID_ARG_TYPE]: The \"urlObject\" argument must be \
791 one of type object or string. Received {received}"
792 ));
793 }
794 let (u, qs) = from_js(v);
795 let out = format_url(&u, qs.as_deref());
796 Ok(with_host(|h| h.new_str(out)))
797}
798
799fn truthy(s: &Option<String>) -> bool {
803 s.as_deref().is_some_and(|s| !s.is_empty())
804}
805
806fn split_path(pathname: &Option<String>) -> Vec<String> {
808 match pathname.as_deref() {
809 Some(p) if !p.is_empty() => p.split('/').map(str::to_string).collect(),
810 _ => Vec::new(),
811 }
812}
813
814fn split_auth_in_host(result: &mut Url) {
818 let Some(host) = result.host.clone() else {
819 return;
820 };
821 if host.find('@').is_some_and(|i| i > 0) {
822 let mut parts = host.split('@');
823 result.auth = parts.next().map(str::to_string);
824 let rest = parts.next().map(str::to_string);
825 result.host = rest.clone();
826 result.hostname = rest;
827 }
828}
829
830fn set_request_path(result: &mut Url) {
833 if result.pathname.is_some() || result.search.is_some() {
834 result.path = Some(format!(
835 "{}{}",
836 result.pathname.clone().unwrap_or_default(),
837 result.search.clone().unwrap_or_default()
838 ));
839 }
840}
841
842fn with_href(mut result: Url) -> Url {
844 result.href = Some(format_url(&result, None));
845 result
846}
847
848pub fn resolve_object(source: &Url, mut relative: Url) -> Url {
856 let mut result = source.clone();
857 result.hash = relative.hash.clone();
859 if relative.href.as_deref() == Some("") {
860 return with_href(result);
861 }
862
863 if relative.slashes == Some(true) && !truthy(&relative.protocol) {
865 let protocol = result.protocol.take();
866 result = Url {
867 protocol,
868 ..relative
869 };
870 if result.protocol.as_deref().is_some_and(is_slashed_protocol)
871 && truthy(&result.hostname)
872 && !truthy(&result.pathname)
873 {
874 result.pathname = Some("/".into());
875 result.path = Some("/".into());
876 }
877 return with_href(result);
878 }
879
880 if truthy(&relative.protocol) && relative.protocol != result.protocol {
881 let rel_proto = relative.protocol.clone().unwrap_or_default();
882 if !is_slashed_protocol(&rel_proto) {
885 return with_href(relative);
886 }
887 result.protocol = relative.protocol.clone();
888 if !truthy(&relative.host)
889 && rel_proto != "file"
890 && rel_proto != "file:"
891 && !is_hostless_protocol(&rel_proto)
892 {
893 let mut rel_path = relative
895 .pathname
896 .clone()
897 .unwrap_or_default()
898 .split('/')
899 .map(str::to_string)
900 .collect::<Vec<_>>();
901 while !rel_path.is_empty() {
902 let seg = rel_path.remove(0);
903 let found = !seg.is_empty();
904 relative.host = Some(seg);
905 if found {
906 break;
907 }
908 }
909 if !truthy(&relative.host) {
910 relative.host = Some(String::new());
911 }
912 if !truthy(&relative.hostname) {
913 relative.hostname = Some(String::new());
914 }
915 if rel_path.first().map(String::as_str) != Some("") {
916 rel_path.insert(0, String::new());
917 }
918 if rel_path.len() < 2 {
919 rel_path.insert(0, String::new());
920 }
921 result.pathname = Some(rel_path.join("/"));
922 } else {
923 result.pathname = relative.pathname.clone();
924 }
925 result.search = relative.search.clone();
926 result.query = relative.query.clone();
927 result.host = Some(relative.host.clone().unwrap_or_default());
928 result.auth = relative.auth.clone();
929 result.hostname = if truthy(&relative.hostname) {
930 relative.hostname.clone()
931 } else {
932 relative.host.clone()
933 };
934 result.port = relative.port.clone();
935 if truthy(&result.pathname) || truthy(&result.search) {
936 result.path = Some(format!(
937 "{}{}",
938 result.pathname.clone().unwrap_or_default(),
939 result.search.clone().unwrap_or_default()
940 ));
941 }
942 if result.slashes != Some(true) {
943 result.slashes = relative.slashes;
944 }
945 return with_href(result);
946 }
947
948 let is_source_abs = result
949 .pathname
950 .as_deref()
951 .is_some_and(|p| p.starts_with('/'));
952 let is_rel_abs = truthy(&relative.host)
953 || relative
954 .pathname
955 .as_deref()
956 .is_some_and(|p| p.starts_with('/'));
957 let mut must_end_abs =
958 is_rel_abs || is_source_abs || (truthy(&result.host) && truthy(&relative.pathname));
959 let remove_all_dots = must_end_abs;
960 let mut src_path = split_path(&result.pathname);
961 let mut rel_path = split_path(&relative.pathname);
962 let no_leading_slashes = result
963 .protocol
964 .as_deref()
965 .is_some_and(|p| !p.is_empty() && !is_slashed_protocol(p));
966
967 if no_leading_slashes {
970 result.hostname = Some(String::new());
971 result.port = None;
972 if let Some(host) = result.host.clone().filter(|h| !h.is_empty()) {
973 if src_path.first().map(String::as_str) == Some("") {
974 src_path[0] = host;
975 } else {
976 src_path.insert(0, host);
977 }
978 }
979 result.host = Some(String::new());
980 if truthy(&relative.protocol) {
981 relative.hostname = None;
982 relative.port = None;
983 result.auth = None;
984 if let Some(host) = relative.host.clone().filter(|h| !h.is_empty()) {
985 if rel_path.first().map(String::as_str) == Some("") {
986 rel_path[0] = host;
987 } else {
988 rel_path.insert(0, host);
989 }
990 }
991 relative.host = None;
992 }
993 must_end_abs = must_end_abs
994 && (rel_path.first().map(String::as_str) == Some("")
995 || src_path.first().map(String::as_str) == Some(""));
996 }
997
998 if is_rel_abs {
999 if relative.host.is_some() {
1000 if result.host != relative.host {
1001 result.auth = None;
1002 }
1003 result.host = relative.host.clone();
1004 result.port = relative.port.clone();
1005 }
1006 if relative.hostname.is_some() {
1007 if result.hostname != relative.hostname {
1008 result.auth = None;
1009 }
1010 result.hostname = relative.hostname.clone();
1011 }
1012 result.search = relative.search.clone();
1013 result.query = relative.query.clone();
1014 src_path = rel_path;
1015 } else if !rel_path.is_empty() {
1016 src_path.pop();
1018 src_path.extend(rel_path);
1019 result.search = relative.search.clone();
1020 result.query = relative.query.clone();
1021 } else if relative.search.is_some() {
1022 if no_leading_slashes {
1024 let host = (!src_path.is_empty()).then(|| src_path.remove(0));
1025 result.host = host.clone();
1026 result.hostname = host;
1027 split_auth_in_host(&mut result);
1028 }
1029 result.search = relative.search.clone();
1030 result.query = relative.query.clone();
1031 set_request_path(&mut result);
1032 return with_href(result);
1033 }
1034
1035 if src_path.is_empty() {
1036 result.pathname = None;
1038 result.path = result
1039 .search
1040 .as_deref()
1041 .filter(|s| !s.is_empty())
1042 .map(|s| format!("/{s}"));
1043 return with_href(result);
1044 }
1045
1046 let last = src_path.last().cloned().unwrap_or_default();
1049 let has_trailing_slash =
1050 ((truthy(&result.host) || truthy(&relative.host) || src_path.len() > 1)
1051 && (last == "." || last == ".."))
1052 || last.is_empty();
1053
1054 let mut up = 0usize;
1056 let mut i = src_path.len();
1057 while i > 0 {
1058 i -= 1;
1059 if src_path[i] == "." {
1060 src_path.remove(i);
1061 } else if src_path[i] == ".." {
1062 src_path.remove(i);
1063 up += 1;
1064 } else if up > 0 {
1065 src_path.remove(i);
1066 up -= 1;
1067 }
1068 }
1069 if !must_end_abs && !remove_all_dots {
1070 for _ in 0..up {
1071 src_path.insert(0, "..".into());
1072 }
1073 }
1074
1075 let first_is_rooted = |p: &[String]| {
1076 p.first()
1077 .is_some_and(|s| s.is_empty() || s.starts_with('/'))
1078 };
1079 if must_end_abs && !first_is_rooted(&src_path) {
1080 src_path.insert(0, String::new());
1081 }
1082 if has_trailing_slash && !src_path.join("/").ends_with('/') {
1083 src_path.push(String::new());
1084 }
1085 let is_absolute = first_is_rooted(&src_path);
1086
1087 if no_leading_slashes {
1089 let host = if is_absolute || src_path.is_empty() {
1090 String::new()
1091 } else {
1092 src_path.remove(0)
1093 };
1094 result.host = Some(host.clone());
1095 result.hostname = Some(host);
1096 split_auth_in_host(&mut result);
1097 }
1098
1099 must_end_abs = must_end_abs || (truthy(&result.host) && !src_path.is_empty());
1100 if must_end_abs && !is_absolute {
1101 src_path.insert(0, String::new());
1102 }
1103
1104 if src_path.is_empty() {
1105 result.pathname = None;
1106 result.path = None;
1107 } else {
1108 result.pathname = Some(src_path.join("/"));
1109 }
1110 set_request_path(&mut result);
1111 if truthy(&relative.auth) {
1112 result.auth = relative.auth.clone();
1113 }
1114 if result.slashes != Some(true) {
1115 result.slashes = relative.slashes;
1116 }
1117 with_href(result)
1118}