use super::arg_str;
use crate::host::{with_host, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
pub const MODULE_METHODS: &[&str] = &[
"parse",
"format",
"fileURLToPath",
"fileURLToPathBuffer",
"pathToFileURL",
"domainToASCII",
"domainToUnicode",
"urlToHttpOptions",
"resolve",
"resolveObject",
];
pub const COMPONENTS: &[&str] = &[
"protocol", "username", "password", "host", "hostname", "port", "pathname", "search", "hash",
"href",
];
pub fn is_component(name: &str) -> bool {
COMPONENTS.contains(&name)
}
fn recompute(url: &Value, sync_params: bool) {
let read = |k: &str| {
with_host(|h| match h.get(url) {
Some(JsObj::Object(p)) => p.get(k).map(|v| h.str_of(v)).unwrap_or_default(),
_ => String::new(),
})
};
let mut protocol = read("@@protocol");
if !protocol.is_empty() && !protocol.ends_with(':') {
protocol.push(':');
}
let delimited = |s: String, lead: char| {
if s.is_empty() || s.starts_with(lead) {
s
} else {
format!("{lead}{s}")
}
};
let parts = Parts {
protocol,
username: read("@@username"),
password: read("@@password"),
hostname: read("@@hostname"),
port: read("@@port"),
pathname: read("@@pathname"),
search: delimited(read("@@search"), '?'),
hash: delimited(read("@@hash"), '#'),
};
let (href, host, origin) = (parts.href(), parts.host(), parts.origin());
let search = parts.search.clone();
if sync_params {
let query = search.strip_prefix('?').unwrap_or(&search).to_string();
let params = with_host(|h| match h.get(url) {
Some(JsObj::Object(p)) => p.get("@@searchParams").cloned(),
_ => None,
});
if let Some(params) = params {
write_pairs(¶ms, &parse_query(&query));
}
}
with_host(|h| {
let vals = [
("@@href", h.new_str(href)),
("@@host", h.new_str(host)),
("@@origin", h.new_str(origin)),
("@@protocol", h.new_str(parts.protocol.clone())),
("@@search", h.new_str(search)),
("@@hash", h.new_str(parts.hash.clone())),
];
if let Some(JsObj::Object(p)) = h.get_mut(url) {
for (k, v) in vals {
p.insert(k.to_string(), v);
}
}
});
}
pub fn refresh(url: &Value) {
recompute(url, true);
}
pub fn split_host(url: &Value) {
let host = with_host(|h| match h.get(url) {
Some(JsObj::Object(p)) => p.get("@@host").map(|v| h.str_of(v)).unwrap_or_default(),
_ => String::new(),
});
let split = match host.rfind(']') {
Some(i) => host[i..].find(':').map(|j| i + j),
None => host.rfind(':'),
};
let (hostname, port) = match split {
Some(i) => (host[..i].to_string(), host[i + 1..].to_string()),
None => (host.clone(), String::new()),
};
with_host(|h| {
let (hn, pt) = (h.new_str(hostname), h.new_str(port));
if let Some(JsObj::Object(p)) = h.get_mut(url) {
p.insert("@@hostname".into(), hn);
p.insert("@@port".into(), pt);
}
});
refresh(url);
}
pub fn reparse(url: &Value) {
let href = with_host(|h| match h.get(url) {
Some(JsObj::Object(p)) => p.get("@@href").map(|v| h.str_of(v)).unwrap_or_default(),
_ => String::new(),
});
let Some(parts) = parse_absolute(&href) else {
return;
};
let fresh = build(&parts);
let props = with_host(|h| match h.get(&fresh) {
Some(JsObj::Object(p)) => p.clone(),
_ => IndexMap::new(),
});
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(url) {
for (k, v) in props {
p.insert(k, v);
}
}
});
}
struct Parts {
protocol: String,
username: String,
password: String,
hostname: String,
port: String,
pathname: String,
search: String,
hash: String,
}
impl Parts {
fn host(&self) -> String {
if self.port.is_empty() {
self.hostname.clone()
} else {
format!("{}:{}", self.hostname, self.port)
}
}
fn origin(&self) -> String {
let scheme = self.protocol.strip_suffix(':').unwrap_or(&self.protocol);
if self.hostname.is_empty() || special_port(scheme).is_none() {
"null".into()
} else {
format!("{}//{}", self.protocol, self.host())
}
}
fn href(&self) -> String {
let auth = if self.username.is_empty() {
String::new()
} else if self.password.is_empty() {
format!("{}@", self.username)
} else {
format!("{}:{}@", self.username, self.password)
};
format!(
"{}//{auth}{}{}{}{}",
self.protocol,
self.host(),
self.pathname,
self.search,
self.hash
)
}
}
fn special_port(scheme: &str) -> Option<&'static str> {
match scheme {
"http" | "ws" => Some("80"),
"https" | "wss" => Some("443"),
"ftp" => Some("21"),
_ => None,
}
}
fn parse_absolute(input: &str) -> Option<Parts> {
let stripped: String;
let input = if input.contains(['\t', '\n', '\r']) {
stripped = input.replace(['\t', '\n', '\r'], "");
stripped.as_str()
} else {
input
};
let (scheme, rest) = input.split_once("://")?;
let backslashed: String;
let rest = if special_port(&scheme.to_ascii_lowercase()).is_some() && rest.contains('\\') {
let cut = rest.find(['?', '#']).unwrap_or(rest.len());
backslashed = format!("{}{}", rest[..cut].replace('\\', "/"), &rest[cut..]);
backslashed.as_str()
} else {
rest
};
if scheme.is_empty()
|| !scheme
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'))
{
return None;
}
let rest = if special_port(&scheme.to_ascii_lowercase()).is_some() {
rest.trim_start_matches('/')
} else {
rest
};
let auth_end = rest.find(['/', '?', '#']).unwrap_or(rest.len());
let authority = &rest[..auth_end];
let mut tail = &rest[auth_end..];
let (userinfo, hostport) = match authority.rsplit_once('@') {
Some((u, h)) => (u, h),
None => ("", authority),
};
let (username, password) = match userinfo.split_once(':') {
Some((u, p)) => (u.to_string(), p.to_string()),
None => (userinfo.to_string(), String::new()),
};
let (hostname, port) = if hostport.starts_with('[') {
let close = hostport.find(']')?;
match &hostport[close + 1..] {
"" => (&hostport[..=close], ""),
p => (&hostport[..=close], p.strip_prefix(':')?),
}
} else {
hostport.split_once(':').unwrap_or((hostport, ""))
};
let lower_scheme = scheme.to_ascii_lowercase();
let special = special_port(&lower_scheme).is_some();
let hostname = if special {
if hostname.is_empty() {
return None;
}
url::Host::parse(hostname).ok()?.to_string()
} else if hostname.is_empty() {
String::new()
} else {
url::Host::parse_opaque(hostname).ok()?.to_string()
};
let port = if port.is_empty() {
String::new()
} else if port.bytes().all(|b| b.is_ascii_digit()) {
port.trim_start_matches('0').parse::<u16>().map_or_else(
|_| if port.bytes().all(|b| b == b'0') { Some("0".to_string()) } else { None },
|n| Some(n.to_string()),
)?
} else {
return None;
};
let hash = match tail.find('#') {
Some(i) => {
let h = tail[i..].to_string();
tail = &tail[..i];
h
}
None => String::new(),
};
let search = match tail.find('?') {
Some(i) => {
let s = tail[i..].to_string();
tail = &tail[..i];
s
}
None => String::new(),
};
let scheme = scheme.to_ascii_lowercase();
let default_port = special_port(&scheme);
let pathname = if tail.is_empty() {
"/".to_string()
} else {
normalize_path(tail)
};
let port = if default_port == Some(port.as_str()) {
String::new()
} else {
port
};
Some(Parts {
protocol: format!("{scheme}:"),
username,
password,
hostname,
port,
pathname,
search,
hash,
})
}
fn normalize_path(path: &str) -> String {
if !path.contains('.') {
return path.to_string();
}
let rooted = path.starts_with('/');
let mut out: Vec<&str> = Vec::new();
let mut trailing_slash = false;
for seg in path.split('/') {
match seg {
"." => trailing_slash = true,
".." => {
out.pop();
trailing_slash = true;
}
_ => {
out.push(seg);
trailing_slash = false;
}
}
}
if rooted && out.first() != Some(&"") {
out.insert(0, "");
}
let mut joined = out.join("/");
if trailing_slash && !joined.ends_with('/') {
joined.push('/');
}
if joined.is_empty() {
joined.push('/');
}
joined
}
pub fn construct(args: &[Value]) -> Result<Value, String> {
let to_str = |v: &Value| {
crate::host::to_string_value(v).map(|s| crate::host::with_host(|h| h.str_of(&s)))
};
let input = match args.first() {
Some(v) => to_str(v)?,
None => "undefined".to_string(),
};
let base = match args.get(1) {
Some(Value::Undef) | None => None,
Some(v) => Some(to_str(v)?),
};
let parts = parse_absolute(&input)
.or_else(|| {
if let Some(base) = &base {
parse_absolute(base).map(|mut b| {
let mut rest = input.as_str();
let hash = match rest.find('#') {
Some(i) => {
let h = rest[i..].to_string();
rest = &rest[..i];
h
}
None => String::new(),
};
let search = match rest.find('?') {
Some(i) => {
let q = rest[i..].to_string();
rest = &rest[..i];
q
}
None => String::new(),
};
let merged = if rest.starts_with('/') {
rest.to_string()
} else if rest.is_empty() {
b.pathname.clone()
} else {
let dir = match b.pathname.rfind('/') {
Some(i) => &b.pathname[..=i],
None => "/",
};
format!("{dir}{rest}")
};
b.pathname = normalize_path(&merged);
b.search = search;
b.hash = hash;
b
})
} else {
None
}
})
.ok_or_else(|| {
let mut fields = vec![("input", input.as_str())];
if let Some(b) = &base {
fields.push(("base", b.as_str()));
}
crate::host::plain_coded_error_with("TypeError", "ERR_INVALID_URL", "Invalid URL", &fields)
})?;
Ok(build(&parts))
}
fn percent_encode(s: &str, extra: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'%' && i + 2 < bytes.len() + 1 {
let hex = bytes.get(i + 1..i + 3);
if hex.is_some_and(|h| h.iter().all(|c| c.is_ascii_hexdigit())) {
out.push('%');
out.push(bytes[i + 1] as char);
out.push(bytes[i + 2] as char);
i += 3;
continue;
}
}
if b < 0x20 || b == 0x7f || b >= 0x80 || extra.as_bytes().contains(&b) {
out.push_str(&format!("%{b:02X}"));
} else {
out.push(b as char);
}
i += 1;
}
out
}
const PATH_SET: &str = " \"<>^`{}";
const QUERY_SET: &str = " \"'<>";
const FRAGMENT_SET: &str = " \"<>`";
const USERINFO_SET: &str = " \";<=>@[]^`{|}";
fn build(p: &Parts) -> Value {
let p = &Parts {
protocol: p.protocol.clone(),
username: percent_encode(&p.username, USERINFO_SET),
password: percent_encode(&p.password, USERINFO_SET),
hostname: p.hostname.clone(),
port: p.port.clone(),
pathname: percent_encode(&p.pathname, PATH_SET),
search: percent_encode(&p.search, QUERY_SET),
hash: percent_encode(&p.hash, FRAGMENT_SET),
};
let query = p.search.strip_prefix('?').unwrap_or(&p.search);
let search_params = make_search_params(&parse_query(query));
with_host(|h| {
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("URL"));
m.insert("@@href".into(), h.new_str(p.href()));
m.insert("@@origin".into(), h.new_str(p.origin()));
m.insert("@@protocol".into(), h.new_str(p.protocol.clone()));
m.insert("@@username".into(), h.new_str(p.username.clone()));
m.insert("@@password".into(), h.new_str(p.password.clone()));
m.insert("@@host".into(), h.new_str(p.host()));
m.insert("@@hostname".into(), h.new_str(p.hostname.clone()));
m.insert("@@port".into(), h.new_str(p.port.clone()));
m.insert("@@pathname".into(), h.new_str(p.pathname.clone()));
m.insert("@@search".into(), h.new_str(p.search.clone()));
m.insert("@@searchParams".into(), search_params.clone());
m.insert("@@hash".into(), h.new_str(p.hash.clone()));
let obj = h.new_object(m);
if let Some(JsObj::Object(sp)) = h.get_mut(&search_params) {
sp.insert("@@ownerUrl".into(), obj.clone());
}
obj
})
}
pub const STATIC_METHODS: &[&str] = &["canParse", "parse"];
pub fn static_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
let parsed = construct(args);
Some(match method {
"canParse" => Ok(Value::Bool(parsed.is_ok())),
"parse" => Ok(parsed.unwrap_or_else(|_| with_host(|h| h.null()))),
_ => return None,
})
}
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"parse" => legacy_parse(args).map(|u| super::url_legacy::to_js(&u)),
"format" => super::url_legacy::format_value(&args.first().cloned().unwrap_or(Value::Undef)),
"fileURLToPath" => file_url_to_path(args).map(|s| with_host(|h| h.new_str(s))),
"fileURLToPathBuffer" => {
file_url_to_path(args).map(|s| super::buffer::from_bytes(s.as_bytes()))
}
"pathToFileURL" => Ok(path_to_file_url(&arg_str(args, 0))),
"domainToASCII" => Ok(punycode_domain(args, true)),
"domainToUnicode" => Ok(punycode_domain(args, false)),
"urlToHttpOptions" => Ok(url_to_http_options(
&args.first().cloned().unwrap_or(Value::Undef),
)),
"resolve" => legacy_resolve_object(args)
.map(|u| with_host(|h| h.new_str(u.href.unwrap_or_default()))),
"resolveObject" => {
if !args.first().is_some_and(|v| with_host(|h| h.truthy(v))) {
return Some(Ok(args.get(1).cloned().unwrap_or(Value::Undef)));
}
legacy_resolve_object(args).map(|u| super::url_legacy::to_js(&u))
}
_ => return None,
})
}
fn legacy_parse(args: &[Value]) -> Result<super::url_legacy::Url, String> {
emit_url_parse_deprecation();
let input = arg_str(args, 0);
let truthy = |i: usize| {
args.get(i)
.map(|v| with_host(|h| h.truthy(v)))
.unwrap_or(false)
};
super::url_legacy::parse(&input, truthy(1), truthy(2))
}
fn emit_url_parse_deprecation() {
super::process::emit_deprecation_warning(
"DEP0169",
"`url.parse()` behavior is not standardized and prone to errors that \
have security implications. Use the WHATWG URL API instead. CVEs are \
not issued for `url.parse()` vulnerabilities.",
);
}
fn legacy_resolve_object(args: &[Value]) -> Result<super::url_legacy::Url, String> {
emit_url_parse_deprecation();
let source = super::url_legacy::parse(&arg_str(args, 0), false, true)?;
let relative = super::url_legacy::parse(&arg_str(args, 1), false, true)?;
Ok(super::url_legacy::resolve_object(&source, relative))
}
pub fn instance_call(recv: &Value, method: &str, _args: &[Value]) -> Result<Value, String> {
match method {
"toString" | "toJSON" => Ok(with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@href").cloned().unwrap_or(Value::Undef),
_ => Value::Undef,
})),
_ => Err(crate::host::type_error(&format!(
"url.{method} is not a function"
))),
}
}
fn url_href(v: &Value) -> String {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => match p.get("@@native").map(|x| h.str_of(x)).as_deref() {
Some("URL") => p.get("@@href").map(|x| h.str_of(x)).unwrap_or_default(),
_ => h.str_of(v),
},
_ => h.str_of(v),
})
}
fn file_url_to_path(args: &[Value]) -> Result<String, String> {
let v = args.first().cloned().unwrap_or(Value::Undef);
let href = url_href(&v);
let rest = href.strip_prefix("file://").ok_or_else(|| {
crate::host::plain_coded_error(
"TypeError",
"ERR_INVALID_URL_SCHEME",
"The URL must be of scheme file",
)
})?;
let path = match rest.find('/') {
Some(0) => rest,
Some(i) => &rest[i..],
None => "/",
};
Ok(percent_decode(path))
}
fn path_to_file_url(path: &str) -> Value {
let enc = encode_path_component(path);
let pathname = if enc.starts_with('/') {
enc
} else {
format!("/{enc}")
};
let parts = Parts {
protocol: "file:".into(),
username: String::new(),
password: String::new(),
hostname: String::new(),
port: String::new(),
pathname,
search: String::new(),
hash: String::new(),
};
build(&parts)
}
fn punycode_domain(args: &[Value], ascii: bool) -> Value {
let method = if ascii { "toASCII" } else { "toUnicode" };
match super::punycode::call(method, args) {
Some(Ok(v)) => v,
_ => with_host(|h| h.new_str("")),
}
}
fn url_to_http_options(v: &Value) -> Value {
let get = |key: &str| -> String {
with_host(|h| match h.get(v) {
Some(JsObj::Object(p)) => p.get(key).map(|x| h.str_of(x)).unwrap_or_default(),
_ => String::new(),
})
};
let protocol = get("@@protocol");
let mut hostname = get("@@hostname");
if hostname.starts_with('[') && hostname.ends_with(']') && hostname.len() >= 2 {
hostname = hostname[1..hostname.len() - 1].to_string();
}
let hash = get("@@hash");
let search = get("@@search");
let pathname = get("@@pathname");
let href = get("@@href");
let port = get("@@port");
let username = get("@@username");
let password = get("@@password");
let path = format!("{pathname}{search}");
let auth = if username.is_empty() && password.is_empty() {
None
} else {
Some(format!(
"{}:{}",
percent_decode(&username),
percent_decode(&password)
))
};
let port_num = if port.is_empty() {
None
} else {
port.parse::<f64>().ok()
};
with_host(|h| {
let mut m = IndexMap::new();
m.insert("protocol".into(), h.new_str(protocol));
m.insert("hostname".into(), h.new_str(hostname));
m.insert("hash".into(), h.new_str(hash));
m.insert("search".into(), h.new_str(search));
m.insert("pathname".into(), h.new_str(pathname));
m.insert("path".into(), h.new_str(path));
m.insert("href".into(), h.new_str(href));
if let Some(n) = port_num {
m.insert("port".into(), Value::Float(n));
}
if let Some(a) = auth {
m.insert("auth".into(), h.new_str(a));
}
h.new_object(m)
})
}
pub(crate) fn percent_decode(s: &str) -> String {
let b = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(b.len());
let mut i = 0;
while i < b.len() {
if b[i] == b'%' && i + 2 < b.len() {
if let (Some(hi), Some(lo)) = (hex_val(b[i + 1]), hex_val(b[i + 2])) {
out.push((hi << 4) | lo);
i += 3;
continue;
}
}
out.push(b[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn encode_path_component(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
let keep = b.is_ascii_alphanumeric()
|| matches!(
b,
b'/' | b'-'
| b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b':'
| b'@'
);
if keep {
out.push(b as char);
} else {
out.push('%');
out.push(hex_upper(b >> 4));
out.push(hex_upper(b & 0x0f));
}
}
out
}
pub const SEARCH_PARAMS_METHODS: &[&str] = &[
"get",
"getAll",
"has",
"set",
"append",
"delete",
"keys",
"values",
"entries",
"forEach",
"toString",
"sort",
"@@iterator",
];
fn make_search_params(pairs: &[(String, String)]) -> Value {
with_host(|h| {
let items: Vec<Value> = pairs
.iter()
.map(|(k, v)| {
let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
h.new_array(kv)
})
.collect();
let arr = h.new_array(items);
let mut m = IndexMap::new();
m.insert("@@native".into(), h.new_str("URLSearchParams"));
m.insert("@@pairs".into(), arr);
m.insert("size".into(), Value::Float(pairs.len() as f64));
let obj = h.new_object(m);
h.hide_prop(&obj, "size");
obj
})
}
fn encode_query(pairs: &[(String, String)]) -> String {
pairs
.iter()
.map(|(k, v)| format!("{}={}", form_encode(k), form_encode(v)))
.collect::<Vec<_>>()
.join("&")
}
fn pairs_of(recv: &Value) -> Vec<(String, String)> {
with_host(|h| {
let items: Vec<Value> = match h.get(recv) {
Some(JsObj::Object(p)) => match p.get("@@pairs").and_then(|a| h.get(a)) {
Some(JsObj::Array(items)) => items.clone(),
_ => Vec::new(),
},
_ => Vec::new(),
};
items
.iter()
.map(|it| match h.get(it) {
Some(JsObj::Array(kv)) => {
let kv = kv.clone();
let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
let v = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
(k, v)
}
_ => (h.str_of(it), String::new()),
})
.collect()
})
}
fn set_pairs(recv: &Value, pairs: &[(String, String)]) {
write_pairs(recv, pairs);
let owner = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@ownerUrl").cloned(),
_ => None,
});
if let Some(owner) = owner {
let query = encode_query(pairs);
with_host(|h| {
let s = h.new_str(if query.is_empty() {
String::new()
} else {
format!("?{query}")
});
if let Some(JsObj::Object(p)) = h.get_mut(&owner) {
p.insert("@@search".into(), s);
}
});
recompute(&owner, false);
}
}
fn write_pairs(recv: &Value, pairs: &[(String, String)]) {
with_host(|h| {
let items: Vec<Value> = pairs
.iter()
.map(|(k, v)| {
let kv = vec![h.new_str(k.clone()), h.new_str(v.clone())];
h.new_array(kv)
})
.collect();
let arr = h.new_array(items);
let n = Value::Float(pairs.len() as f64);
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert("@@pairs".into(), arr);
p.insert("size".into(), n);
}
h.hide_prop(recv, "size");
});
}
pub fn construct_search_params(args: &[Value]) -> Result<Value, String> {
let pairs = match args.first() {
None => Vec::new(),
Some(v) if matches!(v, Value::Undef) || with_host(|h| h.is_null(v)) => Vec::new(),
Some(v) => pairs_from_init(v),
};
Ok(make_search_params(&pairs))
}
fn pairs_from_init(v: &Value) -> Vec<(String, String)> {
if super::native_tag(v).as_deref() == Some("URLSearchParams") {
return pairs_of(v);
}
if let Some(s) = with_host(|h| h.as_str(v)) {
return parse_query(s.strip_prefix('?').unwrap_or(&s));
}
with_host(|h| match h.get(v) {
Some(JsObj::Array(items)) => {
let items = items.clone();
items
.iter()
.map(|it| match h.get(it) {
Some(JsObj::Array(kv)) => {
let kv = kv.clone();
let k = kv.first().map(|x| h.str_of(x)).unwrap_or_default();
let val = kv.get(1).map(|x| h.str_of(x)).unwrap_or_default();
(k, val)
}
_ => (h.str_of(it), String::new()),
})
.collect()
}
Some(JsObj::Object(p)) => {
let entries: Vec<(String, Value)> = p
.iter()
.filter(|(k, _)| !k.starts_with("@@"))
.map(|(k, val)| (k.clone(), val.clone()))
.collect();
entries
.into_iter()
.map(|(k, val)| (k, h.str_of(&val)))
.collect()
}
_ => Vec::new(),
})
}
pub fn search_params_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
match method {
"get" => {
let name = arg_str(args, 0);
match pairs_of(recv).into_iter().find(|(k, _)| *k == name) {
Some((_, v)) => Ok(with_host(|h| h.new_str(v))),
None => Ok(with_host(|h| h.null())),
}
}
"getAll" => {
let name = arg_str(args, 0);
let vals: Vec<String> = pairs_of(recv)
.into_iter()
.filter(|(k, _)| *k == name)
.map(|(_, v)| v)
.collect();
Ok(with_host(|h| {
let items = vals.into_iter().map(|v| h.new_str(v)).collect();
h.new_array(items)
}))
}
"has" => {
let name = arg_str(args, 0);
let pairs = pairs_of(recv);
let found = if args.len() > 1 {
let val = arg_str(args, 1);
pairs.iter().any(|(k, v)| *k == name && *v == val)
} else {
pairs.iter().any(|(k, _)| *k == name)
};
Ok(Value::Bool(found))
}
"append" => {
let mut pairs = pairs_of(recv);
pairs.push((arg_str(args, 0), arg_str(args, 1)));
set_pairs(recv, &pairs);
Ok(Value::Undef)
}
"set" => {
let name = arg_str(args, 0);
let val = arg_str(args, 1);
let mut pairs = pairs_of(recv);
let mut seen = false;
pairs.retain_mut(|(k, v)| {
if *k == name {
if seen {
false
} else {
*v = val.clone();
seen = true;
true
}
} else {
true
}
});
if !seen {
pairs.push((name, val));
}
set_pairs(recv, &pairs);
Ok(Value::Undef)
}
"delete" => {
let name = arg_str(args, 0);
let mut pairs = pairs_of(recv);
if args.len() > 1 {
let val = arg_str(args, 1);
pairs.retain(|(k, v)| !(*k == name && *v == val));
} else {
pairs.retain(|(k, _)| *k != name);
}
set_pairs(recv, &pairs);
Ok(Value::Undef)
}
"sort" => {
let mut pairs = pairs_of(recv);
pairs.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
set_pairs(recv, &pairs);
Ok(Value::Undef)
}
"toString" => {
let s = encode_query(&pairs_of(recv));
Ok(with_host(|h| h.new_str(s)))
}
"keys" => {
let pairs = pairs_of(recv);
Ok(with_host(|h| {
let items = pairs.into_iter().map(|(k, _)| h.new_str(k)).collect();
h.alloc(JsObj::Iter { items, idx: 0 })
}))
}
"values" => {
let pairs = pairs_of(recv);
Ok(with_host(|h| {
let items = pairs.into_iter().map(|(_, v)| h.new_str(v)).collect();
h.alloc(JsObj::Iter { items, idx: 0 })
}))
}
"entries" | "@@iterator" => {
let pairs = pairs_of(recv);
Ok(with_host(|h| {
let items = pairs
.into_iter()
.map(|(k, v)| {
let kv = vec![h.new_str(k), h.new_str(v)];
h.new_array(kv)
})
.collect();
h.alloc(JsObj::Iter { items, idx: 0 })
}))
}
"forEach" => {
let cb = args.first().cloned().unwrap_or(Value::Undef);
let this_arg = args.get(1).cloned();
for (k, v) in pairs_of(recv) {
let (value, name) = with_host(|h| (h.new_str(v), h.new_str(k)));
crate::host::invoke(&cb, vec![value, name, recv.clone()], this_arg.clone())?;
}
Ok(Value::Undef)
}
_ => Err(crate::host::type_error(&format!(
"urlSearchParams.{method} is not a function"
))),
}
}
fn parse_query(q: &str) -> Vec<(String, String)> {
q.split('&')
.filter(|s| !s.is_empty())
.map(|seg| match seg.split_once('=') {
Some((k, v)) => (form_decode(k), form_decode(v)),
None => (form_decode(seg), String::new()),
})
.collect()
}
fn form_decode(s: &str) -> String {
let b = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(b.len());
let mut i = 0;
while i < b.len() {
match b[i] {
b'+' => {
out.push(b' ');
i += 1;
}
b'%' if i + 2 < b.len() => match (hex_val(b[i + 1]), hex_val(b[i + 2])) {
(Some(hi), Some(lo)) => {
out.push((hi << 4) | lo);
i += 3;
}
_ => {
out.push(b'%');
i += 1;
}
},
c => {
out.push(c);
i += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
fn form_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
match b {
b' ' => out.push('+'),
b'*' | b'-' | b'.' | b'_' => out.push(b as char),
_ if b.is_ascii_alphanumeric() => out.push(b as char),
_ => {
out.push('%');
out.push(hex_upper(b >> 4));
out.push(hex_upper(b & 0x0f));
}
}
}
out
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
fn hex_upper(n: u8) -> char {
char::from_digit(n as u32, 16).unwrap().to_ascii_uppercase()
}