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
45pub mod 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            Err(ParseUrlError { input })
234        }
235    }
236
237    /// Returns whether or not the URL can be parsed or not.
238    ///
239    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-canparse)
240    ///
241    /// ```
242    /// use ada_url::Url;
243    /// assert!(Url::can_parse("https://ada-url.github.io/ada", None));
244    /// assert!(Url::can_parse("/pathname", Some("https://ada-url.github.io/ada")));
245    /// ```
246    #[must_use]
247    pub fn can_parse(input: &str, base: Option<&str>) -> bool {
248        unsafe {
249            if let Some(base) = base {
250                ffi::ada_can_parse_with_base(
251                    input.as_ptr().cast(),
252                    input.len(),
253                    base.as_ptr().cast(),
254                    base.len(),
255                )
256            } else {
257                ffi::ada_can_parse(input.as_ptr().cast(), input.len())
258            }
259        }
260    }
261
262    /// Returns the type of the host such as default, ipv4 or ipv6.
263    #[must_use]
264    pub fn host_type(&self) -> HostType {
265        HostType::from(unsafe { ffi::ada_get_host_type(self.0) })
266    }
267
268    /// Returns the type of the scheme such as http, https, etc.
269    #[must_use]
270    pub fn scheme_type(&self) -> SchemeType {
271        SchemeType::from(unsafe { ffi::ada_get_scheme_type(self.0) })
272    }
273
274    /// Return the origin of this URL
275    ///
276    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-origin)
277    ///
278    /// ```
279    /// use ada_url::Url;
280    ///
281    /// let url = Url::parse("blob:https://example.com/foo", None).expect("Invalid URL");
282    /// assert_eq!(url.origin(), "https://example.com");
283    /// ```
284    #[must_use]
285    #[cfg(feature = "std")]
286    pub fn origin(&self) -> String {
287        unsafe { ffi::ada_get_origin(self.0) }.to_string()
288    }
289
290    /// Return the parsed version of the URL with all components.
291    ///
292    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-href)
293    #[must_use]
294    pub fn href(&self) -> &str {
295        unsafe { ffi::ada_get_href(self.0) }.as_str()
296    }
297
298    /// Updates the href of the URL, and triggers the URL parser.
299    ///
300    /// ```
301    /// use ada_url::Url;
302    ///
303    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
304    /// url.set_href("https://lemire.me").unwrap();
305    /// assert_eq!(url.href(), "https://lemire.me/");
306    /// ```
307    #[allow(clippy::result_unit_err)]
308    pub fn set_href(&mut self, input: &str) -> SetterResult {
309        setter_result(unsafe { ffi::ada_set_href(self.0, input.as_ptr().cast(), input.len()) })
310    }
311
312    /// Return the username for this URL as a percent-encoded ASCII string.
313    ///
314    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-username)
315    ///
316    /// ```
317    /// use ada_url::Url;
318    ///
319    /// let url = Url::parse("ftp://rms:secret123@example.com", None).expect("Invalid URL");
320    /// assert_eq!(url.username(), "rms");
321    /// ```
322    #[must_use]
323    pub fn username(&self) -> &str {
324        unsafe { ffi::ada_get_username(self.0) }.as_str()
325    }
326
327    /// Updates the `username` of the URL.
328    ///
329    /// ```
330    /// use ada_url::Url;
331    ///
332    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
333    /// url.set_username(Some("username")).unwrap();
334    /// assert_eq!(url.href(), "https://username@yagiz.co/");
335    /// ```
336    #[allow(clippy::result_unit_err)]
337    pub fn set_username(&mut self, input: Option<&str>) -> SetterResult {
338        setter_result(unsafe {
339            ffi::ada_set_username(
340                self.0,
341                input.unwrap_or("").as_ptr().cast(),
342                input.map_or(0, str::len),
343            )
344        })
345    }
346
347    /// Return the password for this URL, if any, as a percent-encoded ASCII string.
348    ///
349    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-password)
350    ///
351    /// ```
352    /// use ada_url::Url;
353    ///
354    /// let url = Url::parse("ftp://rms:secret123@example.com", None).expect("Invalid URL");
355    /// assert_eq!(url.password(), "secret123");
356    /// ```
357    #[must_use]
358    pub fn password(&self) -> &str {
359        unsafe { ffi::ada_get_password(self.0) }.as_str()
360    }
361
362    /// Updates the `password` of the URL.
363    ///
364    /// ```
365    /// use ada_url::Url;
366    ///
367    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
368    /// url.set_password(Some("password")).unwrap();
369    /// assert_eq!(url.href(), "https://:password@yagiz.co/");
370    /// ```
371    #[allow(clippy::result_unit_err)]
372    pub fn set_password(&mut self, input: Option<&str>) -> SetterResult {
373        setter_result(unsafe {
374            ffi::ada_set_password(
375                self.0,
376                input.unwrap_or("").as_ptr().cast(),
377                input.map_or(0, str::len),
378            )
379        })
380    }
381
382    /// Return the port number for this URL, or an empty string.
383    ///
384    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-port)
385    ///
386    /// ```
387    /// use ada_url::Url;
388    ///
389    /// let url = Url::parse("https://example.com", None).expect("Invalid URL");
390    /// assert_eq!(url.port(), "");
391    ///
392    /// let url = Url::parse("https://example.com:8080", None).expect("Invalid URL");
393    /// assert_eq!(url.port(), "8080");
394    /// ```
395    #[must_use]
396    pub fn port(&self) -> &str {
397        unsafe { ffi::ada_get_port(self.0) }.as_str()
398    }
399
400    /// Updates the `port` of the URL.
401    ///
402    /// ```
403    /// use ada_url::Url;
404    ///
405    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
406    /// url.set_port(Some("8080")).unwrap();
407    /// assert_eq!(url.href(), "https://yagiz.co:8080/");
408    /// ```
409    #[allow(clippy::result_unit_err)]
410    pub fn set_port(&mut self, input: Option<&str>) -> SetterResult {
411        if let Some(value) = input {
412            setter_result(unsafe { ffi::ada_set_port(self.0, value.as_ptr().cast(), value.len()) })
413        } else {
414            unsafe { ffi::ada_clear_port(self.0) }
415            Ok(())
416        }
417    }
418
419    /// Return this URL’s fragment identifier, or an empty string.
420    /// A fragment is the part of the URL with the # symbol.
421    /// The fragment is optional and, if present, contains a fragment identifier that identifies
422    /// a secondary resource, such as a section heading of a document.
423    /// In HTML, the fragment identifier is usually the id attribute of a an element that is
424    /// scrolled to on load. Browsers typically will not send the fragment portion of a URL to the
425    /// server.
426    ///
427    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-hash)
428    ///
429    /// ```
430    /// use ada_url::Url;
431    ///
432    /// let url = Url::parse("https://example.com/data.csv#row=4", None).expect("Invalid URL");
433    /// assert_eq!(url.hash(), "#row=4");
434    /// assert!(url.has_hash());
435    /// ```
436    #[must_use]
437    pub fn hash(&self) -> &str {
438        unsafe { ffi::ada_get_hash(self.0) }.as_str()
439    }
440
441    /// Updates the `hash` of the URL.
442    ///
443    /// ```
444    /// use ada_url::Url;
445    ///
446    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
447    /// url.set_hash(Some("this-is-my-hash"));
448    /// assert_eq!(url.href(), "https://yagiz.co/#this-is-my-hash");
449    /// ```
450    pub fn set_hash(&mut self, input: Option<&str>) {
451        match input {
452            Some(value) => unsafe { ffi::ada_set_hash(self.0, value.as_ptr().cast(), value.len()) },
453            None => unsafe { ffi::ada_clear_hash(self.0) },
454        }
455    }
456
457    /// Return the parsed representation of the host for this URL with an optional port number.
458    ///
459    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-host)
460    ///
461    /// ```
462    /// use ada_url::Url;
463    ///
464    /// let url = Url::parse("https://127.0.0.1:8080/index.html", None).expect("Invalid URL");
465    /// assert_eq!(url.host(), "127.0.0.1:8080");
466    /// ```
467    #[must_use]
468    pub fn host(&self) -> &str {
469        unsafe { ffi::ada_get_host(self.0) }.as_str()
470    }
471
472    /// Updates the `host` of the URL.
473    ///
474    /// ```
475    /// use ada_url::Url;
476    ///
477    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
478    /// url.set_host(Some("localhost:3000")).unwrap();
479    /// assert_eq!(url.href(), "https://localhost:3000/");
480    /// ```
481    #[allow(clippy::result_unit_err)]
482    pub fn set_host(&mut self, input: Option<&str>) -> SetterResult {
483        setter_result(unsafe {
484            ffi::ada_set_host(
485                self.0,
486                input.unwrap_or("").as_ptr().cast(),
487                input.map_or(0, str::len),
488            )
489        })
490    }
491
492    /// Return the parsed representation of the host for this URL. Non-ASCII domain labels are
493    /// punycode-encoded per IDNA if this is the host of a special URL, or percent encoded for
494    /// non-special URLs.
495    ///
496    /// Hostname does not contain port number.
497    ///
498    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-hostname)
499    ///
500    /// ```
501    /// use ada_url::Url;
502    ///
503    /// let url = Url::parse("https://127.0.0.1:8080/index.html", None).expect("Invalid URL");
504    /// assert_eq!(url.hostname(), "127.0.0.1");
505    /// ```
506    #[must_use]
507    pub fn hostname(&self) -> &str {
508        unsafe { ffi::ada_get_hostname(self.0) }.as_str()
509    }
510
511    /// Updates the `hostname` of the URL.
512    ///
513    /// ```
514    /// use ada_url::Url;
515    ///
516    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
517    /// url.set_hostname(Some("localhost")).unwrap();
518    /// assert_eq!(url.href(), "https://localhost/");
519    /// ```
520    #[allow(clippy::result_unit_err)]
521    pub fn set_hostname(&mut self, input: Option<&str>) -> SetterResult {
522        setter_result(unsafe {
523            ffi::ada_set_hostname(
524                self.0,
525                input.unwrap_or("").as_ptr().cast(),
526                input.map_or(0, str::len),
527            )
528        })
529    }
530
531    /// Return the path for this URL, as a percent-encoded ASCII string.
532    ///
533    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-pathname)
534    ///
535    /// ```
536    /// use ada_url::Url;
537    ///
538    /// let url = Url::parse("https://example.com/api/versions?page=2", None).expect("Invalid URL");
539    /// assert_eq!(url.pathname(), "/api/versions");
540    /// ```
541    #[must_use]
542    pub fn pathname(&self) -> &str {
543        unsafe { ffi::ada_get_pathname(self.0) }.as_str()
544    }
545
546    /// Updates the `pathname` of the URL.
547    ///
548    /// ```
549    /// use ada_url::Url;
550    ///
551    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
552    /// url.set_pathname(Some("/contact")).unwrap();
553    /// assert_eq!(url.href(), "https://yagiz.co/contact");
554    /// ```
555    #[allow(clippy::result_unit_err)]
556    pub fn set_pathname(&mut self, input: Option<&str>) -> SetterResult {
557        setter_result(unsafe {
558            ffi::ada_set_pathname(
559                self.0,
560                input.unwrap_or("").as_ptr().cast(),
561                input.map_or(0, str::len),
562            )
563        })
564    }
565
566    /// Return this URL’s query string, if any, as a percent-encoded ASCII string.
567    ///
568    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-search)
569    ///
570    /// ```
571    /// use ada_url::Url;
572    ///
573    /// let url = Url::parse("https://example.com/products?page=2", None).expect("Invalid URL");
574    /// assert_eq!(url.search(), "?page=2");
575    ///
576    /// let url = Url::parse("https://example.com/products", None).expect("Invalid URL");
577    /// assert_eq!(url.search(), "");
578    /// ```
579    #[must_use]
580    pub fn search(&self) -> &str {
581        unsafe { ffi::ada_get_search(self.0) }.as_str()
582    }
583
584    /// Updates the `search` of the URL.
585    ///
586    /// ```
587    /// use ada_url::Url;
588    ///
589    /// let mut url = Url::parse("https://yagiz.co", None).expect("Invalid URL");
590    /// url.set_search(Some("?page=1"));
591    /// assert_eq!(url.href(), "https://yagiz.co/?page=1");
592    /// ```
593    pub fn set_search(&mut self, input: Option<&str>) {
594        match input {
595            Some(value) => unsafe {
596                ffi::ada_set_search(self.0, value.as_ptr().cast(), value.len());
597            },
598            None => unsafe { ffi::ada_clear_search(self.0) },
599        }
600    }
601
602    /// Return the scheme of this URL, lower-cased, as an ASCII string with the ‘:’ delimiter.
603    ///
604    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-protocol)
605    ///
606    /// ```
607    /// use ada_url::Url;
608    ///
609    /// let url = Url::parse("file:///tmp/foo", None).expect("Invalid URL");
610    /// assert_eq!(url.protocol(), "file:");
611    /// ```
612    #[must_use]
613    pub fn protocol(&self) -> &str {
614        unsafe { ffi::ada_get_protocol(self.0) }.as_str()
615    }
616
617    /// Updates the `protocol` of the URL.
618    ///
619    /// ```
620    /// use ada_url::Url;
621    ///
622    /// let mut url = Url::parse("http://yagiz.co", None).expect("Invalid URL");
623    /// url.set_protocol("http").unwrap();
624    /// assert_eq!(url.href(), "http://yagiz.co/");
625    /// ```
626    #[allow(clippy::result_unit_err)]
627    pub fn set_protocol(&mut self, input: &str) -> SetterResult {
628        setter_result(unsafe { ffi::ada_set_protocol(self.0, input.as_ptr().cast(), input.len()) })
629    }
630
631    /// A URL includes credentials if its username or password is not the empty string.
632    #[must_use]
633    pub fn has_credentials(&self) -> bool {
634        unsafe { ffi::ada_has_credentials(self.0) }
635    }
636
637    /// Returns true if it has an host but it is the empty string.
638    #[must_use]
639    pub fn has_empty_hostname(&self) -> bool {
640        unsafe { ffi::ada_has_empty_hostname(self.0) }
641    }
642
643    /// Returns true if it has a host (included an empty host)
644    #[must_use]
645    pub fn has_hostname(&self) -> bool {
646        unsafe { ffi::ada_has_hostname(self.0) }
647    }
648
649    /// Returns true if URL has a non-empty username.
650    #[must_use]
651    pub fn has_non_empty_username(&self) -> bool {
652        unsafe { ffi::ada_has_non_empty_username(self.0) }
653    }
654
655    /// Returns true if URL has a non-empty password.
656    #[must_use]
657    pub fn has_non_empty_password(&self) -> bool {
658        unsafe { ffi::ada_has_non_empty_password(self.0) }
659    }
660
661    /// Returns true if URL has a port.
662    #[must_use]
663    pub fn has_port(&self) -> bool {
664        unsafe { ffi::ada_has_port(self.0) }
665    }
666
667    /// Returns true if URL has password.
668    #[must_use]
669    pub fn has_password(&self) -> bool {
670        unsafe { ffi::ada_has_password(self.0) }
671    }
672
673    /// Returns true if URL has a hash/fragment.
674    #[must_use]
675    pub fn has_hash(&self) -> bool {
676        unsafe { ffi::ada_has_hash(self.0) }
677    }
678
679    /// Returns true if URL has search/query.
680    #[must_use]
681    pub fn has_search(&self) -> bool {
682        unsafe { ffi::ada_has_search(self.0) }
683    }
684
685    /// Returns the parsed version of the URL with all components.
686    ///
687    /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-href)
688    #[must_use]
689    pub fn as_str(&self) -> &str {
690        self.href()
691    }
692
693    /// Returns the URL components of the instance.
694    #[must_use]
695    pub fn components(&self) -> UrlComponents {
696        unsafe { ffi::ada_get_components(self.0).as_ref().unwrap() }.into()
697    }
698}
699
700/// Serializes this URL into a `serde` stream.
701///
702/// This implementation is only available if the `serde` Cargo feature is enabled.
703#[cfg(feature = "serde")]
704impl serde::Serialize for Url {
705    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
706    where
707        S: serde::Serializer,
708    {
709        serializer.serialize_str(self.as_str())
710    }
711}
712
713/// Deserializes this URL from a `serde` stream.
714///
715/// This implementation is only available if the `serde` Cargo feature is enabled.
716#[cfg(feature = "serde")]
717#[cfg(feature = "std")]
718impl<'de> serde::Deserialize<'de> for Url {
719    fn deserialize<D>(deserializer: D) -> Result<Url, D::Error>
720    where
721        D: serde::Deserializer<'de>,
722    {
723        use serde::de::{Error, Unexpected, Visitor};
724
725        struct UrlVisitor;
726
727        impl Visitor<'_> for UrlVisitor {
728            type Value = Url;
729
730            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
731                formatter.write_str("a string representing an URL")
732            }
733
734            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
735            where
736                E: Error,
737            {
738                Url::parse(s, None).map_err(|err| {
739                    let err_s = std::format!("{}", err);
740                    Error::invalid_value(Unexpected::Str(s), &err_s.as_str())
741                })
742            }
743        }
744
745        deserializer.deserialize_str(UrlVisitor)
746    }
747}
748
749/// Send is required for sharing Url between threads safely
750unsafe impl Send for Url {}
751
752/// Sync is required for sharing Url between threads safely
753unsafe impl Sync for Url {}
754
755/// URLs compare like their stringification.
756impl PartialEq for Url {
757    fn eq(&self, other: &Self) -> bool {
758        self.href() == other.href()
759    }
760}
761
762impl PartialOrd for Url {
763    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
764        Some(self.cmp(other))
765    }
766}
767
768impl Ord for Url {
769    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
770        self.href().cmp(other.href())
771    }
772}
773
774impl hash::Hash for Url {
775    fn hash<H: hash::Hasher>(&self, state: &mut H) {
776        self.href().hash(state);
777    }
778}
779
780impl borrow::Borrow<str> for Url {
781    fn borrow(&self) -> &str {
782        self.href()
783    }
784}
785
786impl AsRef<[u8]> for Url {
787    fn as_ref(&self) -> &[u8] {
788        self.href().as_bytes()
789    }
790}
791
792#[cfg(feature = "std")]
793impl From<Url> for String {
794    fn from(val: Url) -> Self {
795        val.href().to_owned()
796    }
797}
798
799impl fmt::Debug for Url {
800    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
801        f.debug_struct("Url")
802            .field("href", &self.href())
803            .field("components", &self.components())
804            .finish()
805    }
806}
807
808impl<'input> TryFrom<&'input str> for Url {
809    type Error = ParseUrlError<&'input str>;
810
811    fn try_from(value: &'input str) -> Result<Self, Self::Error> {
812        Self::parse(value, None)
813    }
814}
815
816#[cfg(feature = "std")]
817impl TryFrom<String> for Url {
818    type Error = ParseUrlError<String>;
819
820    fn try_from(value: String) -> Result<Self, Self::Error> {
821        Self::parse(value, None)
822    }
823}
824
825#[cfg(feature = "std")]
826impl<'input> TryFrom<&'input String> for Url {
827    type Error = ParseUrlError<&'input String>;
828
829    fn try_from(value: &'input String) -> Result<Self, Self::Error> {
830        Self::parse(value, None)
831    }
832}
833
834impl ops::Deref for Url {
835    type Target = str;
836    fn deref(&self) -> &Self::Target {
837        self.href()
838    }
839}
840
841impl AsRef<str> for Url {
842    fn as_ref(&self) -> &str {
843        self.href()
844    }
845}
846
847impl fmt::Display for Url {
848    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
849        f.write_str(self.href())
850    }
851}
852
853#[cfg(feature = "std")]
854impl core::str::FromStr for Url {
855    type Err = ParseUrlError<Box<str>>;
856
857    fn from_str(s: &str) -> Result<Self, Self::Err> {
858        Self::parse(s, None).map_err(|ParseUrlError { input }| ParseUrlError {
859            input: input.into(),
860        })
861    }
862}
863
864#[cfg(test)]
865mod test {
866    use super::*;
867
868    #[test]
869    fn should_display_serialization() {
870        let tests = [
871            ("http://example.com/", "http://example.com/"),
872            ("HTTP://EXAMPLE.COM", "http://example.com/"),
873            ("http://user:pwd@domain.com", "http://user:pwd@domain.com/"),
874            (
875                "HTTP://EXAMPLE.COM/FOO/BAR?K1=V1&K2=V2",
876                "http://example.com/FOO/BAR?K1=V1&K2=V2",
877            ),
878            (
879                "http://example.com/🦀/❤️/",
880                "http://example.com/%F0%9F%A6%80/%E2%9D%A4%EF%B8%8F/",
881            ),
882            (
883                "https://example.org/hello world.html",
884                "https://example.org/hello%20world.html",
885            ),
886            (
887                "https://三十六計.org/走為上策/",
888                "https://xn--ehq95fdxbx86i.org/%E8%B5%B0%E7%82%BA%E4%B8%8A%E7%AD%96/",
889            ),
890        ];
891        for (value, expected) in tests {
892            let url = Url::parse(value, None).expect("Should have parsed url");
893            assert_eq!(url.as_str(), expected);
894        }
895    }
896
897    #[test]
898    fn try_from_ok() {
899        let url = Url::try_from("http://example.com/foo/bar?k1=v1&k2=v2");
900        #[cfg(feature = "std")]
901        std::dbg!(&url);
902        let url = url.unwrap();
903        assert_eq!(url.href(), "http://example.com/foo/bar?k1=v1&k2=v2");
904        assert_eq!(
905            url,
906            Url::parse("http://example.com/foo/bar?k1=v1&k2=v2", None).unwrap(),
907        );
908    }
909
910    #[test]
911    fn try_from_err() {
912        let url = Url::try_from("this is not a url");
913        #[cfg(feature = "std")]
914        std::dbg!(&url);
915        let error = url.unwrap_err();
916        #[cfg(feature = "std")]
917        assert_eq!(error.to_string(), r#"Invalid url: "this is not a url""#);
918        assert_eq!(error.input, "this is not a url");
919    }
920
921    #[test]
922    fn should_compare_urls() {
923        let tests = [
924            ("http://example.com/", "http://example.com/", true),
925            ("http://example.com/", "https://example.com/", false),
926            ("http://example.com#", "https://example.com/#", false),
927            ("http://example.com", "https://example.com#", false),
928            (
929                "https://user:pwd@example.com",
930                "https://user:pwd@example.com",
931                true,
932            ),
933        ];
934        for (left, right, expected) in tests {
935            let left_url = Url::parse(left, None).expect("Should have parsed url");
936            let right_url = Url::parse(right, None).expect("Should have parsed url");
937            assert_eq!(
938                left_url == right_url,
939                expected,
940                "left: {left}, right: {right}, expected: {expected}",
941            );
942        }
943    }
944    #[test]
945    fn should_order_alphabetically() {
946        let left = Url::parse("https://example.com/", None).expect("Should have parsed url");
947        let right = Url::parse("https://zoo.tld/", None).expect("Should have parsed url");
948        assert!(left < right);
949        let left = Url::parse("https://c.tld/", None).expect("Should have parsed url");
950        let right = Url::parse("https://a.tld/", None).expect("Should have parsed url");
951        assert!(right < left);
952    }
953
954    #[test]
955    fn should_parse_simple_url() {
956        let mut out = Url::parse(
957            "https://username:password@google.com:9090/search?query#hash",
958            None,
959        )
960        .expect("Should have parsed a simple url");
961
962        #[cfg(feature = "std")]
963        assert_eq!(out.origin(), "https://google.com:9090");
964
965        assert_eq!(
966            out.href(),
967            "https://username:password@google.com:9090/search?query#hash"
968        );
969
970        assert_eq!(out.scheme_type(), SchemeType::Https);
971
972        out.set_username(Some("new-username")).unwrap();
973        assert_eq!(out.username(), "new-username");
974
975        out.set_password(Some("new-password")).unwrap();
976        assert_eq!(out.password(), "new-password");
977
978        out.set_port(Some("4242")).unwrap();
979        assert_eq!(out.port(), "4242");
980        out.set_port(None).unwrap();
981        assert_eq!(out.port(), "");
982
983        out.set_hash(Some("#new-hash"));
984        assert_eq!(out.hash(), "#new-hash");
985
986        out.set_host(Some("yagiz.co:9999")).unwrap();
987        assert_eq!(out.host(), "yagiz.co:9999");
988
989        out.set_hostname(Some("domain.com")).unwrap();
990        assert_eq!(out.hostname(), "domain.com");
991
992        out.set_pathname(Some("/new-search")).unwrap();
993        assert_eq!(out.pathname(), "/new-search");
994        out.set_pathname(None).unwrap();
995        assert_eq!(out.pathname(), "/");
996
997        out.set_search(Some("updated-query"));
998        assert_eq!(out.search(), "?updated-query");
999
1000        out.set_protocol("wss").unwrap();
1001        assert_eq!(out.protocol(), "wss:");
1002        assert_eq!(out.scheme_type(), SchemeType::Wss);
1003
1004        assert!(out.has_credentials());
1005        assert!(out.has_non_empty_username());
1006        assert!(out.has_non_empty_password());
1007        assert!(out.has_search());
1008        assert!(out.has_hash());
1009        assert!(out.has_password());
1010
1011        assert_eq!(out.host_type(), HostType::Domain);
1012    }
1013
1014    #[test]
1015    fn scheme_types() {
1016        assert_eq!(
1017            Url::parse("file:///foo/bar", None)
1018                .expect("bad url")
1019                .scheme_type(),
1020            SchemeType::File
1021        );
1022        assert_eq!(
1023            Url::parse("ws://example.com/ws", None)
1024                .expect("bad url")
1025                .scheme_type(),
1026            SchemeType::Ws
1027        );
1028        assert_eq!(
1029            Url::parse("wss://example.com/wss", None)
1030                .expect("bad url")
1031                .scheme_type(),
1032            SchemeType::Wss
1033        );
1034        assert_eq!(
1035            Url::parse("ftp://example.com/file.txt", None)
1036                .expect("bad url")
1037                .scheme_type(),
1038            SchemeType::Ftp
1039        );
1040        assert_eq!(
1041            Url::parse("http://example.com/file.txt", None)
1042                .expect("bad url")
1043                .scheme_type(),
1044            SchemeType::Http
1045        );
1046        assert_eq!(
1047            Url::parse("https://example.com/file.txt", None)
1048                .expect("bad url")
1049                .scheme_type(),
1050            SchemeType::Https
1051        );
1052        assert_eq!(
1053            Url::parse("foo://example.com", None)
1054                .expect("bad url")
1055                .scheme_type(),
1056            SchemeType::NotSpecial
1057        );
1058    }
1059
1060    #[test]
1061    fn can_parse_simple_url() {
1062        assert!(Url::can_parse("https://google.com", None));
1063        assert!(Url::can_parse("/helo", Some("https://www.google.com")));
1064    }
1065
1066    #[cfg(feature = "std")]
1067    #[cfg(feature = "serde")]
1068    #[test]
1069    fn test_serde_serialize_deserialize() {
1070        let input = "https://www.google.com";
1071        let output = "\"https://www.google.com/\"";
1072        let url = Url::parse(&input, None).unwrap();
1073        assert_eq!(serde_json::to_string(&url).unwrap(), output);
1074
1075        let deserialized: Url = serde_json::from_str(&output).unwrap();
1076        assert_eq!(deserialized.href(), "https://www.google.com/");
1077    }
1078
1079    #[test]
1080    fn should_clone() {
1081        let first = Url::parse("https://lemire.me", None).unwrap();
1082        let mut second = first.clone();
1083        second.set_href("https://yagiz.co").unwrap();
1084        assert_ne!(first.href(), second.href());
1085        assert_eq!(first.href(), "https://lemire.me/");
1086        assert_eq!(second.href(), "https://yagiz.co/");
1087    }
1088
1089    #[test]
1090    fn should_handle_empty_host() {
1091        // Ref: https://github.com/ada-url/rust/issues/74
1092        let url = Url::parse("file:///C:/Users/User/Documents/example.pdf", None).unwrap();
1093        assert_eq!(url.host(), "");
1094        assert_eq!(url.hostname(), "");
1095    }
1096}