use crate::host::{with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
fn is_unsafe_protocol(lower_proto: &str) -> bool {
matches!(lower_proto, "javascript" | "javascript:")
}
fn is_hostless_protocol(lower_proto: &str) -> bool {
matches!(lower_proto, "javascript" | "javascript:")
}
fn is_slashed_protocol(p: &str) -> bool {
matches!(
p,
"http"
| "http:"
| "https"
| "https:"
| "ftp"
| "ftp:"
| "gopher"
| "gopher:"
| "file"
| "file:"
| "ws"
| "ws:"
| "wss"
| "wss:"
)
}
fn escaped_code(c: char) -> Option<&'static str> {
Some(match c {
'\t' => "%09",
'\n' => "%0A",
'\r' => "%0D",
' ' => "%20",
'"' => "%22",
'\'' => "%27",
'<' => "%3C",
'>' => "%3E",
'\\' => "%5C",
'^' => "%5E",
'`' => "%60",
'{' => "%7B",
'|' => "%7C",
'}' => "%7D",
_ => return None,
})
}
fn auto_escape_str(rest: &str) -> String {
let mut out = String::with_capacity(rest.len());
let mut escaped_any = false;
for c in rest.chars() {
match escaped_code(c) {
Some(e) => {
out.push_str(e);
escaped_any = true;
}
None => out.push(c),
}
}
if escaped_any {
out
} else {
rest.to_string()
}
}
fn is_js_space(c: char) -> bool {
c.is_whitespace() || c == '\u{feff}'
}
fn is_trim_ws(c: char) -> bool {
(c as u32) < 33 || c == '\u{a0}' || c == '\u{feff}'
}
#[derive(Default, Clone)]
pub struct Url {
pub protocol: Option<String>,
pub slashes: Option<bool>,
pub auth: Option<String>,
pub host: Option<String>,
pub port: Option<String>,
pub hostname: Option<String>,
pub hash: Option<String>,
pub search: Option<String>,
pub query: Option<Result<String, String>>,
pub pathname: Option<String>,
pub path: Option<String>,
pub href: Option<String>,
}
fn parse_host(u: &mut Url) {
let Some(host) = u.host.clone() else { return };
let hc: Vec<char> = host.chars().collect();
let mut i = hc.len();
while i > 0 && hc[i - 1].is_ascii_digit() {
i -= 1;
}
let mut host = host;
if i > 0 && hc[i - 1] == ':' {
let port: String = hc[i..].iter().collect();
if !port.is_empty() {
u.port = Some(port);
}
host = hc[..i - 1].iter().collect();
}
if !host.is_empty() {
u.hostname = Some(host);
}
}
fn match_protocol(rest: &[char]) -> Option<String> {
let mut i = 0;
while i < rest.len() && (rest[i].is_ascii_alphanumeric() || matches!(rest[i], '.' | '+' | '-'))
{
i += 1;
}
if i > 0 && rest.get(i) == Some(&':') {
Some(rest[..=i].iter().collect())
} else {
None
}
}
fn matches_host_pattern(rest: &[char]) -> bool {
if rest.len() < 2 || rest[0] != '/' || rest[1] != '/' {
return false;
}
let mut i = 2;
let start = i;
while i < rest.len() && rest[i] != '@' && rest[i] != '/' {
i += 1;
}
if i == start || rest.get(i) != Some(&'@') {
return false;
}
i += 1;
let start = i;
while i < rest.len() && rest[i] != '@' && rest[i] != '/' {
i += 1;
}
i > start
}
fn match_simple_path(rest: &[char]) -> Option<(String, Option<String>)> {
if rest.first() != Some(&'/') {
return None;
}
let mut i = 1;
if rest.get(1) == Some(&'/') {
i = 2;
}
if rest.get(i) == Some(&'/') {
return None;
}
let mut j = i;
while j < rest.len() && rest[j] != '?' && !is_js_space(rest[j]) {
j += 1;
}
let group1: String = rest[..j].iter().collect();
if j == rest.len() {
return Some((group1, None));
}
if rest[j] != '?' {
return None;
}
if rest[j + 1..].iter().any(|&c| is_js_space(c)) {
return None;
}
let group2: String = rest[j..].iter().collect();
Some((group1, Some(group2)))
}
fn get_hostname(u: &mut Url, rest: &str, hostname: &str, url: &str) -> Result<String, String> {
for (i, c) in hostname.chars().enumerate() {
if matches!(c, '/' | '\\' | '#' | '?' | ':') {
if c == ':' {
return Err(std::format!(
"TypeError [ERR_INVALID_ARG_VALUE]: The argument 'url' {url}. \
Received 'Invalid port in url'"
));
}
let head: String = hostname.chars().take(i).collect();
let tail: String = hostname.chars().skip(i).collect();
u.hostname = Some(head);
return Ok(std::format!("/{tail}{rest}"));
}
}
Ok(rest.to_string())
}
fn is_ipv6_hostname(h: &str) -> bool {
h.starts_with('[') && h.ends_with(']') && h.len() >= 2
}
fn has_forbidden_host_char(h: &str, ipv6: bool) -> bool {
h.chars().any(|c| {
matches!(
c,
'\0' | '\t'
| '\n'
| '\r'
| ' '
| '#'
| '%'
| '/'
| '<'
| '>'
| '?'
| '@'
| '\\'
| '^'
| '|'
) || (!ipv6 && matches!(c, ':' | '[' | ']'))
})
}
pub fn parse(
url: &str,
parse_query_string: bool,
slashes_denote_host: bool,
) -> Result<Url, String> {
let mut u = Url::default();
let uc: Vec<char> = url.chars().collect();
let mut has_hash = false;
let mut has_at = false;
let mut start: isize = -1;
let mut end: isize = -1;
let mut rest = String::new();
let mut last_pos: usize = 0;
let mut in_ws = false;
let mut split = false;
for (i, &code) in uc.iter().enumerate() {
let is_ws = is_trim_ws(code);
if start == -1 {
if is_ws {
continue;
}
last_pos = i;
start = i as isize;
} else if in_ws {
if !is_ws {
end = -1;
in_ws = false;
}
} else if is_ws {
end = i as isize;
in_ws = true;
}
if !split {
match code {
'@' => has_at = true,
'#' => {
has_hash = true;
split = true;
}
'?' => split = true,
'\\' => {
if i > last_pos {
rest.extend(&uc[last_pos..i]);
}
rest.push('/');
last_pos = i + 1;
}
_ => {}
}
} else if !has_hash && code == '#' {
has_hash = true;
}
}
if start != -1 {
let s = start as usize;
if last_pos == s {
rest = if end == -1 {
uc[s..].iter().collect()
} else {
uc[s..end as usize].iter().collect()
};
} else if end == -1 && last_pos < uc.len() {
rest.extend(&uc[last_pos..]);
} else if end != -1 && (last_pos as isize) < end {
rest.extend(&uc[last_pos..end as usize]);
}
}
let set_query = |u: &mut Url, raw: String| {
u.query = Some(if parse_query_string {
Err(raw)
} else {
Ok(raw)
});
};
if !slashes_denote_host && !has_hash && !has_at {
let rc: Vec<char> = rest.chars().collect();
if let Some((g1, g2)) = match_simple_path(&rc) {
u.path = Some(rest.clone());
u.href = Some(rest.clone());
u.pathname = Some(g1);
match g2 {
Some(q) => {
let raw: String = q.chars().skip(1).collect();
u.search = Some(q);
set_query(&mut u, raw);
}
None if parse_query_string => {
u.search = None;
u.query = Some(Err(String::new()));
}
None => {}
}
return Ok(u);
}
}
let mut rc: Vec<char> = rest.chars().collect();
let proto = match_protocol(&rc);
let mut lower_proto = String::new();
if let Some(p) = &proto {
lower_proto = p.to_lowercase();
u.protocol = Some(lower_proto.clone());
rc = rc[p.chars().count()..].to_vec();
}
let mut slashes = false;
if slashes_denote_host || proto.is_some() || matches_host_pattern(&rc) {
slashes = rc.first() == Some(&'/') && rc.get(1) == Some(&'/');
if slashes && !(proto.is_some() && is_hostless_protocol(&lower_proto)) {
rc = rc[2..].to_vec();
u.slashes = Some(true);
}
}
if !is_hostless_protocol(&lower_proto)
&& (slashes || (proto.is_some() && !is_slashed_protocol(proto.as_deref().unwrap_or(""))))
{
let mut host_end: isize = -1;
let mut at_sign: isize = -1;
let mut non_host: isize = -1;
let mut i = 0usize;
while i < rc.len() {
match rc[i] {
'\t' | '\n' | '\r' => {
rc.remove(i);
continue;
}
' ' | '"' | '%' | '\'' | ';' | '<' | '>' | '\\' | '^' | '`' | '{' | '|' | '}' => {
if non_host == -1 {
non_host = i as isize;
}
}
'#' | '/' | '?' => {
if non_host == -1 {
non_host = i as isize;
}
host_end = i as isize;
}
'@' => {
at_sign = i as isize;
non_host = -1;
}
_ => {}
}
if host_end != -1 {
break;
}
i += 1;
}
let mut start = 0usize;
if at_sign != -1 {
u.auth = Some(super::url::percent_decode(
&rc[..at_sign as usize].iter().collect::<String>(),
));
start = at_sign as usize + 1;
}
if non_host == -1 {
u.host = Some(rc[start..].iter().collect());
rc = Vec::new();
} else {
u.host = Some(rc[start..non_host as usize].iter().collect());
rc = rc[non_host as usize..].to_vec();
}
parse_host(&mut u);
if u.hostname.is_none() {
u.hostname = Some(String::new());
}
let hostname = u.hostname.clone().unwrap_or_default();
let ipv6 = is_ipv6_hostname(&hostname);
if !ipv6 {
let rest_s: String = rc.iter().collect();
rc = get_hostname(&mut u, &rest_s, &hostname, url)?
.chars()
.collect();
}
let hn = u.hostname.clone().unwrap_or_default();
u.hostname = Some(if hn.chars().count() > 255 {
String::new()
} else {
hn.to_lowercase()
});
let hn = u.hostname.clone().unwrap_or_default();
if !hn.is_empty() {
if ipv6 {
if has_forbidden_host_char(&hn, true) {
return Err(invalid_url(url));
}
} else {
let ascii = super::punycode::to_ascii(&hn);
u.hostname = Some(ascii.clone());
if ascii.is_empty() || has_forbidden_host_char(&ascii, false) {
return Err(invalid_url(url));
}
}
}
let p = match &u.port {
Some(p) => std::format!(":{p}"),
None => String::new(),
};
let h = u.hostname.clone().unwrap_or_default();
u.host = Some(std::format!("{h}{p}"));
if ipv6 {
let hn = u.hostname.clone().unwrap_or_default();
let inner: String = {
let c: Vec<char> = hn.chars().collect();
if c.len() >= 2 {
c[1..c.len() - 1].iter().collect()
} else {
String::new()
}
};
u.hostname = Some(inner);
if rc.first() != Some(&'/') {
rc.insert(0, '/');
}
}
}
if !is_unsafe_protocol(&lower_proto) {
rc = auto_escape_str(&rc.iter().collect::<String>())
.chars()
.collect();
}
let mut question_idx: isize = -1;
let mut hash_idx: isize = -1;
for (i, &c) in rc.iter().enumerate() {
if c == '#' {
u.hash = Some(rc[i..].iter().collect());
hash_idx = i as isize;
break;
} else if c == '?' && question_idx == -1 {
question_idx = i as isize;
}
}
if question_idx != -1 {
let q = question_idx as usize;
if hash_idx == -1 {
u.search = Some(rc[q..].iter().collect());
set_query(&mut u, rc[q + 1..].iter().collect());
} else {
let h = hash_idx as usize;
u.search = Some(rc[q..h].iter().collect());
set_query(&mut u, rc[q + 1..h].iter().collect());
}
} else if parse_query_string {
u.search = None;
u.query = Some(Err(String::new()));
}
let use_question = question_idx != -1 && (hash_idx == -1 || question_idx < hash_idx);
let first_idx = if use_question { question_idx } else { hash_idx };
if first_idx == -1 {
if !rc.is_empty() {
u.pathname = Some(rc.iter().collect());
}
} else if first_idx > 0 {
u.pathname = Some(rc[..first_idx as usize].iter().collect());
}
if is_slashed_protocol(&lower_proto)
&& !u.hostname.as_deref().unwrap_or("").is_empty()
&& u.pathname.as_deref().unwrap_or("").is_empty()
{
u.pathname = Some("/".into());
}
if u.pathname.is_some() || u.search.is_some() {
let p = u.pathname.clone().unwrap_or_default();
let s = u.search.clone().unwrap_or_default();
u.path = Some(std::format!("{p}{s}"));
}
u.href = Some(format_url(&u, None));
Ok(u)
}
fn invalid_url(_url: &str) -> String {
"TypeError [ERR_INVALID_URL]: Invalid URL".into()
}
fn auth_needs_escape(c: char) -> bool {
!(c.is_ascii_alphanumeric()
|| matches!(
c,
'!' | '-' | '.' | '_' | '~' | '\'' | '(' | ')' | '*' | ':'
))
}
fn encode_auth(auth: &str) -> String {
let mut out = String::with_capacity(auth.len());
for c in auth.chars() {
if auth_needs_escape(c) {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).as_bytes() {
out.push_str(&std::format!("%{b:02X}"));
}
} else {
out.push(c);
}
}
out
}
pub fn format_url(u: &Url, query_string: Option<&str>) -> String {
let mut auth = u.auth.clone().unwrap_or_default();
if !auth.is_empty() {
auth = std::format!("{}@", encode_auth(&auth));
}
let mut protocol = u.protocol.clone().unwrap_or_default();
if !protocol.is_empty() && !protocol.ends_with(':') {
protocol.push(':');
}
let mut pathname = u.pathname.clone().unwrap_or_default();
let mut hash = u.hash.clone().unwrap_or_default();
let mut host = String::new();
if let Some(h) = u.host.as_ref().filter(|h| !h.is_empty()) {
host = std::format!("{auth}{h}");
} else if let Some(hn) = u.hostname.as_ref().filter(|h| !h.is_empty()) {
let bracketed = if hn.contains(':') && !is_ipv6_hostname(hn) {
std::format!("[{hn}]")
} else {
hn.clone()
};
host = std::format!("{auth}{bracketed}");
if let Some(p) = u.port.as_ref().filter(|p| !p.is_empty()) {
host.push(':');
host.push_str(p);
}
}
let query = query_string.unwrap_or("");
let mut search = u.search.clone().unwrap_or_default();
if search.is_empty() && !query.is_empty() {
search = std::format!("?{query}");
}
if pathname.contains('#') || pathname.contains('?') {
pathname = pathname
.chars()
.map(|c| match c {
'#' => "%23".to_string(),
'?' => "%3F".to_string(),
c => c.to_string(),
})
.collect();
}
if u.slashes == Some(true) || is_slashed_protocol(&protocol) {
if u.slashes == Some(true) || !host.is_empty() {
if !pathname.is_empty() && !pathname.starts_with('/') {
pathname = std::format!("/{pathname}");
}
host = std::format!("//{host}");
} else if protocol.starts_with("file") {
host = "//".into();
}
}
if search.contains('#') {
search = search.replace('#', "%23");
}
if !hash.is_empty() && !hash.starts_with('#') {
hash = std::format!("#{hash}");
}
if !search.is_empty() && !search.starts_with('?') {
search = std::format!("?{search}");
}
std::format!("{protocol}{host}{pathname}{search}{hash}")
}
pub fn to_js(u: &Url) -> Value {
let query_val: Option<Value> = u.query.as_ref().map(|q| match q {
Ok(raw) => with_host(|h| h.new_str(raw.clone())),
Err(raw) => {
let arg = with_host(|h| h.new_str(raw.clone()));
super::querystring::call("parse", &[arg])
.and_then(|r| r.ok())
.unwrap_or(Value::Undef)
}
});
with_host(|h| {
let mut m = IndexMap::new();
let opt = |h: &mut crate::host::JsHost, v: &Option<String>| match v {
Some(s) => h.new_str(s.clone()),
None => h.null(),
};
m.insert("protocol".into(), opt(h, &u.protocol));
m.insert(
"slashes".into(),
match u.slashes {
Some(b) => Value::Bool(b),
None => h.null(),
},
);
m.insert("auth".into(), opt(h, &u.auth));
m.insert("host".into(), opt(h, &u.host));
m.insert("port".into(), opt(h, &u.port));
m.insert("hostname".into(), opt(h, &u.hostname));
m.insert("hash".into(), opt(h, &u.hash));
m.insert("search".into(), opt(h, &u.search));
m.insert(
"query".into(),
query_val.clone().unwrap_or_else(|| h.null()),
);
m.insert("pathname".into(), opt(h, &u.pathname));
m.insert("path".into(), opt(h, &u.path));
m.insert("href".into(), opt(h, &u.href));
h.new_object(m)
})
}
fn from_js(v: &Value) -> (Url, Option<String>) {
let query_obj = with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get("query").cloned(),
_ => None,
});
let query_string = match &query_obj {
Some(q) if with_host(|h| matches!(h.get(q), Some(JsObj::Object(_)))) => {
super::querystring::call("stringify", std::slice::from_ref(q))
.and_then(|r| r.ok())
.map(|s| with_host(|h| h.str_of(&s)))
}
_ => None,
};
let get = |k: &str| {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => match p.get(k) {
None | Some(Value::Undef) => None,
Some(x) if h.is_null(x) => None,
Some(x) => Some(h.str_of(x)),
},
_ => None,
})
};
let slashes = with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get("slashes").map(|x| h.truthy(x)),
_ => None,
});
let u = Url {
protocol: get("protocol"),
slashes,
auth: get("auth"),
host: get("host"),
port: get("port"),
hostname: get("hostname"),
hash: get("hash"),
search: get("search"),
query: None,
pathname: get("pathname"),
path: get("path"),
href: get("href"),
};
(u, query_string)
}
pub fn format_value(v: &Value) -> Result<Value, String> {
if let Some(s) = with_host(|h| h.as_str(v)) {
let u = parse(&s, false, false)?;
let out = format_url(&u, None);
return Ok(with_host(|h| h.new_str(out)));
}
let href = with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) if p.get("@@native").is_some() => p.get("href").map(|x| h.str_of(x)),
_ => None,
});
if let Some(href) = href {
return Ok(with_host(|h| h.new_str(href)));
}
let is_obj = with_host(|h| matches!(h.get(v), Some(JsObj::Object(_))));
if !is_obj {
let received = super::received_desc(v);
return Err(std::format!(
"TypeError [ERR_INVALID_ARG_TYPE]: The \"urlObject\" argument must be \
one of type object or string. Received {received}"
));
}
let (u, qs) = from_js(v);
let out = format_url(&u, qs.as_deref());
Ok(with_host(|h| h.new_str(out)))
}
fn truthy(s: &Option<String>) -> bool {
s.as_deref().is_some_and(|s| !s.is_empty())
}
fn split_path(pathname: &Option<String>) -> Vec<String> {
match pathname.as_deref() {
Some(p) if !p.is_empty() => p.split('/').map(str::to_string).collect(),
_ => Vec::new(),
}
}
fn split_auth_in_host(result: &mut Url) {
let Some(host) = result.host.clone() else {
return;
};
if host.find('@').is_some_and(|i| i > 0) {
let mut parts = host.split('@');
result.auth = parts.next().map(str::to_string);
let rest = parts.next().map(str::to_string);
result.host = rest.clone();
result.hostname = rest;
}
}
fn set_request_path(result: &mut Url) {
if result.pathname.is_some() || result.search.is_some() {
result.path = Some(format!(
"{}{}",
result.pathname.clone().unwrap_or_default(),
result.search.clone().unwrap_or_default()
));
}
}
fn with_href(mut result: Url) -> Url {
result.href = Some(format_url(&result, None));
result
}
pub fn resolve_object(source: &Url, mut relative: Url) -> Url {
let mut result = source.clone();
result.hash = relative.hash.clone();
if relative.href.as_deref() == Some("") {
return with_href(result);
}
if relative.slashes == Some(true) && !truthy(&relative.protocol) {
let protocol = result.protocol.take();
result = Url {
protocol,
..relative
};
if result.protocol.as_deref().is_some_and(is_slashed_protocol)
&& truthy(&result.hostname)
&& !truthy(&result.pathname)
{
result.pathname = Some("/".into());
result.path = Some("/".into());
}
return with_href(result);
}
if truthy(&relative.protocol) && relative.protocol != result.protocol {
let rel_proto = relative.protocol.clone().unwrap_or_default();
if !is_slashed_protocol(&rel_proto) {
return with_href(relative);
}
result.protocol = relative.protocol.clone();
if !truthy(&relative.host)
&& rel_proto != "file"
&& rel_proto != "file:"
&& !is_hostless_protocol(&rel_proto)
{
let mut rel_path = relative
.pathname
.clone()
.unwrap_or_default()
.split('/')
.map(str::to_string)
.collect::<Vec<_>>();
while !rel_path.is_empty() {
let seg = rel_path.remove(0);
let found = !seg.is_empty();
relative.host = Some(seg);
if found {
break;
}
}
if !truthy(&relative.host) {
relative.host = Some(String::new());
}
if !truthy(&relative.hostname) {
relative.hostname = Some(String::new());
}
if rel_path.first().map(String::as_str) != Some("") {
rel_path.insert(0, String::new());
}
if rel_path.len() < 2 {
rel_path.insert(0, String::new());
}
result.pathname = Some(rel_path.join("/"));
} else {
result.pathname = relative.pathname.clone();
}
result.search = relative.search.clone();
result.query = relative.query.clone();
result.host = Some(relative.host.clone().unwrap_or_default());
result.auth = relative.auth.clone();
result.hostname = if truthy(&relative.hostname) {
relative.hostname.clone()
} else {
relative.host.clone()
};
result.port = relative.port.clone();
if truthy(&result.pathname) || truthy(&result.search) {
result.path = Some(format!(
"{}{}",
result.pathname.clone().unwrap_or_default(),
result.search.clone().unwrap_or_default()
));
}
if result.slashes != Some(true) {
result.slashes = relative.slashes;
}
return with_href(result);
}
let is_source_abs = result
.pathname
.as_deref()
.is_some_and(|p| p.starts_with('/'));
let is_rel_abs = truthy(&relative.host)
|| relative
.pathname
.as_deref()
.is_some_and(|p| p.starts_with('/'));
let mut must_end_abs =
is_rel_abs || is_source_abs || (truthy(&result.host) && truthy(&relative.pathname));
let remove_all_dots = must_end_abs;
let mut src_path = split_path(&result.pathname);
let mut rel_path = split_path(&relative.pathname);
let no_leading_slashes = result
.protocol
.as_deref()
.is_some_and(|p| !p.is_empty() && !is_slashed_protocol(p));
if no_leading_slashes {
result.hostname = Some(String::new());
result.port = None;
if let Some(host) = result.host.clone().filter(|h| !h.is_empty()) {
if src_path.first().map(String::as_str) == Some("") {
src_path[0] = host;
} else {
src_path.insert(0, host);
}
}
result.host = Some(String::new());
if truthy(&relative.protocol) {
relative.hostname = None;
relative.port = None;
result.auth = None;
if let Some(host) = relative.host.clone().filter(|h| !h.is_empty()) {
if rel_path.first().map(String::as_str) == Some("") {
rel_path[0] = host;
} else {
rel_path.insert(0, host);
}
}
relative.host = None;
}
must_end_abs = must_end_abs
&& (rel_path.first().map(String::as_str) == Some("")
|| src_path.first().map(String::as_str) == Some(""));
}
if is_rel_abs {
if relative.host.is_some() {
if result.host != relative.host {
result.auth = None;
}
result.host = relative.host.clone();
result.port = relative.port.clone();
}
if relative.hostname.is_some() {
if result.hostname != relative.hostname {
result.auth = None;
}
result.hostname = relative.hostname.clone();
}
result.search = relative.search.clone();
result.query = relative.query.clone();
src_path = rel_path;
} else if !rel_path.is_empty() {
src_path.pop();
src_path.extend(rel_path);
result.search = relative.search.clone();
result.query = relative.query.clone();
} else if relative.search.is_some() {
if no_leading_slashes {
let host = (!src_path.is_empty()).then(|| src_path.remove(0));
result.host = host.clone();
result.hostname = host;
split_auth_in_host(&mut result);
}
result.search = relative.search.clone();
result.query = relative.query.clone();
set_request_path(&mut result);
return with_href(result);
}
if src_path.is_empty() {
result.pathname = None;
result.path = result
.search
.as_deref()
.filter(|s| !s.is_empty())
.map(|s| format!("/{s}"));
return with_href(result);
}
let last = src_path.last().cloned().unwrap_or_default();
let has_trailing_slash =
((truthy(&result.host) || truthy(&relative.host) || src_path.len() > 1)
&& (last == "." || last == ".."))
|| last.is_empty();
let mut up = 0usize;
let mut i = src_path.len();
while i > 0 {
i -= 1;
if src_path[i] == "." {
src_path.remove(i);
} else if src_path[i] == ".." {
src_path.remove(i);
up += 1;
} else if up > 0 {
src_path.remove(i);
up -= 1;
}
}
if !must_end_abs && !remove_all_dots {
for _ in 0..up {
src_path.insert(0, "..".into());
}
}
let first_is_rooted = |p: &[String]| {
p.first()
.is_some_and(|s| s.is_empty() || s.starts_with('/'))
};
if must_end_abs && !first_is_rooted(&src_path) {
src_path.insert(0, String::new());
}
if has_trailing_slash && !src_path.join("/").ends_with('/') {
src_path.push(String::new());
}
let is_absolute = first_is_rooted(&src_path);
if no_leading_slashes {
let host = if is_absolute || src_path.is_empty() {
String::new()
} else {
src_path.remove(0)
};
result.host = Some(host.clone());
result.hostname = Some(host);
split_auth_in_host(&mut result);
}
must_end_abs = must_end_abs || (truthy(&result.host) && !src_path.is_empty());
if must_end_abs && !is_absolute {
src_path.insert(0, String::new());
}
if src_path.is_empty() {
result.pathname = None;
result.path = None;
} else {
result.pathname = Some(src_path.join("/"));
}
set_request_path(&mut result);
if truthy(&relative.auth) {
result.auth = relative.auth.clone();
}
if result.slashes != Some(true) {
result.slashes = relative.slashes;
}
with_href(result)
}