Skip to main content

ada_url/
lib.rs

1//! # Ada URL
2//!
3//! Ada is a fast and spec-compliant URL parser written in C++.
4//! - It's widely tested by both Web Platform Tests and Google OSS Fuzzer.
5//! - It is extremely fast.
6//! - It's the default URL parser of Node.js since Node 18.16.0.
7//! - It supports Unicode Technical Standard.
8//!
9//! The Ada library passes the full range of tests from the specification, across a wide range
10//! of platforms (e.g., Windows, Linux, macOS).
11//!
12//! ## Performance
13//!
14//! Ada is extremely fast.
15//! For more information read our [benchmark page](https://ada-url.com/docs/performance).
16//!
17//! ```text
18//!      ada  ▏  188 ns/URL ███▏
19//! servo url ▏  664 ns/URL ███████████▎
20//!     CURL  ▏ 1471 ns/URL █████████████████████████
21//! ```
22//!
23//! ## serde
24//!
25//! If you enable the `serde` feature, [`Url`](struct.Url.html) will implement
26//! [`serde::Serialize`](https://docs.rs/serde/1/serde/trait.Serialize.html) and
27//! [`serde::Deserialize`](https://docs.rs/serde/1/serde/trait.Deserialize.html).
28//! See [serde documentation](https://serde.rs) for more information.
29//!
30//! ```toml
31//! ada-url = { version = "1", features = ["serde"] }
32//! ```
33//!
34//! ## no-std
35//!
36//! Whilst `ada-url` has `std` feature enabled by default, you can set `no-default-features`
37//! get a subset of features that work in no-std environment.
38//!
39//! ```toml
40//! ada-url = { version = "1", no-default-features = true }
41//! ```
42
43#![cfg_attr(not(feature = "std"), no_std)]
44
45mod ffi;
46mod idna;
47mod url_search_params;
48pub use idna::Idna;
49pub use url_search_params::{
50    UrlSearchParams, UrlSearchParamsEntry, UrlSearchParamsEntryIterator,
51    UrlSearchParamsKeyIterator, UrlSearchParamsValueIterator,
52};
53
54#[cfg(feature = "std")]
55extern crate std;
56
57#[cfg(feature = "std")]
58use std::string::String;
59
60use core::{borrow, ffi::c_uint, fmt, hash, ops};
61
62/// Error type of [`Url::parse`].
63#[derive(Debug, PartialEq, Eq)]
64pub struct ParseUrlError<Input> {
65    /// The invalid input that caused the error.
66    pub input: Input,
67}
68
69impl<Input: core::fmt::Debug> fmt::Display for ParseUrlError<Input> {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "Invalid url: {:?}", self.input)
72    }
73}
74
75#[cfg(feature = "std")] // error still requires std: https://github.com/rust-lang/rust/issues/103765
76impl<Input: core::fmt::Debug> std::error::Error for ParseUrlError<Input> {}
77
78/// Defines the type of the host.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum HostType {
81    Domain = 0,
82    IPV4 = 1,
83    IPV6 = 2,
84}
85
86impl From<c_uint> for HostType {
87    fn from(value: c_uint) -> Self {
88        match value {
89            0 => Self::Domain,
90            1 => Self::IPV4,
91            2 => Self::IPV6,
92            _ => Self::Domain,
93        }
94    }
95}
96
97/// Defines the scheme type of the url.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum SchemeType {
100    Http = 0,
101    NotSpecial = 1,
102    Https = 2,
103    Ws = 3,
104    Ftp = 4,
105    Wss = 5,
106    File = 6,
107}
108
109impl From<c_uint> for SchemeType {
110    fn from(value: c_uint) -> Self {
111        match value {
112            0 => Self::Http,
113            1 => Self::NotSpecial,
114            2 => Self::Https,
115            3 => Self::Ws,
116            4 => Self::Ftp,
117            5 => Self::Wss,
118            6 => Self::File,
119            _ => Self::NotSpecial,
120        }
121    }
122}
123
124/// Components are a serialization-free representation of a URL.
125/// For usages where string serialization has a high cost, you can
126/// use url components with `href` attribute.
127///
128/// By using 32-bit integers, we implicitly assume that the URL string
129/// cannot exceed 4 GB.
130///
131/// ```text
132/// https://user:pass@example.com:1234/foo/bar?baz#quux
133///       |     |    |          | ^^^^|       |   |
134///       |     |    |          | |   |       |   `----- hash_start
135///       |     |    |          | |   |       `--------- search_start
136///       |     |    |          | |   `----------------- pathname_start
137///       |     |    |          | `--------------------- port
138///       |     |    |          `----------------------- host_end
139///       |     |    `---------------------------------- host_start
140///       |     `--------------------------------------- username_end
141///       `--------------------------------------------- protocol_end
142/// ```
143#[derive(Debug)]
144pub struct UrlComponents {
145    pub protocol_end: u32,
146    pub username_end: u32,
147    pub host_start: u32,
148    pub host_end: u32,
149    pub port: Option<u32>,
150    pub pathname_start: Option<u32>,
151    pub search_start: Option<u32>,
152    pub hash_start: Option<u32>,
153}
154
155impl From<&ffi::ada_url_components> for UrlComponents {
156    fn from(value: &ffi::ada_url_components) -> Self {
157        let port = (value.port != u32::MAX).then_some(value.port);
158        let pathname_start = (value.pathname_start != u32::MAX).then_some(value.pathname_start);
159        let search_start = (value.search_start != u32::MAX).then_some(value.search_start);
160        let hash_start = (value.hash_start != u32::MAX).then_some(value.hash_start);
161        Self {
162            protocol_end: value.protocol_end,
163            username_end: value.username_end,
164            host_start: value.host_start,
165            host_end: value.host_end,
166            port,
167            pathname_start,
168            search_start,
169            hash_start,
170        }
171    }
172}
173
174/// A parsed URL struct according to WHATWG URL specification.
175#[derive(Eq)]
176pub struct Url(*mut ffi::ada_url);
177
178/// Clone trait by default uses bit-wise copy.
179/// In Rust, FFI requires deep copy, which requires an additional/inexpensive FFI call.
180impl Clone for Url {
181    fn clone(&self) -> Self {
182        unsafe { ffi::ada_copy(self.0).into() }
183    }
184}
185
186impl Drop for Url {
187    fn drop(&mut self) {
188        unsafe { ffi::ada_free(self.0) }
189    }
190}
191
192impl From<*mut ffi::ada_url> for Url {
193    fn from(value: *mut ffi::ada_url) -> Self {
194        Self(value)
195    }
196}
197
198type SetterResult = Result<(), ()>;
199
200#[inline]
201const fn setter_result(successful: bool) -> SetterResult {
202    if successful { Ok(()) } else { Err(()) }
203}
204
205impl Url {
206    /// Parses the input with an optional base
207    ///
208    /// ```
209    /// use ada_url::Url;
210    /// let out = Url::parse("https://ada-url.github.io/ada", None)
211    ///     .expect("This is a valid URL. Should have parsed it.");
212    /// assert_eq!(out.protocol(), "https:");
213    /// ```
214    pub fn parse<Input>(input: Input, base: Option<&str>) -> Result<Self, ParseUrlError<Input>>
215    where
216        Input: AsRef<str>,
217    {
218        let url_aggregator = match base {
219            Some(base) => unsafe {
220                ffi::ada_parse_with_base(
221                    input.as_ref().as_ptr().cast(),
222                    input.as_ref().len(),
223                    base.as_ptr().cast(),
224                    base.len(),
225                )
226            },
227            None => unsafe { ffi::ada_parse(input.as_ref().as_ptr().cast(), input.as_ref().len()) },
228        };
229
230        if unsafe { ffi::ada_is_valid(url_aggregator) } {
231            Ok(url_aggregator.into())
232        } else {
233            // `ada_parse`/`ada_parse_with_base` always allocate a result on the
234            // heap, even when parsing fails. On the success path the pointer is
235            // owned by `Url` and freed via its `Drop` impl, but on the error path
236            // we must free it here to avoid leaking the allocation.
237            unsafe { ffi::ada_free(url_aggregator) };
238            Err(ParseUrlError { input })
239        }
240    }
241
242    /// Returns whether or not the URL can be parsed or not.
243    ///
244    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-canparse)
245    ///
246    /// ```
247    /// use ada_url::Url;
248    /// assert!(Url::can_parse("https://ada-url.github.io/ada", None));
249    /// assert!(Url::can_parse("/pathname", Some("https://ada-url.github.io/ada")));
250    /// ```
251    #[must_use]
252    pub fn can_parse(input: &str, base: Option<&str>) -> bool {
253        unsafe {
254            if let Some(base) = base {
255                ffi::ada_can_parse_with_base(
256                    input.as_ptr().cast(),
257                    input.len(),
258                    base.as_ptr().cast(),
259                    base.len(),
260                )
261            } else {
262                ffi::ada_can_parse(input.as_ptr().cast(), input.len())
263            }
264        }
265    }
266
267    /// Returns the type of the host such as default, ipv4 or ipv6.
268    #[must_use]
269    pub fn host_type(&self) -> HostType {
270        HostType::from(unsafe { ffi::ada_get_host_type(self.0) })
271    }
272
273    /// Returns the type of the scheme such as http, https, etc.
274    #[must_use]
275    pub fn scheme_type(&self) -> SchemeType {
276        SchemeType::from(unsafe { ffi::ada_get_scheme_type(self.0) })
277    }
278
279    /// Return the origin of this URL
280    ///
281    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-origin)
282    ///
283    /// ```
284    /// use ada_url::Url;
285    ///
286    /// let url = Url::parse("blob:https://example.com/foo", None).expect("Invalid URL");
287    /// assert_eq!(url.origin(), "https://example.com");
288    /// ```
289    #[must_use]
290    #[cfg(feature = "std")]
291    pub fn origin(&self) -> String {
292        unsafe { ffi::ada_get_origin(self.0) }.to_string()
293    }
294
295    /// Return the parsed version of the URL with all components.
296    ///
297    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-href)
298    #[must_use]
299    pub fn href(&self) -> &str {
300        unsafe { ffi::ada_get_href(self.0) }.as_str()
301    }
302
303    /// Updates the href of the URL, and triggers the URL parser.
304    ///
305    /// ```
306    /// use ada_url::Url;
307    ///
308    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
309    /// url.set_href("https://lemire.me").unwrap();
310    /// assert_eq!(url.href(), "https://lemire.me/");
311    /// ```
312    #[allow(clippy::result_unit_err)]
313    pub fn set_href(&mut self, input: &str) -> SetterResult {
314        setter_result(unsafe { ffi::ada_set_href(self.0, input.as_ptr().cast(), input.len()) })
315    }
316
317    /// Return the username for this URL as a percent-encoded ASCII string.
318    ///
319    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-username)
320    ///
321    /// ```
322    /// use ada_url::Url;
323    ///
324    /// let url = Url::parse("ftp://rms:secret123@example.com", None).expect("Invalid URL");
325    /// assert_eq!(url.username(), "rms");
326    /// ```
327    #[must_use]
328    pub fn username(&self) -> &str {
329        unsafe { ffi::ada_get_username(self.0) }.as_str()
330    }
331
332    /// Updates the `username` of the URL.
333    ///
334    /// ```
335    /// use ada_url::Url;
336    ///
337    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
338    /// url.set_username(Some("username")).unwrap();
339    /// assert_eq!(url.href(), "https://username@yagiz.co/");
340    /// ```
341    #[allow(clippy::result_unit_err)]
342    pub fn set_username(&mut self, input: Option<&str>) -> SetterResult {
343        setter_result(unsafe {
344            ffi::ada_set_username(
345                self.0,
346                input.unwrap_or("").as_ptr().cast(),
347                input.map_or(0, str::len),
348            )
349        })
350    }
351
352    /// Return the password for this URL, if any, as a percent-encoded ASCII string.
353    ///
354    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-password)
355    ///
356    /// ```
357    /// use ada_url::Url;
358    ///
359    /// let url = Url::parse("ftp://rms:secret123@example.com", None).expect("Invalid URL");
360    /// assert_eq!(url.password(), "secret123");
361    /// ```
362    #[must_use]
363    pub fn password(&self) -> &str {
364        unsafe { ffi::ada_get_password(self.0) }.as_str()
365    }
366
367    /// Updates the `password` of the URL.
368    ///
369    /// ```
370    /// use ada_url::Url;
371    ///
372    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
373    /// url.set_password(Some("password")).unwrap();
374    /// assert_eq!(url.href(), "https://:password@yagiz.co/");
375    /// ```
376    #[allow(clippy::result_unit_err)]
377    pub fn set_password(&mut self, input: Option<&str>) -> SetterResult {
378        setter_result(unsafe {
379            ffi::ada_set_password(
380                self.0,
381                input.unwrap_or("").as_ptr().cast(),
382                input.map_or(0, str::len),
383            )
384        })
385    }
386
387    /// Return the port number for this URL, or an empty string.
388    ///
389    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-port)
390    ///
391    /// ```
392    /// use ada_url::Url;
393    ///
394    /// let url = Url::parse("https://example.com", None).expect("Invalid URL");
395    /// assert_eq!(url.port(), "");
396    ///
397    /// let url = Url::parse("https://example.com:8080", None).expect("Invalid URL");
398    /// assert_eq!(url.port(), "8080");
399    /// ```
400    #[must_use]
401    pub fn port(&self) -> &str {
402        unsafe { ffi::ada_get_port(self.0) }.as_str()
403    }
404
405    /// Updates the `port` of the URL.
406    ///
407    /// ```
408    /// use ada_url::Url;
409    ///
410    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
411    /// url.set_port(Some("8080")).unwrap();
412    /// assert_eq!(url.href(), "https://yagiz.co:8080/");
413    /// ```
414    #[allow(clippy::result_unit_err)]
415    pub fn set_port(&mut self, input: Option<&str>) -> SetterResult {
416        if let Some(value) = input {
417            setter_result(unsafe { ffi::ada_set_port(self.0, value.as_ptr().cast(), value.len()) })
418        } else {
419            unsafe { ffi::ada_clear_port(self.0) }
420            Ok(())
421        }
422    }
423
424    /// Return this URL’s fragment identifier, or an empty string.
425    /// A fragment is the part of the URL with the # symbol.
426    /// The fragment is optional and, if present, contains a fragment identifier that identifies
427    /// a secondary resource, such as a section heading of a document.
428    /// In HTML, the fragment identifier is usually the id attribute of a an element that is
429    /// scrolled to on load. Browsers typically will not send the fragment portion of a URL to the
430    /// server.
431    ///
432    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-hash)
433    ///
434    /// ```
435    /// use ada_url::Url;
436    ///
437    /// let url = Url::parse("https://example.com/data.csv#row=4", None).expect("Invalid URL");
438    /// assert_eq!(url.hash(), "#row=4");
439    /// assert!(url.has_hash());
440    /// ```
441    #[must_use]
442    pub fn hash(&self) -> &str {
443        unsafe { ffi::ada_get_hash(self.0) }.as_str()
444    }
445
446    /// Updates the `hash` of the URL.
447    ///
448    /// ```
449    /// use ada_url::Url;
450    ///
451    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
452    /// url.set_hash(Some("this-is-my-hash"));
453    /// assert_eq!(url.href(), "https://yagiz.co/#this-is-my-hash");
454    /// ```
455    pub fn set_hash(&mut self, input: Option<&str>) {
456        match input {
457            Some(value) => unsafe { ffi::ada_set_hash(self.0, value.as_ptr().cast(), value.len()) },
458            None => unsafe { ffi::ada_clear_hash(self.0) },
459        }
460    }
461
462    /// Return the parsed representation of the host for this URL with an optional port number.
463    ///
464    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-host)
465    ///
466    /// ```
467    /// use ada_url::Url;
468    ///
469    /// let url = Url::parse("https://127.0.0.1:8080/index.html", None).expect("Invalid URL");
470    /// assert_eq!(url.host(), "127.0.0.1:8080");
471    /// ```
472    #[must_use]
473    pub fn host(&self) -> &str {
474        unsafe { ffi::ada_get_host(self.0) }.as_str()
475    }
476
477    /// Updates the `host` of the URL.
478    ///
479    /// ```
480    /// use ada_url::Url;
481    ///
482    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
483    /// url.set_host(Some("localhost:3000")).unwrap();
484    /// assert_eq!(url.href(), "https://localhost:3000/");
485    /// ```
486    #[allow(clippy::result_unit_err)]
487    pub fn set_host(&mut self, input: Option<&str>) -> SetterResult {
488        setter_result(unsafe {
489            ffi::ada_set_host(
490                self.0,
491                input.unwrap_or("").as_ptr().cast(),
492                input.map_or(0, str::len),
493            )
494        })
495    }
496
497    /// Return the parsed representation of the host for this URL. Non-ASCII domain labels are
498    /// punycode-encoded per IDNA if this is the host of a special URL, or percent encoded for
499    /// non-special URLs.
500    ///
501    /// Hostname does not contain port number.
502    ///
503    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-hostname)
504    ///
505    /// ```
506    /// use ada_url::Url;
507    ///
508    /// let url = Url::parse("https://127.0.0.1:8080/index.html", None).expect("Invalid URL");
509    /// assert_eq!(url.hostname(), "127.0.0.1");
510    /// ```
511    #[must_use]
512    pub fn hostname(&self) -> &str {
513        unsafe { ffi::ada_get_hostname(self.0) }.as_str()
514    }
515
516    /// Updates the `hostname` of the URL.
517    ///
518    /// ```
519    /// use ada_url::Url;
520    ///
521    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
522    /// url.set_hostname(Some("localhost")).unwrap();
523    /// assert_eq!(url.href(), "https://localhost/");
524    /// ```
525    #[allow(clippy::result_unit_err)]
526    pub fn set_hostname(&mut self, input: Option<&str>) -> SetterResult {
527        setter_result(unsafe {
528            ffi::ada_set_hostname(
529                self.0,
530                input.unwrap_or("").as_ptr().cast(),
531                input.map_or(0, str::len),
532            )
533        })
534    }
535
536    /// Return the path for this URL, as a percent-encoded ASCII string.
537    ///
538    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-pathname)
539    ///
540    /// ```
541    /// use ada_url::Url;
542    ///
543    /// let url = Url::parse("https://example.com/api/versions?page=2", None).expect("Invalid URL");
544    /// assert_eq!(url.pathname(), "/api/versions");
545    /// ```
546    #[must_use]
547    pub fn pathname(&self) -> &str {
548        unsafe { ffi::ada_get_pathname(self.0) }.as_str()
549    }
550
551    /// Updates the `pathname` of the URL.
552    ///
553    /// ```
554    /// use ada_url::Url;
555    ///
556    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
557    /// url.set_pathname(Some("/contact")).unwrap();
558    /// assert_eq!(url.href(), "https://yagiz.co/contact");
559    /// ```
560    #[allow(clippy::result_unit_err)]
561    pub fn set_pathname(&mut self, input: Option<&str>) -> SetterResult {
562        setter_result(unsafe {
563            ffi::ada_set_pathname(
564                self.0,
565                input.unwrap_or("").as_ptr().cast(),
566                input.map_or(0, str::len),
567            )
568        })
569    }
570
571    /// Return this URL’s query string, if any, as a percent-encoded ASCII string.
572    ///
573    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-search)
574    ///
575    /// ```
576    /// use ada_url::Url;
577    ///
578    /// let url = Url::parse("https://example.com/products?page=2", None).expect("Invalid URL");
579    /// assert_eq!(url.search(), "?page=2");
580    ///
581    /// let url = Url::parse("https://example.com/products", None).expect("Invalid URL");
582    /// assert_eq!(url.search(), "");
583    /// ```
584    #[must_use]
585    pub fn search(&self) -> &str {
586        unsafe { ffi::ada_get_search(self.0) }.as_str()
587    }
588
589    /// Updates the `search` of the URL.
590    ///
591    /// ```
592    /// use ada_url::Url;
593    ///
594    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
595    /// url.set_search(Some("?page=1"));
596    /// assert_eq!(url.href(), "https://yagiz.co/?page=1");
597    /// ```
598    pub fn set_search(&mut self, input: Option<&str>) {
599        match input {
600            Some(value) => unsafe {
601                ffi::ada_set_search(self.0, value.as_ptr().cast(), value.len());
602            },
603            None => unsafe { ffi::ada_clear_search(self.0) },
604        }
605    }
606
607    /// Return the scheme of this URL, lower-cased, as an ASCII string with the ‘:’ delimiter.
608    ///
609    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-protocol)
610    ///
611    /// ```
612    /// use ada_url::Url;
613    ///
614    /// let url = Url::parse("file:///tmp/foo", None).expect("Invalid URL");
615    /// assert_eq!(url.protocol(), "file:");
616    /// ```
617    #[must_use]
618    pub fn protocol(&self) -> &str {
619        unsafe { ffi::ada_get_protocol(self.0) }.as_str()
620    }
621
622    /// Updates the `protocol` of the URL.
623    ///
624    /// ```
625    /// use ada_url::Url;
626    ///
627    /// let mut url = Url::parse("http://yagiz.co", None).expect("Invalid URL");
628    /// url.set_protocol("http").unwrap();
629    /// assert_eq!(url.href(), "http://yagiz.co/");
630    /// ```
631    #[allow(clippy::result_unit_err)]
632    pub fn set_protocol(&mut self, input: &str) -> SetterResult {
633        setter_result(unsafe { ffi::ada_set_protocol(self.0, input.as_ptr().cast(), input.len()) })
634    }
635
636    /// A URL includes credentials if its username or password is not the empty string.
637    #[must_use]
638    pub fn has_credentials(&self) -> bool {
639        unsafe { ffi::ada_has_credentials(self.0) }
640    }
641
642    /// Returns true if it has an host but it is the empty string.
643    #[must_use]
644    pub fn has_empty_hostname(&self) -> bool {
645        unsafe { ffi::ada_has_empty_hostname(self.0) }
646    }
647
648    /// Returns true if it has a host (included an empty host)
649    #[must_use]
650    pub fn has_hostname(&self) -> bool {
651        unsafe { ffi::ada_has_hostname(self.0) }
652    }
653
654    /// Returns true if URL has a non-empty username.
655    #[must_use]
656    pub fn has_non_empty_username(&self) -> bool {
657        unsafe { ffi::ada_has_non_empty_username(self.0) }
658    }
659
660    /// Returns true if URL has a non-empty password.
661    #[must_use]
662    pub fn has_non_empty_password(&self) -> bool {
663        unsafe { ffi::ada_has_non_empty_password(self.0) }
664    }
665
666    /// Returns true if URL has a port.
667    #[must_use]
668    pub fn has_port(&self) -> bool {
669        unsafe { ffi::ada_has_port(self.0) }
670    }
671
672    /// Returns true if URL has password.
673    #[must_use]
674    pub fn has_password(&self) -> bool {
675        unsafe { ffi::ada_has_password(self.0) }
676    }
677
678    /// Returns true if URL has a hash/fragment.
679    #[must_use]
680    pub fn has_hash(&self) -> bool {
681        unsafe { ffi::ada_has_hash(self.0) }
682    }
683
684    /// Returns true if URL has search/query.
685    #[must_use]
686    pub fn has_search(&self) -> bool {
687        unsafe { ffi::ada_has_search(self.0) }
688    }
689
690    /// Returns the parsed version of the URL with all components.
691    ///
692    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-href)
693    #[must_use]
694    pub fn as_str(&self) -> &str {
695        self.href()
696    }
697
698    /// Returns the URL components of the instance.
699    #[must_use]
700    pub fn components(&self) -> UrlComponents {
701        unsafe { ffi::ada_get_components(self.0).as_ref().unwrap() }.into()
702    }
703}
704
705/// Serializes this URL into a `serde` stream.
706///
707/// This implementation is only available if the `serde` Cargo feature is enabled.
708#[cfg(feature = "serde")]
709impl serde::Serialize for Url {
710    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
711    where
712        S: serde::Serializer,
713    {
714        serializer.serialize_str(self.as_str())
715    }
716}
717
718/// Deserializes this URL from a `serde` stream.
719///
720/// This implementation is only available if the `serde` Cargo feature is enabled.
721#[cfg(feature = "serde")]
722#[cfg(feature = "std")]
723impl<'de> serde::Deserialize<'de> for Url {
724    fn deserialize<D>(deserializer: D) -> Result<Url, D::Error>
725    where
726        D: serde::Deserializer<'de>,
727    {
728        use serde::de::{Error, Unexpected, Visitor};
729
730        struct UrlVisitor;
731
732        impl Visitor<'_> for UrlVisitor {
733            type Value = Url;
734
735            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
736                formatter.write_str("a string representing an URL")
737            }
738
739            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
740            where
741                E: Error,
742            {
743                Url::parse(s, None).map_err(|err| {
744                    let err_s = std::format!("{}", err);
745                    Error::invalid_value(Unexpected::Str(s), &err_s.as_str())
746                })
747            }
748        }
749
750        deserializer.deserialize_str(UrlVisitor)
751    }
752}
753
754/// Send is required for sharing Url between threads safely
755unsafe impl Send for Url {}
756
757/// Sync is required for sharing Url between threads safely
758unsafe impl Sync for Url {}
759
760/// URLs compare like their stringification.
761impl PartialEq for Url {
762    fn eq(&self, other: &Self) -> bool {
763        self.href() == other.href()
764    }
765}
766
767impl PartialOrd for Url {
768    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
769        Some(self.cmp(other))
770    }
771}
772
773impl Ord for Url {
774    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
775        self.href().cmp(other.href())
776    }
777}
778
779impl hash::Hash for Url {
780    fn hash<H: hash::Hasher>(&self, state: &mut H) {
781        self.href().hash(state);
782    }
783}
784
785impl borrow::Borrow<str> for Url {
786    fn borrow(&self) -> &str {
787        self.href()
788    }
789}
790
791impl AsRef<[u8]> for Url {
792    fn as_ref(&self) -> &[u8] {
793        self.href().as_bytes()
794    }
795}
796
797#[cfg(feature = "std")]
798impl From<Url> for String {
799    fn from(val: Url) -> Self {
800        val.href().to_owned()
801    }
802}
803
804impl fmt::Debug for Url {
805    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
806        f.debug_struct("Url")
807            .field("href", &self.href())
808            .field("components", &self.components())
809            .finish()
810    }
811}
812
813impl<'input> TryFrom<&'input str> for Url {
814    type Error = ParseUrlError<&'input str>;
815
816    fn try_from(value: &'input str) -> Result<Self, Self::Error> {
817        Self::parse(value, None)
818    }
819}
820
821#[cfg(feature = "std")]
822impl TryFrom<String> for Url {
823    type Error = ParseUrlError<String>;
824
825    fn try_from(value: String) -> Result<Self, Self::Error> {
826        Self::parse(value, None)
827    }
828}
829
830#[cfg(feature = "std")]
831impl<'input> TryFrom<&'input String> for Url {
832    type Error = ParseUrlError<&'input String>;
833
834    fn try_from(value: &'input String) -> Result<Self, Self::Error> {
835        Self::parse(value, None)
836    }
837}
838
839impl ops::Deref for Url {
840    type Target = str;
841    fn deref(&self) -> &Self::Target {
842        self.href()
843    }
844}
845
846impl AsRef<str> for Url {
847    fn as_ref(&self) -> &str {
848        self.href()
849    }
850}
851
852impl fmt::Display for Url {
853    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
854        f.write_str(self.href())
855    }
856}
857
858#[cfg(feature = "std")]
859impl core::str::FromStr for Url {
860    type Err = ParseUrlError<Box<str>>;
861
862    fn from_str(s: &str) -> Result<Self, Self::Err> {
863        Self::parse(s, None).map_err(|ParseUrlError { input }| ParseUrlError {
864            input: input.into(),
865        })
866    }
867}
868
869#[cfg(test)]
870mod test {
871    use super::*;
872
873    #[test]
874    fn should_display_serialization() {
875        let tests = [
876            ("http://example.com/", "http://example.com/"),
877            ("HTTP://EXAMPLE.COM", "http://example.com/"),
878            ("http://user:pwd@domain.com", "http://user:pwd@domain.com/"),
879            (
880                "HTTP://EXAMPLE.COM/FOO/BAR?K1=V1&K2=V2",
881                "http://example.com/FOO/BAR?K1=V1&K2=V2",
882            ),
883            (
884                "http://example.com/🦀/❤️/",
885                "http://example.com/%F0%9F%A6%80/%E2%9D%A4%EF%B8%8F/",
886            ),
887            (
888                "https://example.org/hello world.html",
889                "https://example.org/hello%20world.html",
890            ),
891            (
892                "https://三十六計.org/走為上策/",
893                "https://xn--ehq95fdxbx86i.org/%E8%B5%B0%E7%82%BA%E4%B8%8A%E7%AD%96/",
894            ),
895        ];
896        for (value, expected) in tests {
897            let url = Url::parse(value, None).expect("Should have parsed url");
898            assert_eq!(url.as_str(), expected);
899        }
900    }
901
902    #[test]
903    fn try_from_ok() {
904        let url = Url::try_from("http://example.com/foo/bar?k1=v1&k2=v2");
905        #[cfg(feature = "std")]
906        std::dbg!(&url);
907        let url = url.unwrap();
908        assert_eq!(url.href(), "http://example.com/foo/bar?k1=v1&k2=v2");
909        assert_eq!(
910            url,
911            Url::parse("http://example.com/foo/bar?k1=v1&k2=v2", None).unwrap(),
912        );
913    }
914
915    #[test]
916    fn try_from_err() {
917        let url = Url::try_from("this is not a url");
918        #[cfg(feature = "std")]
919        std::dbg!(&url);
920        let error = url.unwrap_err();
921        #[cfg(feature = "std")]
922        assert_eq!(error.to_string(), r#"Invalid url: "this is not a url""#);
923        assert_eq!(error.input, "this is not a url");
924    }
925
926    #[test]
927    fn should_compare_urls() {
928        let tests = [
929            ("http://example.com/", "http://example.com/", true),
930            ("http://example.com/", "https://example.com/", false),
931            ("http://example.com#", "https://example.com/#", false),
932            ("http://example.com", "https://example.com#", false),
933            (
934                "https://user:pwd@example.com",
935                "https://user:pwd@example.com",
936                true,
937            ),
938        ];
939        for (left, right, expected) in tests {
940            let left_url = Url::parse(left, None).expect("Should have parsed url");
941            let right_url = Url::parse(right, None).expect("Should have parsed url");
942            assert_eq!(
943                left_url == right_url,
944                expected,
945                "left: {left}, right: {right}, expected: {expected}",
946            );
947        }
948    }
949    #[test]
950    fn should_order_alphabetically() {
951        let left = Url::parse("https://example.com/", None).expect("Should have parsed url");
952        let right = Url::parse("https://zoo.tld/", None).expect("Should have parsed url");
953        assert!(left < right);
954        let left = Url::parse("https://c.tld/", None).expect("Should have parsed url");
955        let right = Url::parse("https://a.tld/", None).expect("Should have parsed url");
956        assert!(right < left);
957    }
958
959    #[test]
960    fn should_parse_simple_url() {
961        let mut out = Url::parse(
962            "https://username:password@google.com:9090/search?query#hash",
963            None,
964        )
965        .expect("Should have parsed a simple url");
966
967        #[cfg(feature = "std")]
968        assert_eq!(out.origin(), "https://google.com:9090");
969
970        assert_eq!(
971            out.href(),
972            "https://username:password@google.com:9090/search?query#hash"
973        );
974
975        assert_eq!(out.scheme_type(), SchemeType::Https);
976
977        out.set_username(Some("new-username")).unwrap();
978        assert_eq!(out.username(), "new-username");
979
980        out.set_password(Some("new-password")).unwrap();
981        assert_eq!(out.password(), "new-password");
982
983        out.set_port(Some("4242")).unwrap();
984        assert_eq!(out.port(), "4242");
985        out.set_port(None).unwrap();
986        assert_eq!(out.port(), "");
987
988        out.set_hash(Some("#new-hash"));
989        assert_eq!(out.hash(), "#new-hash");
990
991        out.set_host(Some("yagiz.co:9999")).unwrap();
992        assert_eq!(out.host(), "yagiz.co:9999");
993
994        out.set_hostname(Some("domain.com")).unwrap();
995        assert_eq!(out.hostname(), "domain.com");
996
997        out.set_pathname(Some("/new-search")).unwrap();
998        assert_eq!(out.pathname(), "/new-search");
999        out.set_pathname(None).unwrap();
1000        assert_eq!(out.pathname(), "/");
1001
1002        out.set_search(Some("updated-query"));
1003        assert_eq!(out.search(), "?updated-query");
1004
1005        out.set_protocol("wss").unwrap();
1006        assert_eq!(out.protocol(), "wss:");
1007        assert_eq!(out.scheme_type(), SchemeType::Wss);
1008
1009        assert!(out.has_credentials());
1010        assert!(out.has_non_empty_username());
1011        assert!(out.has_non_empty_password());
1012        assert!(out.has_search());
1013        assert!(out.has_hash());
1014        assert!(out.has_password());
1015
1016        assert_eq!(out.host_type(), HostType::Domain);
1017    }
1018
1019    #[test]
1020    fn scheme_types() {
1021        assert_eq!(
1022            Url::parse("file:///foo/bar", None)
1023                .expect("bad url")
1024                .scheme_type(),
1025            SchemeType::File
1026        );
1027        assert_eq!(
1028            Url::parse("ws://example.com/ws", None)
1029                .expect("bad url")
1030                .scheme_type(),
1031            SchemeType::Ws
1032        );
1033        assert_eq!(
1034            Url::parse("wss://example.com/wss", None)
1035                .expect("bad url")
1036                .scheme_type(),
1037            SchemeType::Wss
1038        );
1039        assert_eq!(
1040            Url::parse("ftp://example.com/file.txt", None)
1041                .expect("bad url")
1042                .scheme_type(),
1043            SchemeType::Ftp
1044        );
1045        assert_eq!(
1046            Url::parse("http://example.com/file.txt", None)
1047                .expect("bad url")
1048                .scheme_type(),
1049            SchemeType::Http
1050        );
1051        assert_eq!(
1052            Url::parse("https://example.com/file.txt", None)
1053                .expect("bad url")
1054                .scheme_type(),
1055            SchemeType::Https
1056        );
1057        assert_eq!(
1058            Url::parse("foo://example.com", None)
1059                .expect("bad url")
1060                .scheme_type(),
1061            SchemeType::NotSpecial
1062        );
1063    }
1064
1065    #[test]
1066    fn can_parse_simple_url() {
1067        assert!(Url::can_parse("https://google.com", None));
1068        assert!(Url::can_parse("/helo", Some("https://www.google.com")));
1069    }
1070
1071    #[cfg(feature = "std")]
1072    #[cfg(feature = "serde")]
1073    #[test]
1074    fn test_serde_serialize_deserialize() {
1075        let input = "https://www.google.com";
1076        let output = "\"https://www.google.com/\"";
1077        let url = Url::parse(&input, None).unwrap();
1078        assert_eq!(serde_json::to_string(&url).unwrap(), output);
1079
1080        let deserialized: Url = serde_json::from_str(&output).unwrap();
1081        assert_eq!(deserialized.href(), "https://www.google.com/");
1082    }
1083
1084    #[test]
1085    fn should_clone() {
1086        let first = Url::parse("https://lemire.me", None).unwrap();
1087        let mut second = first.clone();
1088        second.set_href("https://yagiz.co").unwrap();
1089        assert_ne!(first.href(), second.href());
1090        assert_eq!(first.href(), "https://lemire.me/");
1091        assert_eq!(second.href(), "https://yagiz.co/");
1092    }
1093
1094    #[test]
1095    fn should_handle_empty_host() {
1096        // Ref: https://github.com/ada-url/rust/issues/74
1097        let url = Url::parse("file:///C:/Users/User/Documents/example.pdf", None).unwrap();
1098        assert_eq!(url.host(), "");
1099        assert_eq!(url.hostname(), "");
1100    }
1101}