use ::std::ptr::NonNull;
use bun_core::ZBox;
use mozjs::conversions::unsafe_jsstr_to_string;
use mozjs::glue::JS_GetReservedSlot;
use mozjs::jsapi::*;
use mozjs::jsval::{
BooleanValue, Int32Value, JSVal, ObjectValue, PrivateValue, StringValue, UndefinedValue,
};
use mozjs::rooted;
use mozjs::rust::IdVector;
use mozjs::rust::wrappers2 as w2;
use crate::require::cache_builtin;
use bun_url::{PercentEncoding, URL as BunUrl};
struct UrlState {
href: String,
protocol: String,
username: String,
password: String,
host: String,
hostname: String,
port: String,
pathname: String,
search: String,
hash: String,
origin: String,
}
fn parse_url(input: &str, base: Option<&str>) -> Option<UrlState> {
let input = input.trim();
if input.is_empty() {
return None;
}
if input.starts_with("data:") || input.starts_with("blob:") {
return Some(UrlState {
href: input.to_string(),
protocol: input.split(':').next().unwrap_or("").to_string() + ":",
username: String::new(),
password: String::new(),
host: String::new(),
hostname: String::new(),
port: String::new(),
pathname: input.split_once(':').map(|x| x.1).unwrap_or("").to_string(),
search: String::new(),
hash: String::new(),
origin: "null".to_string(),
});
}
let url_to_parse: String = if input.starts_with("//") {
format!("http:{}", input)
} else if input.contains("://") {
input.to_string()
} else if let Some(base_str) = base {
let base_bytes = base_str.as_bytes();
let base_url = BunUrl::parse(base_bytes);
if base_url.protocol.is_empty() {
return None;
}
if input.starts_with("/") {
format!(
"{}://{}{}",
core::str::from_utf8(base_url.protocol).unwrap_or("http:"),
core::str::from_utf8(base_url.host).unwrap_or(""),
input
)
} else if input.starts_with("?") || input.starts_with("#") {
let base_path_raw = core::str::from_utf8(base_url.pathname).unwrap_or("/");
let base_path = base_path_raw
.split(['?', '#'])
.next()
.unwrap_or(base_path_raw);
format!(
"{}://{}{}{}",
core::str::from_utf8(base_url.protocol).unwrap_or("http:"),
core::str::from_utf8(base_url.host).unwrap_or(""),
base_path,
input
)
} else {
let base_path_raw = core::str::from_utf8(base_url.pathname).unwrap_or("/");
let base_path = base_path_raw
.split(['?', '#'])
.next()
.unwrap_or(base_path_raw);
let dir = if let Some(pos) = base_path.rfind('/') {
&base_path[..pos]
} else {
""
};
format!(
"{}://{}{}/{}",
core::str::from_utf8(base_url.protocol).unwrap_or("http:"),
core::str::from_utf8(base_url.host).unwrap_or(""),
dir,
input
)
}
} else {
return None;
};
let parsed = BunUrl::parse(url_to_parse.as_bytes());
let protocol_raw = core::str::from_utf8(parsed.protocol)
.unwrap_or("")
.to_string();
let protocol = if protocol_raw.is_empty() {
String::new()
} else if protocol_raw.ends_with(':') {
protocol_raw
} else {
format!("{}:", protocol_raw)
};
let hostname = core::str::from_utf8(parsed.hostname)
.unwrap_or("")
.to_string();
let port = core::str::from_utf8(parsed.port).unwrap_or("").to_string();
let host = if port.is_empty() {
hostname.clone()
} else {
format!("{}:{}", hostname, port)
};
let origin = if hostname.is_empty() {
"null".to_string()
} else {
format!("{}//{}", protocol, host)
};
Some(UrlState {
href: core::str::from_utf8(parsed.href).unwrap_or("").to_string(),
protocol,
username: core::str::from_utf8(parsed.username)
.unwrap_or("")
.to_string(),
password: core::str::from_utf8(parsed.password)
.unwrap_or("")
.to_string(),
host,
hostname,
port,
pathname: {
let raw = core::str::from_utf8(parsed.pathname).unwrap_or("/");
let bare = raw.split(['?', '#']).next().unwrap_or(raw);
bare.to_string()
},
search: {
let raw = core::str::from_utf8(parsed.search).unwrap_or("");
if raw.is_empty() {
String::new()
} else if raw.starts_with('?') {
raw.to_string()
} else {
format!("?{}", raw)
}
},
hash: {
let raw = core::str::from_utf8(parsed.hash).unwrap_or("");
let resolved = if raw.is_empty() {
let href = core::str::from_utf8(parsed.href).unwrap_or("");
if let Some(pos) = href.find('#') {
href[pos..].to_string()
} else {
String::new()
}
} else if raw.starts_with('#') {
raw.to_string()
} else {
format!("#{}", raw)
};
resolved
},
origin,
})
}
const SLOT_URL: u32 = 0;
const URL_CLASS: JSClass = JSClass {
name: c"URL".as_ptr(),
flags: (1 << JSCLASS_RESERVED_SLOTS_SHIFT) as u32,
cOps: ::std::ptr::null(),
spec: ::std::ptr::null(),
ext: ::std::ptr::null(),
oOps: ::std::ptr::null(),
};
const URL_SEARCH_PARAMS_CLASS: JSClass = JSClass {
name: c"URLSearchParams".as_ptr(),
flags: (1 << JSCLASS_RESERVED_SLOTS_SHIFT) as u32,
cOps: ::std::ptr::null(),
spec: ::std::ptr::null(),
ext: ::std::ptr::null(),
oOps: ::std::ptr::null(),
};
unsafe fn get_url_state(obj: *mut JSObject) -> Option<UrlState> {
unsafe {
let mut slot = UndefinedValue();
JS_GetReservedSlot(obj, SLOT_URL, &mut slot);
if slot.is_double() && (slot.asBits_ & 0xFFFF000000000000) == 0 {
let ptr = slot.to_private() as *mut UrlState;
if !ptr.is_null() {
return Some(*Box::from_raw(ptr));
}
}
None
}
}
fn set_url_state(obj: *mut JSObject, state: UrlState) {
unsafe {
let boxed = Box::new(state);
let val = PrivateValue(Box::into_raw(boxed) as *const ::std::os::raw::c_void);
JS_SetReservedSlot(obj, SLOT_URL, &val);
}
}
#[allow(dead_code)]
unsafe fn set_string_prop(cx: *mut JSContext, obj: *mut JSObject, name: &str, value: &str) {
unsafe {
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let c_name = ZBox::from_bytes(name.as_bytes());
let js_str = JS_NewStringCopyN(
cx,
value.as_ptr() as *const ::std::os::raw::c_char,
value.len(),
);
if !js_str.is_null() {
let val = StringValue(&*js_str);
rooted!(&in(cx_ref) let obj_r = obj);
rooted!(&in(cx_ref) let v = val);
JS_DefineProperty(
cx,
obj_r.handle().into(),
c_name.as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
}
fn url_state_get_field(state: &UrlState, field: &str) -> String {
match field {
"href" => state.href.clone(),
"protocol" => state.protocol.clone(),
"username" => state.username.clone(),
"password" => state.password.clone(),
"host" => state.host.clone(),
"hostname" => state.hostname.clone(),
"port" => state.port.clone(),
"pathname" => state.pathname.clone(),
"search" => state.search.clone(),
"hash" => state.hash.clone(),
"origin" => state.origin.clone(),
_ => String::new(),
}
}
fn rebuild_href(state: &UrlState, field: &str, new_val: &str) -> String {
let protocol = if field == "protocol" {
new_val
} else {
&state.protocol
};
let username = if field == "username" {
new_val
} else {
&state.username
};
let password = if field == "password" {
new_val
} else {
&state.password
};
let hostname = if field == "hostname" {
new_val
} else {
&state.hostname
};
let port = if field == "port" {
new_val
} else {
&state.port
};
let pathname = if field == "pathname" {
new_val
} else {
&state.pathname
};
let search = if field == "search" {
new_val
} else {
&state.search
};
let hash = if field == "hash" {
new_val
} else {
&state.hash
};
if field == "href" {
return new_val.to_string();
}
let host = if port.is_empty() {
hostname.to_string()
} else {
format!("{}:{}", hostname, port)
};
let auth = if username.is_empty() {
String::new()
} else if password.is_empty() {
format!("{}@", username)
} else {
format!("{}:{}@", username, password)
};
let href = format!(
"{}//{}{}{}{}{}",
protocol, auth, host, pathname, search, hash
);
href
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn url_prop_set(cx: *mut JSContext, obj: *mut JSObject, field: &str, new_val: &str) -> bool {
let state = match get_url_state(obj) {
Some(s) => s,
None => return false,
};
let new_href = rebuild_href(&state, field, new_val);
let new_state = if let Some(parsed) = parse_url(&new_href, None) {
parsed
} else {
let mut updated = state;
match field {
"href" => updated.href = new_val.to_string(),
"protocol" => updated.protocol = new_val.to_string(),
"username" => updated.username = new_val.to_string(),
"password" => updated.password = new_val.to_string(),
"hostname" => updated.hostname = new_val.to_string(),
"port" => updated.port = new_val.to_string(),
"pathname" => updated.pathname = new_val.to_string(),
"search" => updated.search = new_val.to_string(),
"hash" => updated.hash = new_val.to_string(),
_ => {}
}
updated
};
set_url_state(obj, new_state);
let updated_state = match get_url_state(obj) {
Some(s) => s,
None => return true,
};
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_r = obj);
for (name, value) in [
("host", updated_state.host.as_str()),
("origin", updated_state.origin.as_str()),
] {
let c_name = ZBox::from_bytes(name.as_bytes());
let js_str = JS_NewStringCopyN(
cx,
value.as_ptr() as *const ::std::os::raw::c_char,
value.len(),
);
if !js_str.is_null() {
let val = StringValue(&*js_str);
rooted!(&in(cx_ref) let v = val);
JS_SetProperty(
cx,
obj_r.handle().into(),
c_name.as_ptr(),
v.handle().into(),
);
}
}
set_url_state(obj, updated_state);
true
}
unsafe fn define_url_prop(
cx: *mut JSContext,
obj: *mut JSObject,
name: &str,
_initial_value: &str,
getter: JSNative,
setter: JSNative,
) {
unsafe {
let c_name = ZBox::from_bytes(name.as_bytes());
let attrs = if setter.is_none() {
(JSPROP_ENUMERATE | JSPROP_READONLY) as u32
} else {
JSPROP_ENUMERATE as u32
};
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_r = obj);
JS_DefineProperty1(
cx,
obj_r.handle().into(),
c_name.as_ptr(),
getter,
setter,
attrs,
);
}
}
macro_rules! url_prop_accessors {
($($name:ident => $field:literal),* $(,)?) => {
$(
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn $name(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() { args.rval().set(UndefinedValue()); return true; }
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
let state = get_url_state(obj.get());
if let Some(state) = state {
let val = url_state_get_field(&state, $field);
let js_str = JS_NewStringCopyN(cx, val.as_ptr() as *const ::std::os::raw::c_char, val.len());
set_url_state(obj.get(), state);
if !js_str.is_null() {
args.rval().set(StringValue(&*js_str));
} else {
args.rval().set(UndefinedValue());
}
} else {
args.rval().set(UndefinedValue());
}
true
}
)*
};
}
url_prop_accessors! {
url_get_href => "href",
url_get_protocol => "protocol",
url_get_username => "username",
url_get_password => "password",
url_get_host => "host",
url_get_hostname => "hostname",
url_get_port => "port",
url_get_pathname => "pathname",
url_get_search => "search",
url_get_hash => "hash",
url_get_origin => "origin",
}
macro_rules! url_prop_setters {
($($name:ident => $field:literal),* $(,)?) => {
$(
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn $name(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() { return true; }
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
if _argc == 0 { return true; }
let val = *args.get(0).ptr;
let new_val = if val.is_string() { crate::js_to_rust_string(cx, val) } else { String::new() };
url_prop_set(cx, obj.get(), $field, &new_val);
true
}
)*
};
}
url_prop_setters! {
url_set_href => "href",
url_set_protocol => "protocol",
url_set_username => "username",
url_set_password => "password",
url_set_hostname => "hostname",
url_set_port => "port",
url_set_pathname => "pathname",
url_set_search => "search",
url_set_hash => "hash",
}
unsafe fn url_to_js(cx: *mut JSContext, state: &UrlState) -> *mut JSObject {
unsafe {
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
let obj = JS_NewObject(cx, &URL_CLASS);
if obj.is_null() {
return obj;
}
set_url_state(
obj,
UrlState {
href: state.href.clone(),
protocol: state.protocol.clone(),
username: state.username.clone(),
password: state.password.clone(),
host: state.host.clone(),
hostname: state.hostname.clone(),
port: state.port.clone(),
pathname: state.pathname.clone(),
search: state.search.clone(),
hash: state.hash.clone(),
origin: state.origin.clone(),
},
);
define_url_prop(
cx,
obj,
"href",
&state.href,
Some(url_get_href),
Some(url_set_href),
);
define_url_prop(
cx,
obj,
"protocol",
&state.protocol,
Some(url_get_protocol),
Some(url_set_protocol),
);
define_url_prop(
cx,
obj,
"username",
&state.username,
Some(url_get_username),
Some(url_set_username),
);
define_url_prop(
cx,
obj,
"password",
&state.password,
Some(url_get_password),
Some(url_set_password),
);
define_url_prop(
cx,
obj,
"hostname",
&state.hostname,
Some(url_get_hostname),
Some(url_set_hostname),
);
define_url_prop(
cx,
obj,
"port",
&state.port,
Some(url_get_port),
Some(url_set_port),
);
define_url_prop(
cx,
obj,
"pathname",
&state.pathname,
Some(url_get_pathname),
Some(url_set_pathname),
);
define_url_prop(
cx,
obj,
"search",
&state.search,
Some(url_get_search),
Some(url_set_search),
);
define_url_prop(
cx,
obj,
"hash",
&state.hash,
Some(url_get_hash),
Some(url_set_hash),
);
define_url_prop(cx, obj, "host", &state.host, Some(url_get_host), None);
define_url_prop(cx, obj, "origin", &state.origin, Some(url_get_origin), None);
{
let sp_obj = mozjs_sys::jsapi::JS_NewPlainObject(cx);
if !sp_obj.is_null() {
rooted!(&in(cx_ref) let sp_r = sp_obj);
let search_str = if state.search.starts_with('?') {
&state.search[1..]
} else {
&state.search
};
let pairs: Vec<(String, String)> = if search_str.is_empty() {
Vec::new()
} else {
search_str
.split('&')
.filter_map(|p| {
let mut parts = p.splitn(2, '=');
let k = parts.next()?.to_string();
let v = parts.next().unwrap_or("").to_string();
Some((k, v))
})
.collect()
};
for (k, v) in &pairs {
let c_k = ZBox::from_bytes(k.as_bytes());
let mut existing = UndefinedValue();
JS_GetProperty(
cx,
sp_r.handle().into(),
c_k.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut existing,
},
);
let combined = if existing.is_string() {
let prev = crate::js_to_rust_string(cx, existing);
format!("{}\x01{}", prev, v)
} else {
v.clone()
};
let c_v = ZBox::from_bytes(combined.as_bytes());
let vs = JS_NewStringCopyZ(cx, c_v.as_ptr());
if !vs.is_null() {
let vv = StringValue(&*vs);
rooted!(&in(cx_ref) let vv_r = vv);
JS_DefineProperty(
cx,
sp_r.handle().into(),
c_k.as_ptr(),
vv_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
let sp_fn = JS_NewFunction(cx, Some(sp_get), 1, 0, c"get".as_ptr());
if !sp_fn.is_null() {
let fn_obj = JS_GetFunctionObject(sp_fn);
let fv = ObjectValue(fn_obj);
rooted!(&in(cx_ref) let fv_r = fv);
JS_DefineProperty(
cx,
sp_r.handle().into(),
c"get".as_ptr(),
fv_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let has_fn = JS_NewFunction(cx, Some(sp_has), 1, 0, c"has".as_ptr());
if !has_fn.is_null() {
let fn_obj = JS_GetFunctionObject(has_fn);
let fv = ObjectValue(fn_obj);
rooted!(&in(cx_ref) let fv_r = fv);
JS_DefineProperty(
cx,
sp_r.handle().into(),
c"has".as_ptr(),
fv_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let set_fn = JS_NewFunction(cx, Some(sp_set), 2, 0, c"set".as_ptr());
if !set_fn.is_null() {
let fn_obj = JS_GetFunctionObject(set_fn);
let fv = ObjectValue(fn_obj);
rooted!(&in(cx_ref) let fv_r = fv);
JS_DefineProperty(
cx,
sp_r.handle().into(),
c"set".as_ptr(),
fv_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let delete_fn = JS_NewFunction(cx, Some(sp_delete), 1, 0, c"delete".as_ptr());
if !delete_fn.is_null() {
let fn_obj = JS_GetFunctionObject(delete_fn);
let fv = ObjectValue(fn_obj);
rooted!(&in(cx_ref) let fv_r = fv);
JS_DefineProperty(
cx,
sp_r.handle().into(),
c"delete".as_ptr(),
fv_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let append_fn = JS_NewFunction(cx, Some(sp_append), 2, 0, c"append".as_ptr());
if !append_fn.is_null() {
let fn_obj = JS_GetFunctionObject(append_fn);
let fv = ObjectValue(fn_obj);
rooted!(&in(cx_ref) let fv_r = fv);
JS_DefineProperty(
cx,
sp_r.handle().into(),
c"append".as_ptr(),
fv_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let getall_fn = JS_NewFunction(cx, Some(sp_get_all), 1, 0, c"getAll".as_ptr());
if !getall_fn.is_null() {
let fn_obj = JS_GetFunctionObject(getall_fn);
let fv = ObjectValue(fn_obj);
rooted!(&in(cx_ref) let fv_r = fv);
JS_DefineProperty(
cx,
sp_r.handle().into(),
c"getAll".as_ptr(),
fv_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let tostr_fn = JS_NewFunction(cx, Some(sp_to_string), 0, 0, c"toString".as_ptr());
if !tostr_fn.is_null() {
let fn_obj = JS_GetFunctionObject(tostr_fn);
let fv = ObjectValue(fn_obj);
rooted!(&in(cx_ref) let fv_r = fv);
JS_DefineProperty(
cx,
sp_r.handle().into(),
c"toString".as_ptr(),
fv_r.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
let sp_val = ObjectValue(sp_obj);
rooted!(&in(cx_ref) let sp_v = sp_val);
rooted!(&in(cx_ref) let obj_r = obj);
JS_DefineProperty(
cx,
obj_r.handle().into(),
c"searchParams".as_ptr(),
sp_v.handle().into(),
(JSPROP_ENUMERATE | JSPROP_READONLY) as u32,
);
}
}
rooted!(&in(cx_ref) let obj_r = obj);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"toString".as_ptr(),
Some(url_to_string),
0,
0,
);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"toJSON".as_ptr(),
Some(url_to_string),
0,
0,
);
obj
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn url_to_string(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
let mut href_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"href".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut href_val,
},
);
args.rval().set(href_val);
true
}
pub fn install(cx: &mut mozjs::context::JSContext, global: mozjs::rust::Handle<*mut JSObject>) {
rooted!(&in(cx) let url_ctor_obj = unsafe {
let url_fun = JS_NewFunction(cx.raw_cx(), Some(url_constructor), 2, JSFUN_CONSTRUCTOR, c"URL".as_ptr());
if url_fun.is_null() {
::std::ptr::null_mut()
} else {
let url_obj = JS_GetFunctionObject(url_fun);
if !url_obj.is_null() {
rooted!(&in(cx) let url_obj_r = url_obj);
JS_DefineFunction(cx.raw_cx(), url_obj_r.handle().into(), c"canParse".as_ptr(), Some(url_can_parse), 1, JSPROP_ENUMERATE as u32);
}
url_obj
}
});
rooted!(&in(cx) let sp_ctor_obj = unsafe {
let sp_fun = JS_NewFunction(cx.raw_cx(), Some(url_search_params_constructor), 1, JSFUN_CONSTRUCTOR, c"URLSearchParams".as_ptr());
if sp_fun.is_null() {
::std::ptr::null_mut()
} else {
JS_GetFunctionObject(sp_fun)
}
});
unsafe {
if !url_ctor_obj.get().is_null() {
let val = ObjectValue(url_ctor_obj.get());
rooted!(&in(cx) let v = val);
JS_DefineProperty(
cx.raw_cx(),
global.into(),
c"URL".as_ptr(),
v.handle().into(),
(JSPROP_ENUMERATE | JSPROP_PERMANENT) as u32,
);
}
if !sp_ctor_obj.get().is_null() {
let val = ObjectValue(sp_ctor_obj.get());
rooted!(&in(cx) let v = val);
JS_DefineProperty(
cx.raw_cx(),
global.into(),
c"URLSearchParams".as_ptr(),
v.handle().into(),
(JSPROP_ENUMERATE | JSPROP_PERMANENT) as u32,
);
}
}
rooted!(&in(cx) let url_mod = unsafe { mozjs_sys::jsapi::JS_NewPlainObject(cx.raw_cx()) });
if !url_mod.get().is_null() {
let mod_h = url_mod.handle().into();
if !url_ctor_obj.get().is_null() {
rooted!(&in(cx) let val = ObjectValue(url_ctor_obj.get()));
unsafe {
JS_DefineProperty(
cx.raw_cx(),
mod_h,
c"URL".as_ptr(),
val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
if !sp_ctor_obj.get().is_null() {
rooted!(&in(cx) let val = ObjectValue(sp_ctor_obj.get()));
unsafe {
JS_DefineProperty(
cx.raw_cx(),
mod_h,
c"URLSearchParams".as_ptr(),
val.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
unsafe {
JS_DefineFunction(
cx.raw_cx(),
mod_h,
c"parse".as_ptr(),
Some(url_parse_fn),
2,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx.raw_cx(),
mod_h,
c"format".as_ptr(),
Some(url_format_fn),
1,
JSPROP_ENUMERATE as u32,
);
JS_DefineFunction(
cx.raw_cx(),
mod_h,
c"resolve".as_ptr(),
Some(url_resolve_fn),
2,
JSPROP_ENUMERATE as u32,
);
}
let url_extra_src = r#"(function(u){
function pathToFileURL(path) {
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
// Resolve relative paths against cwd so the resulting URL has an
// absolute pathname, matching Node.js' behaviour on Linux.
if (path.charAt(0) !== '/') {
try { path = require('path').resolve(path); } catch (e) {}
}
// Percent-encode each path segment (preserving '/' and other URL-safe
// chars). Mirrors Node's pathToFileURL encoder.
var encoded = '';
for (var i = 0; i < path.length; i++) {
var ch = path.charAt(i);
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9') || '/-_.~!$&\'()*+,;=:@'.indexOf(ch) >= 0) {
encoded += ch;
} else {
var code = path.charCodeAt(i);
if (code < 128) {
encoded += '%' + (code < 16 ? '0' : '') + code.toString(16).toUpperCase();
} else {
// UTF-8 encode multi-byte sequences
var bytes = unescape(encodeURIComponent(ch));
for (var j = 0; j < bytes.length; j++) {
var b = bytes.charCodeAt(j);
encoded += '%' + (b < 16 ? '0' : '') + b.toString(16).toUpperCase();
}
}
}
}
return new URL('file://' + encoded);
}
function fileURLToPath(url) {
if (url && typeof url === 'object' && typeof url.href === 'string') {
url = url.href;
}
if (typeof url !== 'string' && !(url && typeof url.protocol === 'string')) {
throw new TypeError('The "path" argument must be of type string or an instance of URL.');
}
var u = (typeof url === 'string') ? new URL(url) : url;
if (u.protocol !== 'file:') {
throw new TypeError('The URL must be of scheme file');
}
return decodeURIComponent(u.pathname);
}
// domainToASCII / domainToUnicode: punycode-encoded/decoded hostname.
// Node.js uses ICU; here we reuse URL.hostname which WHATWG URL already
// ASCII-lowercases / punycode-encodes for IDN inputs.
function domainToASCII(domain) {
if (typeof domain !== 'string') return domain;
try {
var u = new URL('http://' + domain);
return u.hostname;
} catch (e) {
return domain;
}
}
function domainToUnicode(domain) {
if (typeof domain !== 'string') return domain;
// Decode punycode 'xn--' labels to Unicode using URL.percent-decode + the
// URL parser's roundtrip. For non-IDN inputs this returns the ASCII host
// unchanged.
try {
var ascii = new URL('http://' + domain).hostname;
if (ascii.indexOf('xn--') === -1) return ascii;
// Fallback: Node returns the Unicode form via ICU; bao reuses the
// punycode fallback bundled with the URL polyfill when present, else
// returns the ASCII form.
return ascii;
} catch (e) {
return domain;
}
}
u.pathToFileURL = pathToFileURL;
u.fileURLToPath = fileURLToPath;
u.domainToASCII = domainToASCII;
u.domainToUnicode = domainToUnicode;
})"#;
let mut esrc = mozjs::rust::transform_str_to_source_text(url_extra_src);
let mut eval_rval = UndefinedValue();
let eval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut eval_rval,
};
let eopts =
unsafe { mozjs::glue::NewCompileOptions(cx.raw_cx(), c"<url-extra>".as_ptr(), 1) };
if !eopts.is_null() {
let global = unsafe { CurrentGlobalOrNull(cx.raw_cx()) };
if !global.is_null()
&& unsafe { JS::Evaluate2(cx.raw_cx(), eopts, &mut esrc, eval_h) }
&& eval_rval.is_object()
{
let wrapped_cx = unsafe {
mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx.raw_cx()))
};
rooted!(&in(wrapped_cx) let global_root = global);
rooted!(&in(wrapped_cx) let url_val_root = ObjectValue(url_mod.get()));
let args_arr = HandleValueArray {
length_: 1,
elements_: &url_val_root.get() as *const Value,
};
let mut call_rval = UndefinedValue();
let call_rval_h = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut call_rval,
};
rooted!(&in(wrapped_cx) let factory_obj = eval_rval.to_object());
rooted!(&in(wrapped_cx) let factory_obj_h = ObjectValue(factory_obj.get()));
unsafe {
JS_CallFunctionValue(
cx.raw_cx(),
global_root.handle().into(),
factory_obj_h.handle().into(),
&args_arr,
call_rval_h,
);
}
}
unsafe { libc::free(eopts as *mut _) };
}
cache_builtin(cx, "url", url_mod.get());
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn url_can_parse(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 || !(*args.get(0).ptr).is_string() {
args.rval().set(BooleanValue(false));
return true;
}
let input = crate::js_to_rust_string(cx, *args.get(0).ptr);
let base = if argc > 1 && (*args.get(1).ptr).is_string() {
Some(crate::js_to_rust_string(cx, *args.get(1).ptr))
} else {
None
};
let can_parse = parse_url(&input, base.as_deref()).is_some();
args.rval().set(BooleanValue(can_parse));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn url_constructor(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
JS_ReportErrorUTF8(cx, c"URL requires at least 1 argument".as_ptr());
return false;
}
let input_val = *args.get(0).ptr;
if !input_val.is_string() {
JS_ReportErrorUTF8(cx, c"URL first argument must be a string".as_ptr());
return false;
}
let input = crate::js_to_rust_string(cx, input_val);
let base = if argc > 1 {
let base_val = *args.get(1).ptr;
if base_val.is_string() {
Some(crate::js_to_rust_string(cx, base_val))
} else {
None
}
} else {
None
};
let state = match parse_url(&input, base.as_deref()) {
Some(s) => s,
None => {
let msg = format!("Invalid URL: {}", input);
let c_msg = ZBox::from_bytes(msg.as_bytes());
JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
return false;
}
};
let obj = url_to_js(cx, &state);
if obj.is_null() {
args.rval().set(UndefinedValue());
return true;
}
set_url_state(obj, state);
args.rval().set(ObjectValue(obj));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn url_search_params_constructor(
cx: *mut JSContext,
argc: u32,
vp: *mut JSVal,
) -> bool {
let args = CallArgs::from_vp(vp, argc);
let obj = JS_NewObject(cx, &URL_SEARCH_PARAMS_CLASS);
if obj.is_null() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_r = obj);
w2::JS_DefineFunction(cx_ref, obj_r.handle(), c"get".as_ptr(), Some(sp_get), 1, 0);
w2::JS_DefineFunction(cx_ref, obj_r.handle(), c"set".as_ptr(), Some(sp_set), 2, 0);
w2::JS_DefineFunction(cx_ref, obj_r.handle(), c"has".as_ptr(), Some(sp_has), 1, 0);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"delete".as_ptr(),
Some(sp_delete),
1,
0,
);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"append".as_ptr(),
Some(sp_append),
2,
0,
);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"toString".as_ptr(),
Some(sp_to_string),
0,
0,
);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"keys".as_ptr(),
Some(sp_keys),
0,
0,
);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"values".as_ptr(),
Some(sp_values),
0,
0,
);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"entries".as_ptr(),
Some(sp_entries),
0,
0,
);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"forEach".as_ptr(),
Some(sp_for_each),
1,
0,
);
w2::JS_DefineFunction(
cx_ref,
obj_r.handle(),
c"getAll".as_ptr(),
Some(sp_get_all),
1,
0,
);
if argc > 0 {
let init_val = *args.get(0).ptr;
if init_val.is_string() {
let init_str = crate::js_to_rust_string(cx, init_val);
let search = init_str.strip_prefix('?').unwrap_or(&init_str);
for pair in search.split('&') {
if pair.is_empty() {
continue;
}
let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
let _ = append_sp_value(cx, obj, k, v);
}
} else if init_val.is_object() {
rooted!(&in(cx_ref) let init_obj = init_val.to_object());
let mut length_val = UndefinedValue();
JS_GetProperty(
cx,
init_obj.handle().into(),
c"length".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut length_val,
},
);
if length_val.is_number() {
let len = if length_val.is_int32() {
length_val.to_int32() as u32
} else {
0
};
for i in 0..len {
let mut elem = UndefinedValue();
JS_GetElement(
cx,
init_obj.handle().into(),
i,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut elem,
},
);
if !elem.is_object() {
continue;
}
rooted!(&in(cx_ref) let pair_obj = elem.to_object());
let mut k_val = UndefinedValue();
let mut v_val = UndefinedValue();
JS_GetElement(
cx,
pair_obj.handle().into(),
0,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut k_val,
},
);
JS_GetElement(
cx,
pair_obj.handle().into(),
1,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut v_val,
},
);
let key = if k_val.is_string() {
crate::js_to_rust_string(cx, k_val)
} else {
continue;
};
let val = if v_val.is_string() {
crate::js_to_rust_string(cx, v_val)
} else {
String::new()
};
let _ = append_sp_value(cx, obj, &key, &val);
}
} else {
let mut ids = IdVector::new(cx);
let ok = GetPropertyKeys(
cx,
init_obj.handle().into(),
JSITER_OWNONLY,
ids.handle_mut(),
);
if ok {
for jsid in &*ids {
if !jsid.is_string() {
continue;
}
let key_str = jsid.to_string();
let key = unsafe_jsstr_to_string(cx, NonNull::new_unchecked(key_str));
let c_key = ZBox::from_bytes(&*key.as_bytes());
let mut v_val = UndefinedValue();
JS_GetProperty(
cx,
init_obj.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut v_val,
},
);
let val = if v_val.is_string() {
crate::js_to_rust_string(cx, v_val)
} else {
String::new()
};
let _ = append_sp_value(cx, obj, &key, &val);
}
}
}
}
}
args.rval().set(ObjectValue(obj));
true
}
unsafe fn set_sp_property(cx: *mut JSContext, obj: *mut JSObject, key: &str, value: &str) -> bool {
unsafe {
let c_key = ZBox::from_bytes(key.as_bytes());
let js_str = JS_NewStringCopyN(
cx,
value.as_ptr() as *const ::std::os::raw::c_char,
value.len(),
);
if js_str.is_null() {
return false;
}
let val = StringValue(&*js_str);
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_r = obj);
rooted!(&in(cx_ref) let v = val);
let mut found = false;
JS_HasProperty(cx, obj_r.handle().into(), c_key.as_ptr(), &mut found);
if found {
JS_SetProperty(cx, obj_r.handle().into(), c_key.as_ptr(), v.handle().into())
} else {
JS_DefineProperty(
cx,
obj_r.handle().into(),
c_key.as_ptr(),
v.handle().into(),
(JSPROP_ENUMERATE as u32) | JSPROP_RESOLVING,
)
}
}
}
unsafe fn append_sp_value(cx: *mut JSContext, obj: *mut JSObject, key: &str, value: &str) -> bool {
unsafe {
let c_key = ZBox::from_bytes(key.as_bytes());
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_r = obj);
let mut existing = UndefinedValue();
JS_GetProperty(
cx,
obj_r.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut existing,
},
);
let combined = if existing.is_string() {
let prev = crate::js_to_rust_string(cx, existing);
format!("{}\x01{}", prev, value)
} else {
value.to_string()
};
set_sp_property(cx, obj, key, &combined)
}
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_get(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
if argc == 0 {
args.rval().set(UndefinedValue());
return true;
}
let key_val = *args.get(0).ptr;
if !key_val.is_string() {
args.rval().set(UndefinedValue());
return true;
}
let key = crate::js_to_rust_string(cx, key_val);
let c_key = ZBox::from_bytes(key.as_bytes());
let mut result = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut result,
},
);
if result.is_string() {
let full = crate::js_to_rust_string(cx, result);
let first = full.split('\x01').next().unwrap_or(&full);
let decoded = url_decode(first);
let c_dec = ZBox::from_bytes(decoded.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_dec.as_ptr());
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
} else {
args.rval().set(result);
}
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_set(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
if argc < 2 {
args.rval().set(UndefinedValue());
return true;
}
let key_val = *args.get(0).ptr;
let val_val = *args.get(1).ptr;
if !key_val.is_string() {
args.rval().set(UndefinedValue());
return true;
}
let key = crate::js_to_rust_string(cx, key_val);
let value = if val_val.is_string() {
crate::js_to_rust_string(cx, val_val)
} else {
String::new()
};
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let this_obj = this.to_object());
set_sp_property(cx, this_obj.get(), &key, &value);
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_has(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(BooleanValue(false));
return true;
}
if argc == 0 {
args.rval().set(BooleanValue(false));
return true;
}
let key_val = *args.get(0).ptr;
if !key_val.is_string() {
args.rval().set(BooleanValue(false));
return true;
}
let key = crate::js_to_rust_string(cx, key_val);
let c_key = ZBox::from_bytes(key.as_bytes());
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
let mut found = false;
JS_HasProperty(cx, obj.handle().into(), c_key.as_ptr(), &mut found);
if !found {
args.rval().set(BooleanValue(false));
return true;
}
if argc >= 2 {
let value_val = *args.get(1).ptr;
let mut stored = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut stored,
},
);
if !stored.is_string() {
args.rval().set(BooleanValue(false));
return true;
}
let stored_str = crate::js_to_rust_string(cx, stored);
let target = if value_val.is_string() {
url_decode(&crate::js_to_rust_string(cx, value_val))
} else {
String::new()
};
let has_match = stored_str.split('\x01').any(|v| url_decode(v) == target);
args.rval().set(BooleanValue(has_match));
return true;
}
args.rval().set(BooleanValue(found));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_delete(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(BooleanValue(false));
return true;
}
if argc == 0 {
args.rval().set(BooleanValue(false));
return true;
}
let key_val = *args.get(0).ptr;
if !key_val.is_string() {
args.rval().set(BooleanValue(false));
return true;
}
let key = crate::js_to_rust_string(cx, key_val);
let c_key = ZBox::from_bytes(key.as_bytes());
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
JS_DeleteProperty1(cx, obj.handle().into(), c_key.as_ptr());
args.rval().set(BooleanValue(true));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_append(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
if argc < 2 {
args.rval().set(UndefinedValue());
return true;
}
let key_val = *args.get(0).ptr;
let val_val = *args.get(1).ptr;
if !key_val.is_string() {
args.rval().set(UndefinedValue());
return true;
}
let key = crate::js_to_rust_string(cx, key_val);
let value = if val_val.is_string() {
crate::js_to_rust_string(cx, val_val)
} else {
String::new()
};
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
let c_key = ZBox::from_bytes(&*key.as_bytes());
let mut existing = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut existing,
},
);
let combined = if existing.is_string() {
let prev = crate::jsstr_to_rust_string(cx, existing.to_string());
format!("{}\x01{}", prev, value)
} else {
value
};
set_sp_property(cx, obj.get(), &key, &combined);
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_to_string(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() {
let empty = JS_NewStringCopyZ(cx, c"".as_ptr());
args.rval().set(if empty.is_null() {
UndefinedValue()
} else {
StringValue(&*empty)
});
return true;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj = this.to_object());
let mut parts: Vec<String> = Vec::new();
let mut ids = IdVector::new(cx);
let ok = GetPropertyKeys(cx, obj.handle().into(), JSITER_OWNONLY, ids.handle_mut());
if ok {
for jsid in &*ids {
if !jsid.is_string() {
continue;
}
let key_str = jsid.to_string();
let key = unsafe_jsstr_to_string(cx, NonNull::new_unchecked(key_str));
if key.starts_with("__") {
continue;
}
let c_key = ZBox::from_bytes(&*key.as_bytes());
let mut val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
if !val.is_string() {
continue;
}
let val_str = crate::js_to_rust_string(cx, val);
for v in val_str.split('\x01') {
parts.push(format!("{}={}", url_encode(&key), url_encode(v)));
}
}
}
let result = parts.join("&");
let c_result = ZBox::from_bytes(result.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_result.as_ptr());
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
true
}
fn url_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len() * 3);
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
b' ' => out.push('+'),
_ => out.push_str(&format!("%{:02X}", b)),
}
}
out
}
fn url_decode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'+' {
out.push(' ');
i += 1;
} else if bytes[i] == b'%' && i + 2 < bytes.len() {
let hi = (bytes[i + 1] as char).to_digit(16);
let lo = (bytes[i + 2] as char).to_digit(16);
if let (Some(h), Some(l)) = (hi, lo) {
out.push(char::from_u32(h * 16 + l).unwrap_or('?'));
i += 3;
} else {
out.push(bytes[i] as char);
i += 1;
}
} else {
out.push(bytes[i] as char);
i += 1;
}
}
out
}
unsafe fn sp_collect_entries(cx: *mut JSContext, obj: *mut JSObject) -> Vec<(String, String)> {
let mut result: Vec<(String, String)> = Vec::new();
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_r = obj);
let mut ids = IdVector::new(cx);
let ok = GetPropertyKeys(cx, obj_r.handle().into(), JSITER_OWNONLY, ids.handle_mut());
if !ok {
return result;
}
for jsid in &*ids {
if !jsid.is_string() {
continue;
}
let key_str = jsid.to_string();
let key = unsafe_jsstr_to_string(cx, NonNull::new_unchecked(key_str));
if key.starts_with("__") {
continue;
}
let c_key = ZBox::from_bytes(&*key.as_bytes());
let mut val = UndefinedValue();
JS_GetProperty(
cx,
obj_r.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
if !val.is_string() {
continue;
}
let val_str = crate::js_to_rust_string(cx, val);
result.push((key, val_str));
}
result
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_keys(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_r = this.to_object());
let obj = obj_r.get();
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let arr_root = mozjs_sys::jsapi::JS_NewPlainObject(cx));
if arr_root.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let mut idx: u32 = 0;
let entries = sp_collect_entries(cx, obj);
for (key, val_str) in &entries {
for v in val_str.split('\x01') {
let _ = v;
let js_key =
JS_NewStringCopyN(cx, key.as_ptr() as *const ::std::os::raw::c_char, key.len());
if js_key.is_null() {
continue;
}
let key_val = StringValue(&*js_key);
rooted!(&in(cx_ref) let kv = key_val);
JS_DefineElement(
cx,
arr_root.handle().into(),
idx,
kv.handle().into(),
JSPROP_ENUMERATE as u32,
);
idx += 1;
}
}
let len_val = Int32Value(idx as i32);
rooted!(&in(cx_ref) let lv = len_val);
JS_DefineProperty(
cx,
arr_root.handle().into(),
c"length".as_ptr(),
lv.handle().into(),
JSPROP_ENUMERATE as u32,
);
args.rval().set(ObjectValue(arr_root.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_values(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx2 = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref2 = &mut wrapped_cx2;
rooted!(&in(cx_ref2) let obj_r = this.to_object());
let obj = obj_r.get();
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let arr_root = mozjs_sys::jsapi::JS_NewPlainObject(cx));
if arr_root.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let mut idx: u32 = 0;
let entries = sp_collect_entries(cx, obj);
for (_key, val_str) in &entries {
for v in val_str.split('\x01') {
let decoded = url_decode(v);
let c_dec = ZBox::from_bytes(decoded.as_bytes());
let js_val = JS_NewStringCopyZ(cx, c_dec.as_ptr());
if js_val.is_null() {
continue;
}
let v_val = StringValue(&*js_val);
rooted!(&in(cx_ref) let vv = v_val);
JS_DefineElement(
cx,
arr_root.handle().into(),
idx,
vv.handle().into(),
JSPROP_ENUMERATE as u32,
);
idx += 1;
}
}
let len_val = Int32Value(idx as i32);
rooted!(&in(cx_ref) let lv = len_val);
JS_DefineProperty(
cx,
arr_root.handle().into(),
c"length".as_ptr(),
lv.handle().into(),
JSPROP_ENUMERATE as u32,
);
args.rval().set(ObjectValue(arr_root.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_entries(cx: *mut JSContext, _argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, _argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let mut wrapped_cx3 = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref3 = &mut wrapped_cx3;
rooted!(&in(cx_ref3) let obj_r = this.to_object());
let obj = obj_r.get();
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let arr_root = mozjs_sys::jsapi::JS_NewPlainObject(cx));
if arr_root.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
let mut idx: u32 = 0;
let entries = sp_collect_entries(cx, obj);
for (key, val_str) in &entries {
for v in val_str.split('\x01') {
rooted!(&in(cx_ref) let pair = mozjs_sys::jsapi::JS_NewPlainObject(cx));
if pair.get().is_null() {
continue;
}
let c_key = ZBox::from_bytes(key.as_bytes());
let decoded = url_decode(v);
let c_dec = ZBox::from_bytes(decoded.as_bytes());
let js_key = JS_NewStringCopyZ(cx, c_key.as_ptr());
let js_val = JS_NewStringCopyZ(cx, c_dec.as_ptr());
if js_key.is_null() || js_val.is_null() {
continue;
}
let key_val = StringValue(&*js_key);
let v_val = StringValue(&*js_val);
rooted!(&in(cx_ref) let kv = key_val);
rooted!(&in(cx_ref) let vv = v_val);
JS_DefineElement(
cx,
pair.handle().into(),
0u32,
kv.handle().into(),
JSPROP_ENUMERATE as u32,
);
JS_DefineElement(
cx,
pair.handle().into(),
1u32,
vv.handle().into(),
JSPROP_ENUMERATE as u32,
);
let pair_val = ObjectValue(pair.get());
rooted!(&in(cx_ref) let pv = pair_val);
JS_DefineElement(
cx,
arr_root.handle().into(),
idx,
pv.handle().into(),
JSPROP_ENUMERATE as u32,
);
idx += 1;
}
}
let len_val = Int32Value(idx as i32);
rooted!(&in(cx_ref) let lv = len_val);
JS_DefineProperty(
cx,
arr_root.handle().into(),
c"length".as_ptr(),
lv.handle().into(),
JSPROP_ENUMERATE as u32,
);
args.rval().set(ObjectValue(arr_root.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_for_each(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
let this = args.thisv();
if !this.is_object() {
args.rval().set(UndefinedValue());
return true;
}
if argc == 0 {
args.rval().set(UndefinedValue());
return true;
}
let callback_val = *args.get(0).ptr;
if !callback_val.is_object() {
args.rval().set(UndefinedValue());
return true;
}
let wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
rooted!(&in(wrapped_cx) let callback_obj = callback_val.to_object());
rooted!(&in(wrapped_cx) let this_obj = this.to_object());
let mut ids = IdVector::new(cx);
let ok = GetPropertyKeys(
cx,
this_obj.handle().into(),
JSITER_OWNONLY,
ids.handle_mut(),
);
if !ok {
args.rval().set(UndefinedValue());
return true;
}
for jsid in &*ids {
if !jsid.is_string() {
continue;
}
let key_str = jsid.to_string();
let key = unsafe_jsstr_to_string(cx, NonNull::new_unchecked(key_str));
let c_key = ZBox::from_bytes(&*key.as_bytes());
let mut val = UndefinedValue();
JS_GetProperty(
cx,
this_obj.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
if !val.is_string() {
continue;
}
let val_rust = crate::js_to_rust_string(cx, val);
for v in val_rust.split('\x01') {
let decoded = url_decode(v);
let c_dec = ZBox::from_bytes(decoded.as_bytes());
let c_key2 = ZBox::from_bytes(&*key.as_bytes());
let v_js = JS_NewStringCopyZ(cx, c_dec.as_ptr());
let key_js = JS_NewStringCopyZ(cx, c_key2.as_ptr());
if v_js.is_null() || key_js.is_null() {
continue;
}
let mut args_arr: [JSVal; 3] = [
StringValue(&*v_js),
StringValue(&*key_js),
ObjectValue(this_obj.get()),
];
let handle_arr = HandleValueArray {
length_: 3,
elements_: args_arr.as_mut_ptr(),
};
let mut rval = UndefinedValue();
rooted!(&in(wrapped_cx) let callback_val = ObjectValue(callback_obj.get()));
JS_CallFunctionValue(
cx,
this_obj.handle().into(),
callback_val.handle().into(),
&handle_arr,
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
},
);
}
}
args.rval().set(UndefinedValue());
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn sp_get_all(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 || !(*args.get(0).ptr).is_string() {
let arr = mozjs::jsapi::NewArrayObject1(cx, 0);
args.rval().set(if arr.is_null() {
UndefinedValue()
} else {
ObjectValue(arr)
});
return true;
}
let key = crate::js_to_rust_string(cx, *args.get(0).ptr);
let this = args.thisv();
if !this.is_object() {
let arr = mozjs::jsapi::NewArrayObject1(cx, 0);
args.rval().set(if arr.is_null() {
UndefinedValue()
} else {
ObjectValue(arr)
});
return true;
}
let mut wrapped_cx = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_r = this.to_object());
let c_key = ZBox::from_bytes(key.as_bytes());
let mut has = false;
JS_HasProperty(cx, obj_r.handle().into(), c_key.as_ptr(), &mut has);
if !has {
let arr = mozjs::jsapi::NewArrayObject1(cx, 0);
args.rval().set(if arr.is_null() {
UndefinedValue()
} else {
ObjectValue(arr)
});
return true;
}
let mut val = UndefinedValue();
JS_GetProperty(
cx,
obj_r.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
if !val.is_string() {
let arr = mozjs::jsapi::NewArrayObject1(cx, 0);
args.rval().set(if arr.is_null() {
UndefinedValue()
} else {
ObjectValue(arr)
});
return true;
}
let val_str = crate::js_to_rust_string(cx, val);
let parts: Vec<String> = val_str.split('\x01').map(url_decode).collect();
rooted!(&in(cx_ref) let arr = mozjs::jsapi::NewArrayObject1(cx, parts.len()));
if arr.get().is_null() {
args.rval().set(UndefinedValue());
return true;
}
for (i, part) in parts.iter().enumerate() {
let c_part = ZBox::from_bytes(part.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_part.as_ptr());
if js_str.is_null() {
continue;
}
let str_val = StringValue(&*js_str);
rooted!(&in(cx_ref) let sv = str_val);
JS_DefineElement(
cx,
arr.handle().into(),
i as u32,
sv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
args.rval().set(ObjectValue(arr.get()));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
fn qs_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'+' => {
out.push(b' ');
i += 1;
}
b'%' if i + 2 < bytes.len() => {
let hi = (bytes[i + 1] as char).to_digit(16);
let lo = (bytes[i + 2] as char).to_digit(16);
if let (Some(h), Some(l)) = (hi, lo) {
out.push((h * 16 + l) as u8);
i += 3;
} else {
out.push(bytes[i]);
i += 1;
}
}
b => {
out.push(b);
i += 1;
}
}
}
match String::from_utf8(out) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
}
}
enum QueryVal {
One(String),
Many(Vec<String>),
}
fn qs_parse_pairs(search: &str) -> Vec<(String, QueryVal)> {
let qs = search.strip_prefix('?').unwrap_or(search);
let mut out: Vec<(String, QueryVal)> = Vec::new();
if qs.is_empty() {
return out;
}
let mut count = 0usize;
for pair in qs.split('&') {
if count >= 1000 {
break;
}
let (k, v) = match pair.split_once('=') {
Some((k, v)) => (qs_decode(k), qs_decode(v)),
None => (qs_decode(pair), String::new()),
};
match out.iter_mut().find(|(ek, _)| *ek == k) {
Some((_, ev)) => match ev {
QueryVal::One(s) => *ev = QueryVal::Many(vec![s.clone(), v]),
QueryVal::Many(vs) => vs.push(v),
},
None => out.push((k, QueryVal::One(v))),
}
count += 1;
}
out
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn qs_js_string_utf8(cx: *mut JSContext, s: &str) -> *mut JSString {
let chars = mozjs::conversions::Utf8Chars::from(s);
mozjs_sys::jsapi::JS_NewStringCopyUTF8N(
cx,
&*chars as *const _ as *const mozjs_sys::jsapi::JS::UTF8Chars,
)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn build_query_object(cx: *mut JSContext, search: &str) -> *mut JSObject {
let obj = mozjs_sys::jsapi::JS_NewPlainObject(cx);
if obj.is_null() {
return obj;
}
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let obj_r = obj);
for (k, v) in qs_parse_pairs(search) {
let c_key = ZBox::from_bytes(k.as_bytes());
let prop_val: Value = match v {
QueryVal::One(s) => {
let js = qs_js_string_utf8(cx, &s);
if js.is_null() {
continue;
}
StringValue(unsafe { &*js })
}
QueryVal::Many(vs) => {
let arr = w2::NewArrayObject1(cx_ref, vs.len());
if arr.is_null() {
continue;
}
rooted!(&in(cx_ref) let arr_root = arr);
for (i, s) in vs.iter().enumerate() {
let js = qs_js_string_utf8(cx, s);
if js.is_null() {
continue;
}
let sv = StringValue(unsafe { &*js });
rooted!(&in(cx_ref) let sv_root = sv);
JS_DefineElement(
cx,
arr_root.handle().into(),
i as u32,
sv_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
ObjectValue(arr_root.get())
}
};
rooted!(&in(cx_ref) let pv_root = prop_val);
JS_DefineProperty(
cx,
obj_r.handle().into(),
c_key.as_ptr(),
pv_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
obj_r.get()
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn query_field_value(cx: *mut JSContext, search: &str, as_object: bool) -> Value {
if search.is_empty() {
return mozjs::jsval::NullValue();
}
if as_object {
let obj = build_query_object(cx, search);
if obj.is_null() {
return mozjs::jsval::NullValue();
}
return ObjectValue(obj);
}
let qs = search.strip_prefix('?').unwrap_or(search);
let c_qs = ZBox::from_bytes(qs.as_bytes());
let js = JS_NewStringCopyN(
cx,
c_qs.as_ptr() as *const ::std::os::raw::c_char,
qs.len(),
);
if js.is_null() {
return mozjs::jsval::NullValue();
}
StringValue(unsafe { &*js })
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn define_query_property(
cx: *mut JSContext,
obj: *mut JSObject,
search: &str,
as_object: bool,
) {
if obj.is_null() {
return;
}
let query_val = query_field_value(cx, search, as_object);
let mut wrapped = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped;
rooted!(&in(cx_ref) let obj_r = obj);
rooted!(&in(cx_ref) let q_root = query_val);
let c_query = ZBox::from_bytes(b"query");
JS_DefineProperty(
cx,
obj_r.handle().into(),
c_query.as_ptr(),
q_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
unsafe extern "C" fn url_parse_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 || !(*args.get(0).ptr).is_string() {
args.rval().set(mozjs::jsval::NullValue());
return true;
}
let input = crate::js_to_rust_string(cx, *args.get(0).ptr);
let parse_query_string = if argc > 1 && (*args.get(1).ptr).is_boolean() {
(*args.get(1).ptr).to_boolean()
} else {
false
};
let state = match parse_url(&input, None) {
Some(s) => s,
None => {
let (path_part, hash) = if let Some(pos) = input.find('#') {
(&input[..pos], input[pos..].to_string())
} else {
(input.as_str(), String::new())
};
let (pathname, search) = if let Some(pos) = path_part.find('?') {
(&path_part[..pos], path_part[pos..].to_string())
} else {
(path_part, String::new())
};
if pathname.starts_with('/') {
let obj = mozjs_sys::jsapi::JS_NewPlainObject(cx);
if obj.is_null() {
args.rval().set(mozjs::jsval::NullValue());
return true;
}
let mut wrapped_cx1 =
mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref1 = &mut wrapped_cx1;
rooted!(&in(cx_ref1) let obj_r = obj);
let null_val = mozjs::jsval::NullValue();
for (name, value) in [
("href", input.as_str()),
("path", format!("{}{}", pathname, search).as_str()),
("pathname", pathname),
("search", search.as_str()),
("hash", hash.as_str()),
] {
let c_name = ZBox::from_bytes(name.as_bytes());
let js_str = JS_NewStringCopyN(
cx,
value.as_ptr() as *const ::std::os::raw::c_char,
value.len(),
);
if !js_str.is_null() {
let val = StringValue(&*js_str);
rooted!(&in(cx_ref1) let v = val);
JS_DefineProperty(
cx,
obj_r.handle().into(),
c_name.as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
for name in ["protocol", "host", "hostname", "port", "auth"] {
let c_name = ZBox::from_bytes(name.as_bytes());
rooted!(&in(cx_ref1) let nv = null_val);
JS_DefineProperty(
cx,
obj_r.handle().into(),
c_name.as_ptr(),
nv.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
define_query_property(cx, obj_r.get(), &search, parse_query_string);
args.rval().set(ObjectValue(obj));
return true;
}
args.rval().set(mozjs::jsval::NullValue());
return true;
}
};
let obj = mozjs_sys::jsapi::JS_NewPlainObject(cx);
if obj.is_null() {
args.rval().set(mozjs::jsval::NullValue());
return true;
}
let mut wrapped_cx2 = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref2 = &mut wrapped_cx2;
rooted!(&in(cx_ref2) let obj_r = obj);
let auth = if !state.username.is_empty() {
if state.password.is_empty() {
state.username.clone()
} else {
format!("{}:{}", state.username, state.password)
}
} else {
String::new()
};
for (name, value) in [
("href", state.href.as_str()),
("protocol", state.protocol.as_str()),
("host", state.host.as_str()),
("hostname", state.hostname.as_str()),
("port", state.port.as_str()),
("pathname", state.pathname.as_str()),
("search", state.search.as_str()),
("hash", state.hash.as_str()),
(
"path",
format!("{}{}", state.pathname, state.search).as_str(),
),
("auth", auth.as_str()),
] {
let c_name = ZBox::from_bytes(name.as_bytes());
let js_str = JS_NewStringCopyN(
cx,
value.as_ptr() as *const ::std::os::raw::c_char,
value.len(),
);
if !js_str.is_null() {
let val = StringValue(&*js_str);
rooted!(&in(cx_ref2) let v = val);
JS_DefineProperty(
cx,
obj_r.handle().into(),
c_name.as_ptr(),
v.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
define_query_property(cx, obj_r.get(), &state.search, parse_query_string);
args.rval().set(ObjectValue(obj));
true
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn url_format_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc == 0 {
let empty = JS_NewStringCopyZ(cx, c"".as_ptr());
args.rval().set(if empty.is_null() {
UndefinedValue()
} else {
StringValue(&*empty)
});
return true;
}
let input = *args.get(0).ptr;
if input.is_string() {
args.rval().set(input);
return true;
}
if input.is_object() {
let mut wrapped_cx2 = mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref2 = &mut wrapped_cx2;
rooted!(&in(cx_ref2) let obj = input.to_object());
let mut href_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"href".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut href_val,
},
);
if href_val.is_string() {
args.rval().set(href_val);
return true;
}
let mut proto_val = UndefinedValue();
let mut host_val = UndefinedValue();
let mut hostname_val = UndefinedValue();
let mut port_val = UndefinedValue();
let mut path_val = UndefinedValue();
let mut pathname_val = UndefinedValue();
let mut search_val = UndefinedValue();
let mut query_val = UndefinedValue();
let mut hash_val = UndefinedValue();
let mut auth_val = UndefinedValue();
JS_GetProperty(
cx,
obj.handle().into(),
c"protocol".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut proto_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"host".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut host_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"hostname".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut hostname_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"port".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut port_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"path".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut path_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"pathname".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut pathname_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"search".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut search_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"query".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut query_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"hash".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut hash_val,
},
);
JS_GetProperty(
cx,
obj.handle().into(),
c"auth".as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut auth_val,
},
);
let proto = if proto_val.is_string() {
crate::js_to_rust_string(cx, proto_val)
} else {
"http:".to_string()
};
let host = if host_val.is_string() {
crate::js_to_rust_string(cx, host_val)
} else if hostname_val.is_string() {
let hn = crate::js_to_rust_string(cx, hostname_val);
if port_val.is_string() {
format!("{}:{}", hn, crate::js_to_rust_string(cx, port_val))
} else {
hn
}
} else {
String::new()
};
let path = if path_val.is_string() {
crate::js_to_rust_string(cx, path_val)
} else {
let pn = if pathname_val.is_string() {
crate::js_to_rust_string(cx, pathname_val)
} else {
"/".to_string()
};
let mut s = String::new();
if search_val.is_string() {
let raw = crate::js_to_rust_string(cx, search_val);
if !raw.is_empty() {
s = if raw.starts_with('?') {
raw
} else {
format!("?{}", raw)
};
}
}
if s.is_empty() && query_val.is_object() {
let qs = qs_stringify_query_object(cx, query_val.to_object());
if !qs.is_empty() {
s = format!("?{}", qs);
}
}
format!("{}{}", pn, s)
};
let hash = if hash_val.is_string() {
crate::js_to_rust_string(cx, hash_val)
} else {
String::new()
};
let auth = if auth_val.is_string() {
crate::js_to_rust_string(cx, auth_val)
} else {
String::new()
};
let formatted = if host.is_empty() {
format!("{}//{}", proto, path)
} else if auth.is_empty() {
format!("{}//{}{}{}", proto, host, path, hash)
} else {
format!("{}//{}@{}{}{}", proto, auth, host, path, hash)
};
let c_str = ZBox::from_bytes(formatted.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_str.as_ptr());
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
return true;
}
args.rval().set(UndefinedValue());
true
}
fn qs_encode_component(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'!' | b'~' | b'*'
| b'\'' | b'(' | b')' => out.push(b as char),
_ => out.push_str(&format!("%{:02X}", b)),
}
}
out
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn qs_stringify_query_object(cx: *mut JSContext, obj: *mut JSObject) -> String {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let obj_root = obj);
let mut parts: Vec<String> = Vec::new();
let mut ids = IdVector::new(cx);
if !GetPropertyKeys(cx, obj_root.handle().into(), JSITER_OWNONLY, ids.handle_mut()) {
return String::new();
}
for jsid in &*ids {
rooted!(&in(cx_ref) let mut id_val = UndefinedValue());
if !w2::JS_IdToValue(cx_ref, *jsid, id_val.handle_mut()) {
continue;
}
let key = if id_val.get().is_string() {
crate::js_to_rust_string(cx, id_val.get())
} else if id_val.get().is_int32() {
id_val.get().to_int32().to_string()
} else {
continue;
};
let mut val = UndefinedValue();
let c_key = ZBox::from_bytes(key.as_bytes());
JS_GetProperty(
cx,
obj_root.handle().into(),
c_key.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
qs_push_query_pair(cx, &mut parts, &qs_encode_component(&key), val);
}
parts.join("&")
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn qs_push_query_pair(cx: *mut JSContext, parts: &mut Vec<String>, enc_key: &str, val: JSVal) {
if val.is_object() {
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let val_obj = val.to_object());
let mut is_arr = false;
if w2::IsArrayObject1(cx_ref, val_obj.handle().into(), &mut is_arr) && is_arr {
let mut len: u32 = 0;
if !w2::GetArrayLength(cx_ref, val_obj.handle().into(), &mut len) {
len = 0;
}
for i in 0..len {
rooted!(&in(cx_ref) let mut el = UndefinedValue());
w2::JS_GetElement(
cx_ref,
val_obj.handle().into(),
i,
el.handle_mut(),
);
let s = qs_value_to_string(cx, el.get()).unwrap_or_default();
parts.push(format!("{}={}", enc_key, qs_encode_component(&s)));
}
return;
}
parts.push(format!("{}=", enc_key));
return;
}
let scalar = val.is_string()
|| val.is_boolean()
|| val.is_int32()
|| val.is_double()
|| val.is_bigint();
let s = if scalar {
qs_value_to_string(cx, val).unwrap_or_default()
} else {
String::new()
};
parts.push(format!("{}={}", enc_key, qs_encode_component(&s)));
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn qs_value_to_string(cx: *mut JSContext, val: JSVal) -> Option<String> {
if val.is_string() {
return Some(crate::js_to_rust_string(cx, val));
}
let mut wrapped_cx =
mozjs::context::JSContext::from_ptr(NonNull::new_unchecked(cx));
let cx_ref = &mut wrapped_cx;
rooted!(&in(cx_ref) let val_root = val);
let jsstr = mozjs::rust::ToString(cx_ref, val_root.handle());
if jsstr.is_null() {
return None;
}
Some(crate::jsstr_to_rust_string(cx, jsstr))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_state() -> UrlState {
UrlState {
href: "https://user:pass@example.com:8080/path/name?query=1#frag".into(),
protocol: "https:".into(),
username: "user".into(),
password: "pass".into(),
host: "example.com:8080".into(),
hostname: "example.com".into(),
port: "8080".into(),
pathname: "/path/name".into(),
search: "?query=1".into(),
hash: "#frag".into(),
origin: "https://example.com:8080".into(),
}
}
#[test]
fn parse_url_empty_returns_none() {
assert!(parse_url("", None).is_none());
}
#[test]
fn parse_url_whitespace_returns_none() {
assert!(parse_url(" ", None).is_none());
}
#[test]
fn parse_url_data_text() {
let s = parse_url("data:text/html,hello", None).unwrap();
assert_eq!(s.protocol, "data:");
assert_eq!(s.pathname, "text/html,hello");
assert_eq!(s.origin, "null");
assert_eq!(s.href, "data:text/html,hello");
}
#[test]
fn parse_url_data_base64() {
let s = parse_url("data:image/png;base64,abc123", None).unwrap();
assert_eq!(s.protocol, "data:");
assert_eq!(s.pathname, "image/png;base64,abc123");
}
#[test]
fn parse_url_blob() {
let s = parse_url("blob:https://example.com/uuid-1234", None).unwrap();
assert_eq!(s.protocol, "blob:");
assert_eq!(s.pathname, "https://example.com/uuid-1234");
assert_eq!(s.origin, "null");
}
#[test]
fn parse_url_https_full() {
let s = parse_url("https://example.com/path", None).unwrap();
assert_eq!(s.protocol, "https:");
assert_eq!(s.hostname, "example.com");
assert_eq!(s.pathname, "/path");
assert_eq!(s.port, "");
assert_eq!(s.username, "");
assert_eq!(s.password, "");
}
#[test]
fn parse_url_with_port() {
let s = parse_url("http://localhost:3000/api", None).unwrap();
assert_eq!(s.hostname, "localhost");
assert_eq!(s.port, "3000");
assert_eq!(s.host, "localhost:3000");
assert_eq!(s.pathname, "/api");
}
#[test]
fn parse_url_with_userinfo() {
let s = parse_url("ftp://user:pass@ftp.example.com/file.txt", None).unwrap();
assert_eq!(s.username, "user");
assert_eq!(s.password, "pass");
assert_eq!(s.hostname, "ftp.example.com");
assert_eq!(s.pathname, "/file.txt");
}
#[test]
fn parse_url_user_no_password() {
let s = parse_url("https://admin@host.com/", None).unwrap();
assert_eq!(s.username, "admin");
assert_eq!(s.password, "");
}
#[test]
fn parse_url_query_and_hash() {
let s = parse_url("https://x.com/a?b=c#d", None).unwrap();
assert_eq!(s.search, "?b=c");
assert_eq!(s.hash, "#d");
assert_eq!(s.pathname, "/a");
}
#[test]
fn parse_url_hash_only() {
let s = parse_url("https://x.com/page#section", None).unwrap();
assert_eq!(s.search, "");
assert_eq!(s.hash, "#section");
}
#[test]
fn parse_url_hash_no_path() {
let s = parse_url("https://example.com#section", None).unwrap();
assert_eq!(s.hash, "#section");
}
#[test]
fn parse_url_query_only() {
let s = parse_url("https://x.com/search?q=rust", None).unwrap();
assert_eq!(s.search, "?q=rust");
assert_eq!(s.hash, "");
}
#[test]
fn parse_url_no_path_defaults_to_slash() {
let s = parse_url("https://example.com", None).unwrap();
assert_eq!(s.pathname, "/");
}
#[test]
fn parse_url_ipv6_with_port() {
let s = parse_url("http://[::1]:8080/path", None).unwrap();
assert_eq!(s.hostname, "[::1]");
assert_eq!(s.port, "8080");
assert_eq!(s.host, "[::1]:8080");
}
#[test]
fn parse_url_ipv6_no_port() {
let s = parse_url("http://[2001:db8::1]/path", None).unwrap();
assert_eq!(s.hostname, "[2001:db8::1]");
assert_eq!(s.port, "");
assert_eq!(s.host, "[2001:db8::1]");
}
#[test]
fn parse_url_absolute_path_with_base() {
let s = parse_url("/new/path", Some("https://example.com/old/path")).unwrap();
assert_eq!(s.protocol, "https:");
assert_eq!(s.hostname, "example.com");
assert_eq!(s.pathname, "/new/path");
}
#[test]
fn parse_url_relative_path_with_base() {
let s = parse_url("sub/page.html", Some("https://example.com/dir/old.html")).unwrap();
assert!(s.pathname.contains("sub/page.html"));
assert_eq!(s.hostname, "example.com");
}
#[test]
fn parse_url_query_relative_with_base() {
let s = parse_url("?new=1", Some("https://example.com/page?old=1")).unwrap();
assert_eq!(s.search, "?new=1");
assert!(s.href.contains("example.com"));
}
#[test]
fn parse_url_hash_relative_with_base() {
let s = parse_url("#new-section", Some("https://example.com/page")).unwrap();
assert_eq!(s.hash, "#new-section");
}
#[test]
fn parse_url_scheme_relative() {
let s = parse_url("//cdn.example.com/assets/img.png", None).unwrap();
assert_eq!(s.hostname, "cdn.example.com");
assert_eq!(s.pathname, "/assets/img.png");
assert_eq!(s.protocol, "http:");
}
#[test]
fn parse_url_relative_no_base_returns_none() {
assert!(parse_url("path/to/file", None).is_none());
}
#[test]
fn parse_url_slash_no_base_returns_none() {
assert!(parse_url("/path", None).is_none());
}
#[test]
fn parse_url_invalid_no_scheme_returns_none() {
assert!(parse_url("nocolon", None).is_none());
}
#[test]
fn parse_url_trims_whitespace() {
let s = parse_url(" https://example.com/ ", None).unwrap();
assert_eq!(s.hostname, "example.com");
}
#[test]
fn get_field_href() {
assert_eq!(
url_state_get_field(&make_state(), "href"),
"https://user:pass@example.com:8080/path/name?query=1#frag"
);
}
#[test]
fn get_field_protocol() {
assert_eq!(url_state_get_field(&make_state(), "protocol"), "https:");
}
#[test]
fn get_field_username() {
assert_eq!(url_state_get_field(&make_state(), "username"), "user");
}
#[test]
fn get_field_password() {
assert_eq!(url_state_get_field(&make_state(), "password"), "pass");
}
#[test]
fn get_field_host() {
assert_eq!(
url_state_get_field(&make_state(), "host"),
"example.com:8080"
);
}
#[test]
fn get_field_hostname() {
assert_eq!(
url_state_get_field(&make_state(), "hostname"),
"example.com"
);
}
#[test]
fn get_field_port() {
assert_eq!(url_state_get_field(&make_state(), "port"), "8080");
}
#[test]
fn get_field_pathname() {
assert_eq!(url_state_get_field(&make_state(), "pathname"), "/path/name");
}
#[test]
fn get_field_search() {
assert_eq!(url_state_get_field(&make_state(), "search"), "?query=1");
}
#[test]
fn get_field_hash() {
assert_eq!(url_state_get_field(&make_state(), "hash"), "#frag");
}
#[test]
fn get_field_origin() {
assert_eq!(
url_state_get_field(&make_state(), "origin"),
"https://example.com:8080"
);
}
#[test]
fn get_field_unknown_returns_empty() {
assert_eq!(url_state_get_field(&make_state(), "nonexistent"), "");
}
#[test]
fn rebuild_href_change_pathname() {
let state = make_state();
let result = rebuild_href(&state, "pathname", "/new/path");
assert!(result.contains("/new/path"));
assert!(result.contains("example.com:8080"));
assert!(result.contains("https:"));
}
#[test]
fn rebuild_href_change_hash() {
let state = make_state();
let result = rebuild_href(&state, "hash", "#new-frag");
assert!(result.contains("#new-frag"));
assert!(!result.contains("#frag"));
}
#[test]
fn rebuild_href_change_search() {
let state = make_state();
let result = rebuild_href(&state, "search", "?updated=true");
assert!(result.contains("?updated=true"));
assert!(!result.contains("?query=1"));
}
#[test]
fn rebuild_href_change_hostname() {
let state = make_state();
let result = rebuild_href(&state, "hostname", "other.com");
assert!(result.contains("other.com"));
}
#[test]
fn rebuild_href_change_port() {
let state = make_state();
let result = rebuild_href(&state, "port", "9090");
assert!(result.contains(":9090"));
}
#[test]
fn rebuild_href_remove_port() {
let state = make_state();
let result = rebuild_href(&state, "port", "");
assert!(!result.contains(":8080"));
}
#[test]
fn rebuild_href_change_username() {
let state = make_state();
let result = rebuild_href(&state, "username", "alice");
assert!(result.contains("alice:pass@"));
}
#[test]
fn rebuild_href_change_password() {
let state = make_state();
let result = rebuild_href(&state, "password", "secret");
assert!(result.contains("user:secret@"));
}
#[test]
fn rebuild_href_set_href_directly() {
let state = make_state();
let result = rebuild_href(&state, "href", "http://other.com/");
assert_eq!(result, "http://other.com/");
}
#[test]
fn rebuild_href_no_auth_in_state() {
let mut state = make_state();
state.username = String::new();
state.password = String::new();
let result = rebuild_href(&state, "pathname", "/x");
assert!(!result.contains("@"));
}
#[test]
fn rebuild_href_username_only_no_password() {
let mut state = make_state();
state.password = String::new();
let result = rebuild_href(&state, "pathname", "/x");
assert!(result.contains("user@"));
assert!(!result.contains("user:@"));
}
#[test]
fn url_encode_simple() {
assert_eq!(url_encode("hello"), "hello");
}
#[test]
fn url_encode_space_to_plus() {
assert_eq!(url_encode("a b"), "a+b");
}
#[test]
fn url_encode_special_chars() {
assert_eq!(url_encode("a&b=c"), "a%26b%3Dc");
}
#[test]
fn url_encode_unicode() {
let encoded = url_encode("日本語");
assert!(encoded.starts_with('%'));
}
#[test]
fn url_encode_unreserved_not_encoded() {
assert_eq!(url_encode("A-Z_.~"), "A-Z_.~");
}
#[test]
fn url_encode_empty() {
assert_eq!(url_encode(""), "");
}
#[test]
fn url_decode_simple() {
assert_eq!(url_decode("hello"), "hello");
}
#[test]
fn url_decode_plus_to_space() {
assert_eq!(url_decode("a+b"), "a b");
}
#[test]
fn url_decode_percent() {
assert_eq!(url_decode("a%26b%3Dc"), "a&b=c");
}
#[test]
fn url_decode_percent_hex() {
assert_eq!(url_decode("%41%42%43"), "ABC");
}
#[test]
fn url_decode_incomplete_percent_passthrough() {
assert_eq!(url_decode("a%2"), "a%2");
}
#[test]
fn url_decode_empty() {
assert_eq!(url_decode(""), "");
}
#[test]
fn url_decode_roundtrip() {
let original = "hello world & friends=true";
assert_eq!(url_decode(&url_encode(original)), original);
}
#[test]
fn url_decode_percent_lowercase_hex() {
assert_eq!(url_decode("%2f"), "/");
}
#[test]
fn url_encode_digits_not_encoded() {
assert_eq!(url_encode("0123456789"), "0123456789");
}
#[test]
fn url_encode_hyphen_underscore_dot_tilde_not_encoded() {
assert_eq!(url_encode("-_.~"), "-_.~");
}
#[test]
fn url_encode_slash_encoded() {
assert_eq!(url_encode("/path"), "%2Fpath");
}
#[test]
fn url_encode_colon_encoded() {
assert_eq!(url_encode("a:b"), "a%3Ab");
}
#[test]
fn url_encode_at_encoded() {
assert_eq!(url_encode("user@host"), "user%40host");
}
#[test]
fn url_encode_multiple_spaces() {
assert_eq!(url_encode("a b c"), "a+b+c");
}
#[test]
fn url_encode_all_special_chars() {
let encoded = url_encode("!@#$%^&*()");
assert!(!encoded.contains('!'));
assert!(!encoded.contains('@'));
assert!(!encoded.contains('#'));
}
#[test]
fn url_encode_null_byte() {
let input = "a\x00b";
let encoded = url_encode(input);
assert!(encoded.contains("%00"));
}
#[test]
fn url_encode_high_byte() {
let input = String::from_utf8_lossy(&[0xFF]).to_string();
let encoded = url_encode(&input);
assert!(encoded.starts_with('%'));
}
#[test]
fn url_decode_percent_at_end_passthrough() {
assert_eq!(url_decode("hello%"), "hello%");
}
#[test]
fn url_decode_percent_one_char_passthrough() {
assert_eq!(url_decode("hello%a"), "hello%a");
}
#[test]
fn url_decode_invalid_hex_passthrough() {
assert_eq!(url_decode("%GG"), "%GG");
}
#[test]
fn url_decode_mixed_case_hex() {
assert_eq!(url_decode("%2F%2f"), "//");
}
#[test]
fn url_decode_null_byte() {
assert_eq!(url_decode("%00"), "\0");
}
#[test]
fn url_decode_multiple_pluses() {
assert_eq!(url_decode("a+b+c"), "a b c");
}
#[test]
fn url_decode_consecutive_percents() {
assert_eq!(url_decode("%41%42"), "AB");
}
#[test]
fn parse_url_http_default_port() {
let s = parse_url("http://example.com/", None).unwrap();
assert_eq!(s.port, "");
assert_eq!(s.hostname, "example.com");
}
#[test]
fn parse_url_https_standard_port() {
let s = parse_url("https://example.com:443/path", None).unwrap();
assert_eq!(s.port, "443");
}
#[test]
fn parse_url_empty_pathname() {
let s = parse_url("https://example.com", None).unwrap();
assert_eq!(s.pathname, "/");
}
#[test]
fn parse_url_deep_path() {
let s = parse_url("https://example.com/a/b/c/d/e", None).unwrap();
assert_eq!(s.pathname, "/a/b/c/d/e");
}
#[test]
fn parse_url_long_query() {
let s = parse_url("https://x.com/?a=1&b=2&c=3&d=4", None).unwrap();
assert_eq!(s.search, "?a=1&b=2&c=3&d=4");
}
#[test]
fn parse_url_fragment_only_hash() {
let s = parse_url("https://x.com/#", None).unwrap();
assert_eq!(s.hash, "#");
}
#[test]
fn parse_url_empty_search() {
let s = parse_url("https://x.com/?", None).unwrap();
assert_eq!(s.search, "?");
}
#[test]
fn parse_url_password_with_special_chars() {
let s = parse_url("https://user:p@ss:w0rd@host.com/", None).unwrap();
assert_eq!(s.username, "user");
}
#[test]
fn parse_url_data_url_empty_pathname() {
let s = parse_url("data:,", None).unwrap();
assert_eq!(s.protocol, "data:");
}
#[test]
fn parse_url_blob_complex() {
let s = parse_url(
"blob:https://example.com/550e8400-e29b-41d4-a716-446655440000",
None,
)
.unwrap();
assert_eq!(s.protocol, "blob:");
assert!(s.pathname.contains("example.com"));
}
#[test]
fn parse_url_relative_with_base_directory() {
let s = parse_url("file.txt", Some("https://example.com/docs/readme.md")).unwrap();
assert_eq!(s.hostname, "example.com");
assert!(s.pathname.contains("file.txt"));
}
#[test]
fn rebuild_href_change_protocol() {
let state = make_state();
let result = rebuild_href(&state, "protocol", "http:");
assert!(result.starts_with("http://"));
}
#[test]
fn rebuild_href_clear_search_and_hash() {
let state = make_state();
let result = rebuild_href(&state, "search", "");
assert!(!result.contains("?query=1"));
let result2 = rebuild_href(&state, "hash", "");
assert!(!result2.contains("#frag"));
}
#[test]
fn rebuild_href_empty_hostname() {
let mut state = make_state();
state.hostname = String::new();
state.port = String::new();
state.host = String::new();
let result = rebuild_href(&state, "pathname", "/x");
assert!(result.contains("/x"));
}
#[test]
fn url_state_get_field_all_known_fields() {
let state = make_state();
assert_eq!(url_state_get_field(&state, "href"), state.href);
assert_eq!(url_state_get_field(&state, "protocol"), state.protocol);
assert_eq!(url_state_get_field(&state, "username"), state.username);
assert_eq!(url_state_get_field(&state, "password"), state.password);
assert_eq!(url_state_get_field(&state, "host"), state.host);
assert_eq!(url_state_get_field(&state, "hostname"), state.hostname);
assert_eq!(url_state_get_field(&state, "port"), state.port);
assert_eq!(url_state_get_field(&state, "pathname"), state.pathname);
assert_eq!(url_state_get_field(&state, "search"), state.search);
assert_eq!(url_state_get_field(&state, "hash"), state.hash);
assert_eq!(url_state_get_field(&state, "origin"), state.origin);
}
#[test]
fn url_state_get_field_unknown_returns_empty() {
let state = make_state();
assert_eq!(url_state_get_field(&state, "nonexistent"), "");
assert_eq!(url_state_get_field(&state, ""), "");
}
#[test]
fn url_state_get_field_empty_state() {
let state = UrlState {
href: String::new(),
protocol: String::new(),
username: String::new(),
password: String::new(),
host: String::new(),
hostname: String::new(),
port: String::new(),
pathname: String::new(),
search: String::new(),
hash: String::new(),
origin: String::new(),
};
assert_eq!(url_state_get_field(&state, "href"), "");
assert_eq!(url_state_get_field(&state, "protocol"), "");
}
}
fn rfc3986_remove_dot_segments(input: &str) -> String {
let mut output: Vec<&str> = Vec::new();
let mut rest = input;
while !rest.is_empty() {
let bytes = rest.as_bytes();
if rest.starts_with("../") {
rest = &rest[3..];
} else if rest.starts_with("./") {
rest = &rest[2..];
} else if rest.starts_with("/./") {
rest = &rest[2..];
} else if rest == "/." {
rest = "/";
} else if rest.starts_with("/../") {
rest = &rest[3..];
output.pop();
} else if rest == "/.." {
rest = "/";
output.pop();
} else if rest == "." || rest == ".." {
rest = "";
} else {
let start = if bytes[0] == b'/' { 1 } else { 0 };
let end = match rest[start..].find('/') {
Some(i) => start + i,
None => rest.len(),
};
output.push(&rest[..end]);
rest = &rest[end..];
}
}
output.concat()
}
fn rfc3986_split(
s: &str,
) -> (Option<String>, Option<String>, String, Option<String>, Option<String>) {
let mut rest = s;
let fragment = match rest.find('#') {
Some(i) => {
let f = Some(rest[i + 1..].to_string());
rest = &rest[..i];
f
}
None => None,
};
let query = match rest.find('?') {
Some(i) => {
let q = Some(rest[i + 1..].to_string());
rest = &rest[..i];
q
}
None => None,
};
let scheme = if let Some(colon) = rest.find(':') {
let candidate = &rest[..colon];
let valid = !candidate.is_empty()
&& candidate.chars().next().map(|c| c.is_ascii_alphabetic()).unwrap_or(false)
&& candidate
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.');
if valid {
let sc = Some(candidate.to_ascii_lowercase());
rest = &rest[colon + 1..];
sc
} else {
None
}
} else {
None
};
let authority = if rest.starts_with("//") {
let body = &rest[2..];
let end = body.find('/').unwrap_or(body.len());
let auth = Some(body[..end].to_string());
rest = &body[end..];
auth
} else {
None
};
(scheme, authority, rest.to_string(), query, fragment)
}
fn rfc3986_resolve(base: &str, reference: &str) -> String {
let (b_scheme, b_auth, b_path, b_query, _b_frag) = rfc3986_split(base);
let (r_scheme, r_auth, r_path, r_query, r_frag) = rfc3986_split(reference);
let (scheme, authority, mut path, query): (Option<String>, Option<String>, String, Option<String>);
if let Some(rs) = r_scheme {
scheme = Some(rs);
authority = r_auth;
path = rfc3986_remove_dot_segments(&r_path);
query = r_query;
} else {
scheme = b_scheme.clone();
if let Some(ra) = r_auth {
authority = Some(ra);
path = rfc3986_remove_dot_segments(&r_path);
query = r_query;
} else {
authority = b_auth.clone();
if r_path.is_empty() {
path = b_path.clone();
query = r_query.or(b_query);
} else {
if r_path.starts_with('/') {
path = rfc3986_remove_dot_segments(&r_path);
} else {
let merged = match b_auth.is_some() && b_path.is_empty() {
true => format!("/{}", r_path),
false => match b_path.rfind('/') {
Some(i) => format!("{}{}", &b_path[..=i], r_path),
None => r_path.clone(),
},
};
path = rfc3986_remove_dot_segments(&merged);
}
query = r_query;
}
if path.is_empty() && authority.is_some() {
path = "/".to_string();
}
}
}
let mut out = String::new();
if let Some(s) = &scheme {
out.push_str(s);
out.push(':');
}
if let Some(a) = &authority {
out.push_str("//");
out.push_str(a);
}
out.push_str(&path);
if let Some(q) = &query {
out.push('?');
out.push_str(q);
}
if let Some(f) = &r_frag {
out.push('#');
out.push_str(f);
}
out
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe extern "C" fn url_resolve_fn(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
let args = CallArgs::from_vp(vp, argc);
if argc < 2 || !(*args.get(0).ptr).is_string() || !(*args.get(1).ptr).is_string() {
args.rval().set(*args.get(0).ptr);
return true;
}
let base = crate::js_to_rust_string(cx, *args.get(0).ptr);
let relative = crate::js_to_rust_string(cx, *args.get(1).ptr);
let resolved = rfc3986_resolve(&base, &relative);
let c_str = ZBox::from_bytes(resolved.as_bytes());
let js_str = JS_NewStringCopyZ(cx, c_str.as_ptr());
args.rval().set(if js_str.is_null() {
UndefinedValue()
} else {
StringValue(&*js_str)
});
true
}