1#![allow(clippy::uninlined_format_args)]
4
5use std::{cell::RefCell, rc::Rc};
6
7use rquickjs::{
8 atom::PredefinedAtom, class::Trace, function::Opt, Class, Coerced, Ctx, Exception, FromJs,
9 IntoJs, Null, Object, Result, Value,
10};
11use url::{quirks, Url};
12
13use super::url_search_params::URLSearchParams;
14
15#[derive(Clone, Trace, rquickjs::JsLifetime)]
19#[rquickjs::class]
20pub struct URL<'js> {
21 #[qjs(skip_trace)]
22 url: Rc<RefCell<Url>>,
23 search_params: Class<'js, URLSearchParams>,
24}
25
26#[rquickjs::methods(rename_all = "camelCase")]
27impl<'js> URL<'js> {
28 #[qjs(constructor)]
29 pub fn new(ctx: Ctx<'js>, input: Value<'js>, base: Opt<Value<'js>>) -> Result<Self> {
30 let input: Result<String> = if input.is_string() {
35 crate::utils::bytes::get_lossy_string(input.clone())
36 } else {
37 Coerced::<String>::from_js(&ctx, input.clone()).map(|c| c.0)
38 };
39 if let Some(base) = base.into_inner() {
40 if let Some(base) = base.as_string() {
41 if let Ok(base) = base.to_string() {
42 let base_url: Url = base
43 .parse()
44 .map_err(|_| Exception::throw_type(&ctx, "Invalid base URL"))?;
45 let base_url = super::preserve_file_url_host(&base, base_url);
51 if let Ok(input) = input {
52 let mut joined = base_url
53 .join(input.as_str())
54 .map_err(|_| Exception::throw_type(&ctx, "Invalid URL"))?;
55 super::restore_file_url_host(&base_url, &mut joined);
56 return Self::from_url(ctx, joined);
57 }
58 return Self::from_str(ctx, &base);
59 }
60 }
61 }
62 if let Ok(input) = input {
63 Self::from_str(ctx, input.as_str())
64 } else {
65 Err(Exception::throw_message(&ctx, "Invalid URL"))
66 }
67 }
68
69 #[qjs(get)]
70 pub fn hash(&self) -> String {
71 quirks::hash(&self.url.borrow()).to_string()
72 }
73
74 #[qjs(set, rename = "hash")]
75 pub fn set_hash(&mut self, hash: String) -> String {
76 self.before_mutation();
77 quirks::set_hash(&mut self.url.borrow_mut(), &hash);
78 hash
79 }
80
81 #[qjs(get)]
82 pub fn host(&self) -> String {
83 quirks::host(&self.url.borrow()).to_string()
84 }
85
86 #[qjs(set, rename = "host")]
87 pub fn set_host(&mut self, host: Coerced<String>) -> String {
88 self.before_mutation();
89 let _ = quirks::set_host(&mut self.url.borrow_mut(), &host);
90 host.0
91 }
92
93 #[qjs(get)]
94 pub fn hostname(&self) -> String {
95 quirks::hostname(&self.url.borrow()).to_string()
96 }
97
98 #[qjs(set, rename = "hostname")]
99 pub fn set_hostname(&mut self, hostname: Coerced<String>) -> String {
100 self.before_mutation();
101 let _ = quirks::set_hostname(&mut self.url.borrow_mut(), hostname.as_str());
102 super::strip_path_sentinel(&mut self.url.borrow_mut());
103 hostname.0
104 }
105
106 #[qjs(get)]
107 pub fn href(&self) -> String {
108 quirks::href(&self.url.borrow()).to_string()
109 }
110
111 #[qjs(set, rename = "href")]
112 pub fn set_href(&mut self, href: String) -> String {
113 self.before_mutation();
114 let _ = quirks::set_href(&mut self.url.borrow_mut(), &href);
115 href
116 }
117
118 #[qjs(get)]
119 pub fn origin(&self) -> String {
120 let url = self.url.borrow();
121 if url.scheme() == "blob" {
128 return match url::Url::parse(url.path()) {
129 Ok(inner) if matches!(inner.scheme(), "http" | "https") => quirks::origin(&inner),
130 _ => "null".into(),
131 };
132 }
133 quirks::origin(&url)
134 }
135
136 #[qjs(get)]
137 pub fn password(&self) -> String {
138 quirks::password(&self.url.borrow()).to_string()
139 }
140
141 #[qjs(set, rename = "password")]
142 pub fn set_password(&mut self, password: Coerced<String>) -> String {
143 self.before_mutation();
144 let _ = quirks::set_password(&mut self.url.borrow_mut(), &password);
145 password.0
146 }
147
148 #[qjs(get)]
149 pub fn pathname(&self) -> String {
150 quirks::pathname(&self.url.borrow()).to_string()
151 }
152
153 #[qjs(set, rename = "pathname")]
154 pub fn set_pathname(&mut self, pathname: Coerced<String>) -> String {
155 self.before_mutation();
156 quirks::set_pathname(&mut self.url.borrow_mut(), pathname.as_str());
157 if pathname.0.is_empty() {
163 super::erase_empty_host_path(&mut self.url.borrow_mut());
164 }
165 pathname.0
166 }
167
168 #[qjs(get)]
169 pub fn port(&self) -> String {
170 quirks::port(&self.url.borrow()).to_string()
171 }
172
173 #[qjs(set, rename = "port")]
174 pub fn set_port(&mut self, ctx: Ctx<'js>, port: Value<'js>) -> Value<'js> {
175 if port.is_null()
176 || port.is_undefined()
177 || (port.is_int() && unsafe { port.as_int().unwrap_unchecked() } < 0)
178 {
179 return port;
180 }
181 if let Ok(port_string) = Coerced::<String>::from_js(&ctx, port.clone()) {
182 self.before_mutation();
183 if port_string.is_empty() {
189 let _ = quirks::set_port(&mut self.url.borrow_mut(), "");
190 } else {
191 let stripped: String = port_string
192 .chars()
193 .filter(|c| !matches!(c, '\t' | '\n' | '\r'))
194 .collect();
195 if !stripped.is_empty() {
196 let _ = quirks::set_port(&mut self.url.borrow_mut(), &stripped);
197 }
198 }
200 }
201 port
202 }
203
204 #[qjs(get)]
205 pub fn protocol(&self) -> String {
206 quirks::protocol(&self.url.borrow()).to_string()
207 }
208
209 #[qjs(set, rename = "protocol")]
210 pub fn set_protocol(&mut self, protocol: Coerced<String>) -> String {
211 self.before_mutation();
212 let _ = quirks::set_protocol(&mut self.url.borrow_mut(), &protocol);
213 protocol.0
214 }
215
216 #[qjs(get)]
217 pub fn search(&self) -> String {
218 quirks::search(&self.url.borrow()).to_string()
219 }
220
221 #[qjs(set, rename = "search")]
222 pub fn set_search(&mut self, search: Coerced<String>) -> String {
223 self.before_mutation();
224 quirks::set_search(&mut self.url.borrow_mut(), &search);
225 search.0
226 }
227
228 #[qjs(get)]
229 pub fn search_params(&self) -> &Value<'js> {
230 self.search_params.as_value()
231 }
232
233 #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
234 pub fn to_string_tag() -> &'static str {
235 stringify!(URL)
236 }
237
238 #[qjs(get)]
239 pub fn username(&self) -> String {
240 quirks::username(&self.url.borrow()).to_string()
241 }
242
243 #[qjs(set, rename = "username")]
244 pub fn set_username(&mut self, username: Coerced<String>) -> String {
245 self.before_mutation();
246 let _ = quirks::set_username(&mut self.url.borrow_mut(), &username);
247 username.0
248 }
249
250 #[qjs(static)]
251 pub fn can_parse(ctx: Ctx<'js>, input: Value<'js>, base: Opt<Value<'js>>) -> bool {
252 Self::new(ctx, input, base).is_ok()
253 }
254
255 #[qjs(static)]
256 pub fn parse(ctx: Ctx<'js>, input: Value<'js>, base: Opt<Value<'js>>) -> Result<Value<'js>> {
257 Self::new(ctx.clone(), input, base)
258 .map_or_else(|_| Null.into_js(&ctx), |instance| instance.into_js(&ctx))
259 }
260
261 #[qjs(rename = PredefinedAtom::ToJSON)]
262 pub fn to_json(&self) -> String {
263 self.to_string()
264 }
265
266 pub fn to_string(&self) -> String {
267 self.href()
268 }
269}
270
271impl<'js> URL<'js> {
272 pub fn from_str(ctx: Ctx<'js>, input: &str) -> Result<Self> {
273 let mut url: Url = input
274 .parse()
275 .map_err(|_| Exception::throw_type(&ctx, "Invalid URL"))?;
276 super::normalize_windows_drive_letter(&mut url);
277 super::convert_trailing_space(&mut url);
278 Self::build(ctx, url)
279 }
280
281 pub fn from_url(ctx: Ctx<'js>, mut url: Url) -> Result<Self> {
282 super::normalize_windows_drive_letter(&mut url);
283 super::convert_trailing_space(&mut url);
284 Self::build(ctx, url)
285 }
286
287 pub fn is_valid(input: &str) -> bool {
291 input.parse::<Url>().is_ok()
292 }
293
294 fn build(ctx: Ctx<'js>, url: Url) -> Result<Self> {
295 let shared = Rc::new(RefCell::new(url));
296 let search_params = Class::instance(ctx, URLSearchParams::from_url(&shared))?;
297 Ok(Self {
298 url: shared,
299 search_params,
300 })
301 }
302
303 fn before_mutation(&mut self) {
304 super::convert_trailing_space(&mut self.url.borrow_mut());
305 }
306}
307
308fn percent_decode(component: &str) -> String {
313 percent_encoding::percent_decode_str(component)
314 .decode_utf8_lossy()
315 .into_owned()
316}
317
318pub fn url_to_http_options<'js>(ctx: Ctx<'js>, url: Class<'js, URL<'js>>) -> Result<Object<'js>> {
319 let obj = Object::new(ctx)?;
320 let url = url.borrow();
321
322 let port = url.port();
323 let username = url.username();
324 let password = url.password();
325 let search = url.search();
326 let hostname = url.hostname();
327
328 obj.set("protocol", url.protocol())?;
329 obj.set(
332 "hostname",
333 hostname
334 .strip_prefix('[')
335 .and_then(|rest| rest.strip_suffix(']'))
336 .unwrap_or(&hostname)
337 .to_string(),
338 )?;
339 obj.set("hash", url.hash())?;
340
341 let pathname = url.pathname();
342 let path = [pathname.as_str(), search.as_str()].concat();
343 obj.set("search", search)?;
344 obj.set("pathname", pathname)?;
345 obj.set("path", path)?;
346 obj.set("href", url.href())?;
347
348 if !username.is_empty() || !password.is_empty() {
349 obj.set(
350 "auth",
351 [percent_decode(&username), percent_decode(&password)].join(":"),
352 )?;
353 }
354
355 if !port.is_empty() {
356 obj.set("port", port.parse::<u32>().unwrap_or_default())?;
359 }
360
361 Ok(obj)
362}