diff --git a/src/ffi.rs b/src/ffi.rs index 341a6d4..9933810 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -14,7 +14,7 @@ pub struct ada_string { } impl ada_string { - pub fn as_str(self) -> &'static str { + pub fn as_str(&self) -> &'static str { unsafe { let slice = std::slice::from_raw_parts(self.data.cast(), self.length); std::str::from_utf8_unchecked(slice) diff --git a/src/lib.rs b/src/lib.rs index 61490d9..0641227 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,8 +35,7 @@ pub mod ffi; mod idna; pub use idna::Idna; -use std::os::raw::c_uint; -use std::{borrow, fmt, hash, ops}; +use std::{borrow, fmt, hash, ops, os::raw::c_uint}; use thiserror::Error; extern crate alloc; @@ -68,25 +67,87 @@ impl From for HostType { } } +/// By using 32-bit integers, we implicitly assume that the URL string +/// cannot exceed 4 GB. +/// +/// https://user:pass@example.com:1234/foo/bar?baz#quux +/// | | | | ^^^^| | | +/// | | | | | | | `----- hash_start +/// | | | | | | `--------- search_start +/// | | | | | `----------------- pathname_start +/// | | | | `--------------------- port +/// | | | `----------------------- host_end +/// | | `---------------------------------- host_start +/// | `--------------------------------------- username_end +/// `--------------------------------------------- protocol_end +#[derive(Debug)] +pub struct UrlComponents { + pub protocol_end: u32, + pub username_end: u32, + pub host_start: u32, + pub host_end: u32, + pub port: Option, + pub pathname_start: Option, + pub search_start: Option, + pub hash_start: Option, +} + +impl From<&ffi::ada_url_components> for UrlComponents { + fn from(value: &ffi::ada_url_components) -> Self { + let port = if value.port == u32::MAX { + None + } else { + Some(value.port) + }; + let pathname_start = if value.pathname_start == u32::MAX { + None + } else { + Some(value.pathname_start) + }; + let search_start = if value.search_start == u32::MAX { + None + } else { + Some(value.search_start) + }; + let hash_start = if value.hash_start == u32::MAX { + None + } else { + Some(value.hash_start) + }; + Self { + protocol_end: value.protocol_end, + username_end: value.username_end, + host_start: value.host_start, + host_end: value.host_end, + port, + pathname_start, + search_start, + hash_start, + } + } +} + /// A parsed URL struct according to WHATWG URL specification. #[derive(Eq)] -pub struct Url { - url: *mut ffi::ada_url, -} +pub struct Url(*mut ffi::ada_url); /// Clone trait by default uses bit-wise copy. /// In Rust, FFI requires deep copy, which requires an additional/inexpensive FFI call. impl Clone for Url { fn clone(&self) -> Self { - Url { - url: unsafe { ffi::ada_copy(self.url) }, - } + unsafe { ffi::ada_copy(self.0).into() } } } impl Drop for Url { fn drop(&mut self) { - unsafe { ffi::ada_free(self.url) } + unsafe { ffi::ada_free(self.0) } + } +} + +impl From<*mut ffi::ada_url> for Url { + fn from(value: *mut ffi::ada_url) -> Self { + Self(value) } } @@ -113,9 +174,7 @@ impl Url { }; if unsafe { ffi::ada_is_valid(url_aggregator) } { - Ok(Url { - url: url_aggregator, - }) + Ok(url_aggregator.into()) } else { Err(Error::ParseUrl(input.to_owned())) } @@ -147,7 +206,7 @@ impl Url { /// Returns the type of the host such as default, ipv4 or ipv6. pub fn host_type(&self) -> HostType { - HostType::from(unsafe { ffi::ada_get_url_host_type(self.url) }) + HostType::from(unsafe { ffi::ada_get_url_host_type(self.0) }) } /// Return the origin of this URL @@ -162,7 +221,7 @@ impl Url { /// ``` pub fn origin(&self) -> &str { unsafe { - let out = ffi::ada_get_origin(self.url); + let out = ffi::ada_get_origin(self.0); let slice = std::slice::from_raw_parts(out.data.cast(), out.length); std::str::from_utf8_unchecked(slice) } @@ -172,11 +231,11 @@ impl Url { /// /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-href) pub fn href(&self) -> &str { - unsafe { ffi::ada_get_href(self.url) }.as_str() + unsafe { ffi::ada_get_href(self.0) }.as_str() } pub fn set_href(&mut self, input: &str) -> bool { - unsafe { ffi::ada_set_href(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_href(self.0, input.as_ptr().cast(), input.len()) } } /// Return the username for this URL as a percent-encoded ASCII string. @@ -190,11 +249,11 @@ impl Url { /// assert_eq!(url.username(), "rms"); /// ``` pub fn username(&self) -> &str { - unsafe { ffi::ada_get_username(self.url) }.as_str() + unsafe { ffi::ada_get_username(self.0) }.as_str() } pub fn set_username(&mut self, input: &str) -> bool { - unsafe { ffi::ada_set_username(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_username(self.0, input.as_ptr().cast(), input.len()) } } /// Return the password for this URL, if any, as a percent-encoded ASCII string. @@ -208,11 +267,11 @@ impl Url { /// assert_eq!(url.password(), "secret123"); /// ``` pub fn password(&self) -> &str { - unsafe { ffi::ada_get_password(self.url) }.as_str() + unsafe { ffi::ada_get_password(self.0) }.as_str() } pub fn set_password(&mut self, input: &str) -> bool { - unsafe { ffi::ada_set_password(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_password(self.0, input.as_ptr().cast(), input.len()) } } /// Return the port number for this URL, or an empty string. @@ -229,11 +288,11 @@ impl Url { /// assert_eq!(url.port(), "8080"); /// ``` pub fn port(&self) -> &str { - unsafe { ffi::ada_get_port(self.url) }.as_str() + unsafe { ffi::ada_get_port(self.0) }.as_str() } pub fn set_port(&mut self, input: &str) -> bool { - unsafe { ffi::ada_set_port(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_port(self.0, input.as_ptr().cast(), input.len()) } } /// Return this URL’s fragment identifier, or an empty string. @@ -254,11 +313,11 @@ impl Url { /// assert!(url.has_hash()); /// ``` pub fn hash(&self) -> &str { - unsafe { ffi::ada_get_hash(self.url) }.as_str() + unsafe { ffi::ada_get_hash(self.0) }.as_str() } pub fn set_hash(&mut self, input: &str) { - unsafe { ffi::ada_set_hash(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_hash(self.0, input.as_ptr().cast(), input.len()) } } /// Return the parsed representation of the host for this URL with an optional port number. @@ -272,11 +331,11 @@ impl Url { /// assert_eq!(url.host(), "127.0.0.1:8080"); /// ``` pub fn host(&self) -> &str { - unsafe { ffi::ada_get_host(self.url) }.as_str() + unsafe { ffi::ada_get_host(self.0) }.as_str() } pub fn set_host(&mut self, input: &str) -> bool { - unsafe { ffi::ada_set_host(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_host(self.0, input.as_ptr().cast(), input.len()) } } /// Return the parsed representation of the host for this URL. Non-ASCII domain labels are @@ -294,11 +353,11 @@ impl Url { /// assert_eq!(url.hostname(), "127.0.0.1"); /// ``` pub fn hostname(&self) -> &str { - unsafe { ffi::ada_get_hostname(self.url) }.as_str() + unsafe { ffi::ada_get_hostname(self.0) }.as_str() } pub fn set_hostname(&mut self, input: &str) -> bool { - unsafe { ffi::ada_set_hostname(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_hostname(self.0, input.as_ptr().cast(), input.len()) } } /// Return the path for this URL, as a percent-encoded ASCII string. @@ -312,11 +371,11 @@ impl Url { /// assert_eq!(url.pathname(), "/api/versions"); /// ``` pub fn pathname(&self) -> &str { - unsafe { ffi::ada_get_pathname(self.url) }.as_str() + unsafe { ffi::ada_get_pathname(self.0) }.as_str() } pub fn set_pathname(&mut self, input: &str) -> bool { - unsafe { ffi::ada_set_pathname(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_pathname(self.0, input.as_ptr().cast(), input.len()) } } /// Return this URL’s query string, if any, as a percent-encoded ASCII string. @@ -333,11 +392,11 @@ impl Url { /// assert_eq!(url.search(), ""); /// ``` pub fn search(&self) -> &str { - unsafe { ffi::ada_get_search(self.url) }.as_str() + unsafe { ffi::ada_get_search(self.0) }.as_str() } pub fn set_search(&mut self, input: &str) { - unsafe { ffi::ada_set_search(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_search(self.0, input.as_ptr().cast(), input.len()) } } /// Return the scheme of this URL, lower-cased, as an ASCII string with the ‘:’ delimiter. @@ -351,57 +410,63 @@ impl Url { /// assert_eq!(url.protocol(), "file:"); /// ``` pub fn protocol(&self) -> &str { - unsafe { ffi::ada_get_protocol(self.url) }.as_str() + unsafe { ffi::ada_get_protocol(self.0) }.as_str() } pub fn set_protocol(&mut self, input: &str) -> bool { - unsafe { ffi::ada_set_protocol(self.url, input.as_ptr().cast(), input.len()) } + unsafe { ffi::ada_set_protocol(self.0, input.as_ptr().cast(), input.len()) } } /// A URL includes credentials if its username or password is not the empty string. pub fn has_credentials(&self) -> bool { - unsafe { ffi::ada_has_credentials(self.url) } + unsafe { ffi::ada_has_credentials(self.0) } } /// Returns true if it has an host but it is the empty string. pub fn has_empty_hostname(&self) -> bool { - unsafe { ffi::ada_has_empty_hostname(self.url) } + unsafe { ffi::ada_has_empty_hostname(self.0) } } /// Returns true if it has a host (included an empty host) pub fn has_hostname(&self) -> bool { - unsafe { ffi::ada_has_hostname(self.url) } + unsafe { ffi::ada_has_hostname(self.0) } } pub fn has_non_empty_username(&self) -> bool { - unsafe { ffi::ada_has_non_empty_username(self.url) } + unsafe { ffi::ada_has_non_empty_username(self.0) } } pub fn has_non_empty_password(&self) -> bool { - unsafe { ffi::ada_has_non_empty_password(self.url) } + unsafe { ffi::ada_has_non_empty_password(self.0) } } pub fn has_port(&self) -> bool { - unsafe { ffi::ada_has_port(self.url) } + unsafe { ffi::ada_has_port(self.0) } } pub fn has_password(&self) -> bool { - unsafe { ffi::ada_has_password(self.url) } + unsafe { ffi::ada_has_password(self.0) } } pub fn has_hash(&self) -> bool { - unsafe { ffi::ada_has_hash(self.url) } + unsafe { ffi::ada_has_hash(self.0) } } pub fn has_search(&self) -> bool { - unsafe { ffi::ada_has_search(self.url) } + unsafe { ffi::ada_has_search(self.0) } } + /// Returns the parsed version of the URL with all components. /// /// For more information, read [WHATWG URL spec](https://url.spec.whatwg.org/#dom-url-href) pub fn as_str(&self) -> &str { self.href() } + + /// Returns the URL components of the instance. + pub fn components(&self) -> UrlComponents { + unsafe { ffi::ada_get_components(self.0).as_ref().unwrap() }.into() + } } /// Serializes this URL into a `serde` stream. @@ -509,48 +574,10 @@ impl From for String { impl fmt::Debug for Url { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - unsafe { - let components = ffi::ada_get_components(self.url).as_ref().unwrap(); - let mut debug = f.debug_struct("Url"); - debug - .field("href", &self.href()) - .field("protocol_end", &components.protocol_end) - .field("host_start", &components.host_start) - .field("host_end", &components.host_end); - let port = if components.port == u32::MAX { - None - } else { - Some(components.port) - }; - let username_end = if components.username_end == u32::MAX { - None - } else { - Some(components.username_end) - }; - let search_start = if components.search_start == u32::MAX { - None - } else { - Some(components.search_start) - }; - let hash_start = if components.hash_start == u32::MAX { - None - } else { - Some(components.hash_start) - }; - let pathname_start = if components.pathname_start == u32::MAX { - None - } else { - Some(components.pathname_start) - }; - - debug - .field("port", &port) - .field("username_end", &username_end) - .field("search_start", &search_start) - .field("hash_start", &hash_start) - .field("pathname_start", &pathname_start) - .finish() - } + f.debug_struct("Url") + .field("href", &self.href()) + .field("components", &self.components()) + .finish() } }