#![cfg_attr(not(feature = "std"), no_std)]
mod ffi;
mod idna;
mod url_search_params;
pub use idna::Idna;
pub use url_search_params::{
UrlSearchParams, UrlSearchParamsEntry, UrlSearchParamsEntryIterator,
UrlSearchParamsKeyIterator, UrlSearchParamsValueIterator,
};
#[cfg(feature = "std")]
extern crate std;
#[cfg(feature = "std")]
use std::string::String;
use core::{borrow, ffi::c_uint, fmt, hash, ops};
#[derive(Debug, PartialEq, Eq)]
pub struct ParseUrlError<Input> {
pub input: Input,
}
impl<Input: core::fmt::Debug> fmt::Display for ParseUrlError<Input> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Invalid url: {:?}", self.input)
}
}
#[cfg(feature = "std")] impl<Input: core::fmt::Debug> std::error::Error for ParseUrlError<Input> {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HostType {
Domain = 0,
IPV4 = 1,
IPV6 = 2,
}
impl From<c_uint> for HostType {
fn from(value: c_uint) -> Self {
match value {
0 => Self::Domain,
1 => Self::IPV4,
2 => Self::IPV6,
_ => Self::Domain,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SchemeType {
Http = 0,
NotSpecial = 1,
Https = 2,
Ws = 3,
Ftp = 4,
Wss = 5,
File = 6,
}
impl From<c_uint> for SchemeType {
fn from(value: c_uint) -> Self {
match value {
0 => Self::Http,
1 => Self::NotSpecial,
2 => Self::Https,
3 => Self::Ws,
4 => Self::Ftp,
5 => Self::Wss,
6 => Self::File,
_ => Self::NotSpecial,
}
}
}
#[derive(Debug)]
pub struct UrlComponents {
pub protocol_end: u32,
pub username_end: u32,
pub host_start: u32,
pub host_end: u32,
pub port: Option<u32>,
pub pathname_start: Option<u32>,
pub search_start: Option<u32>,
pub hash_start: Option<u32>,
}
impl From<&ffi::ada_url_components> for UrlComponents {
fn from(value: &ffi::ada_url_components) -> Self {
let port = (value.port != u32::MAX).then_some(value.port);
let pathname_start = (value.pathname_start != u32::MAX).then_some(value.pathname_start);
let search_start = (value.search_start != u32::MAX).then_some(value.search_start);
let hash_start = (value.hash_start != u32::MAX).then_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,
}
}
}
#[derive(Eq)]
pub struct Url(*mut ffi::ada_url);
impl Clone for Url {
fn clone(&self) -> Self {
unsafe { ffi::ada_copy(self.0).into() }
}
}
impl Drop for Url {
fn drop(&mut self) {
unsafe { ffi::ada_free(self.0) }
}
}
impl From<*mut ffi::ada_url> for Url {
fn from(value: *mut ffi::ada_url) -> Self {
Self(value)
}
}
type SetterResult = Result<(), ()>;
#[inline]
const fn setter_result(successful: bool) -> SetterResult {
if successful { Ok(()) } else { Err(()) }
}
impl Url {
pub fn parse<Input>(input: Input, base: Option<&str>) -> Result<Self, ParseUrlError<Input>>
where
Input: AsRef<str>,
{
let url_aggregator = match base {
Some(base) => unsafe {
ffi::ada_parse_with_base(
input.as_ref().as_ptr().cast(),
input.as_ref().len(),
base.as_ptr().cast(),
base.len(),
)
},
None => unsafe { ffi::ada_parse(input.as_ref().as_ptr().cast(), input.as_ref().len()) },
};
if unsafe { ffi::ada_is_valid(url_aggregator) } {
Ok(url_aggregator.into())
} else {
Err(ParseUrlError { input })
}
}
#[must_use]
pub fn can_parse(input: &str, base: Option<&str>) -> bool {
unsafe {
if let Some(base) = base {
ffi::ada_can_parse_with_base(
input.as_ptr().cast(),
input.len(),
base.as_ptr().cast(),
base.len(),
)
} else {
ffi::ada_can_parse(input.as_ptr().cast(), input.len())
}
}
}
#[must_use]
pub fn host_type(&self) -> HostType {
HostType::from(unsafe { ffi::ada_get_host_type(self.0) })
}
#[must_use]
pub fn scheme_type(&self) -> SchemeType {
SchemeType::from(unsafe { ffi::ada_get_scheme_type(self.0) })
}
#[must_use]
#[cfg(feature = "std")]
pub fn origin(&self) -> String {
unsafe { ffi::ada_get_origin(self.0) }.to_string()
}
#[must_use]
pub fn href(&self) -> &str {
unsafe { ffi::ada_get_href(self.0) }.as_str()
}
#[allow(clippy::result_unit_err)]
pub fn set_href(&mut self, input: &str) -> SetterResult {
setter_result(unsafe { ffi::ada_set_href(self.0, input.as_ptr().cast(), input.len()) })
}
#[must_use]
pub fn username(&self) -> &str {
unsafe { ffi::ada_get_username(self.0) }.as_str()
}
#[allow(clippy::result_unit_err)]
pub fn set_username(&mut self, input: Option<&str>) -> SetterResult {
setter_result(unsafe {
ffi::ada_set_username(
self.0,
input.unwrap_or("").as_ptr().cast(),
input.map_or(0, str::len),
)
})
}
#[must_use]
pub fn password(&self) -> &str {
unsafe { ffi::ada_get_password(self.0) }.as_str()
}
#[allow(clippy::result_unit_err)]
pub fn set_password(&mut self, input: Option<&str>) -> SetterResult {
setter_result(unsafe {
ffi::ada_set_password(
self.0,
input.unwrap_or("").as_ptr().cast(),
input.map_or(0, str::len),
)
})
}
#[must_use]
pub fn port(&self) -> &str {
unsafe { ffi::ada_get_port(self.0) }.as_str()
}
#[allow(clippy::result_unit_err)]
pub fn set_port(&mut self, input: Option<&str>) -> SetterResult {
if let Some(value) = input {
setter_result(unsafe { ffi::ada_set_port(self.0, value.as_ptr().cast(), value.len()) })
} else {
unsafe { ffi::ada_clear_port(self.0) }
Ok(())
}
}
#[must_use]
pub fn hash(&self) -> &str {
unsafe { ffi::ada_get_hash(self.0) }.as_str()
}
pub fn set_hash(&mut self, input: Option<&str>) {
match input {
Some(value) => unsafe { ffi::ada_set_hash(self.0, value.as_ptr().cast(), value.len()) },
None => unsafe { ffi::ada_clear_hash(self.0) },
}
}
#[must_use]
pub fn host(&self) -> &str {
unsafe { ffi::ada_get_host(self.0) }.as_str()
}
#[allow(clippy::result_unit_err)]
pub fn set_host(&mut self, input: Option<&str>) -> SetterResult {
setter_result(unsafe {
ffi::ada_set_host(
self.0,
input.unwrap_or("").as_ptr().cast(),
input.map_or(0, str::len),
)
})
}
#[must_use]
pub fn hostname(&self) -> &str {
unsafe { ffi::ada_get_hostname(self.0) }.as_str()
}
#[allow(clippy::result_unit_err)]
pub fn set_hostname(&mut self, input: Option<&str>) -> SetterResult {
setter_result(unsafe {
ffi::ada_set_hostname(
self.0,
input.unwrap_or("").as_ptr().cast(),
input.map_or(0, str::len),
)
})
}
#[must_use]
pub fn pathname(&self) -> &str {
unsafe { ffi::ada_get_pathname(self.0) }.as_str()
}
#[allow(clippy::result_unit_err)]
pub fn set_pathname(&mut self, input: Option<&str>) -> SetterResult {
setter_result(unsafe {
ffi::ada_set_pathname(
self.0,
input.unwrap_or("").as_ptr().cast(),
input.map_or(0, str::len),
)
})
}
#[must_use]
pub fn search(&self) -> &str {
unsafe { ffi::ada_get_search(self.0) }.as_str()
}
pub fn set_search(&mut self, input: Option<&str>) {
match input {
Some(value) => unsafe {
ffi::ada_set_search(self.0, value.as_ptr().cast(), value.len());
},
None => unsafe { ffi::ada_clear_search(self.0) },
}
}
#[must_use]
pub fn protocol(&self) -> &str {
unsafe { ffi::ada_get_protocol(self.0) }.as_str()
}
#[allow(clippy::result_unit_err)]
pub fn set_protocol(&mut self, input: &str) -> SetterResult {
setter_result(unsafe { ffi::ada_set_protocol(self.0, input.as_ptr().cast(), input.len()) })
}
#[must_use]
pub fn has_credentials(&self) -> bool {
unsafe { ffi::ada_has_credentials(self.0) }
}
#[must_use]
pub fn has_empty_hostname(&self) -> bool {
unsafe { ffi::ada_has_empty_hostname(self.0) }
}
#[must_use]
pub fn has_hostname(&self) -> bool {
unsafe { ffi::ada_has_hostname(self.0) }
}
#[must_use]
pub fn has_non_empty_username(&self) -> bool {
unsafe { ffi::ada_has_non_empty_username(self.0) }
}
#[must_use]
pub fn has_non_empty_password(&self) -> bool {
unsafe { ffi::ada_has_non_empty_password(self.0) }
}
#[must_use]
pub fn has_port(&self) -> bool {
unsafe { ffi::ada_has_port(self.0) }
}
#[must_use]
pub fn has_password(&self) -> bool {
unsafe { ffi::ada_has_password(self.0) }
}
#[must_use]
pub fn has_hash(&self) -> bool {
unsafe { ffi::ada_has_hash(self.0) }
}
#[must_use]
pub fn has_search(&self) -> bool {
unsafe { ffi::ada_has_search(self.0) }
}
#[must_use]
pub fn as_str(&self) -> &str {
self.href()
}
#[must_use]
pub fn components(&self) -> UrlComponents {
unsafe { ffi::ada_get_components(self.0).as_ref().unwrap() }.into()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Url {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
#[cfg(feature = "serde")]
#[cfg(feature = "std")]
impl<'de> serde::Deserialize<'de> for Url {
fn deserialize<D>(deserializer: D) -> Result<Url, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::{Error, Unexpected, Visitor};
struct UrlVisitor;
impl Visitor<'_> for UrlVisitor {
type Value = Url;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string representing an URL")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: Error,
{
Url::parse(s, None).map_err(|err| {
let err_s = std::format!("{}", err);
Error::invalid_value(Unexpected::Str(s), &err_s.as_str())
})
}
}
deserializer.deserialize_str(UrlVisitor)
}
}
unsafe impl Send for Url {}
unsafe impl Sync for Url {}
impl PartialEq for Url {
fn eq(&self, other: &Self) -> bool {
self.href() == other.href()
}
}
impl PartialOrd for Url {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Url {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.href().cmp(other.href())
}
}
impl hash::Hash for Url {
fn hash<H: hash::Hasher>(&self, state: &mut H) {
self.href().hash(state);
}
}
impl borrow::Borrow<str> for Url {
fn borrow(&self) -> &str {
self.href()
}
}
impl AsRef<[u8]> for Url {
fn as_ref(&self) -> &[u8] {
self.href().as_bytes()
}
}
#[cfg(feature = "std")]
impl From<Url> for String {
fn from(val: Url) -> Self {
val.href().to_owned()
}
}
impl fmt::Debug for Url {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Url")
.field("href", &self.href())
.field("components", &self.components())
.finish()
}
}
impl<'input> TryFrom<&'input str> for Url {
type Error = ParseUrlError<&'input str>;
fn try_from(value: &'input str) -> Result<Self, Self::Error> {
Self::parse(value, None)
}
}
#[cfg(feature = "std")]
impl TryFrom<String> for Url {
type Error = ParseUrlError<String>;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value, None)
}
}
#[cfg(feature = "std")]
impl<'input> TryFrom<&'input String> for Url {
type Error = ParseUrlError<&'input String>;
fn try_from(value: &'input String) -> Result<Self, Self::Error> {
Self::parse(value, None)
}
}
impl ops::Deref for Url {
type Target = str;
fn deref(&self) -> &Self::Target {
self.href()
}
}
impl AsRef<str> for Url {
fn as_ref(&self) -> &str {
self.href()
}
}
impl fmt::Display for Url {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.href())
}
}
#[cfg(feature = "std")]
impl core::str::FromStr for Url {
type Err = ParseUrlError<Box<str>>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s, None).map_err(|ParseUrlError { input }| ParseUrlError {
input: input.into(),
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn should_display_serialization() {
let tests = [
("http://example.com/", "http://example.com/"),
("HTTP://EXAMPLE.COM", "http://example.com/"),
("http://user:pwd@domain.com", "http://user:pwd@domain.com/"),
(
"HTTP://EXAMPLE.COM/FOO/BAR?K1=V1&K2=V2",
"http://example.com/FOO/BAR?K1=V1&K2=V2",
),
(
"http://example.com/🦀/❤️/",
"http://example.com/%F0%9F%A6%80/%E2%9D%A4%EF%B8%8F/",
),
(
"https://example.org/hello world.html",
"https://example.org/hello%20world.html",
),
(
"https://三十六計.org/走為上策/",
"https://xn--ehq95fdxbx86i.org/%E8%B5%B0%E7%82%BA%E4%B8%8A%E7%AD%96/",
),
];
for (value, expected) in tests {
let url = Url::parse(value, None).expect("Should have parsed url");
assert_eq!(url.as_str(), expected);
}
}
#[test]
fn try_from_ok() {
let url = Url::try_from("http://example.com/foo/bar?k1=v1&k2=v2");
#[cfg(feature = "std")]
std::dbg!(&url);
let url = url.unwrap();
assert_eq!(url.href(), "http://example.com/foo/bar?k1=v1&k2=v2");
assert_eq!(
url,
Url::parse("http://example.com/foo/bar?k1=v1&k2=v2", None).unwrap(),
);
}
#[test]
fn try_from_err() {
let url = Url::try_from("this is not a url");
#[cfg(feature = "std")]
std::dbg!(&url);
let error = url.unwrap_err();
#[cfg(feature = "std")]
assert_eq!(error.to_string(), r#"Invalid url: "this is not a url""#);
assert_eq!(error.input, "this is not a url");
}
#[test]
fn should_compare_urls() {
let tests = [
("http://example.com/", "http://example.com/", true),
("http://example.com/", "https://example.com/", false),
("http://example.com#", "https://example.com/#", false),
("http://example.com", "https://example.com#", false),
(
"https://user:pwd@example.com",
"https://user:pwd@example.com",
true,
),
];
for (left, right, expected) in tests {
let left_url = Url::parse(left, None).expect("Should have parsed url");
let right_url = Url::parse(right, None).expect("Should have parsed url");
assert_eq!(
left_url == right_url,
expected,
"left: {left}, right: {right}, expected: {expected}",
);
}
}
#[test]
fn should_order_alphabetically() {
let left = Url::parse("https://example.com/", None).expect("Should have parsed url");
let right = Url::parse("https://zoo.tld/", None).expect("Should have parsed url");
assert!(left < right);
let left = Url::parse("https://c.tld/", None).expect("Should have parsed url");
let right = Url::parse("https://a.tld/", None).expect("Should have parsed url");
assert!(right < left);
}
#[test]
fn should_parse_simple_url() {
let mut out = Url::parse(
"https://username:password@google.com:9090/search?query#hash",
None,
)
.expect("Should have parsed a simple url");
#[cfg(feature = "std")]
assert_eq!(out.origin(), "https://google.com:9090");
assert_eq!(
out.href(),
"https://username:password@google.com:9090/search?query#hash"
);
assert_eq!(out.scheme_type(), SchemeType::Https);
out.set_username(Some("new-username")).unwrap();
assert_eq!(out.username(), "new-username");
out.set_password(Some("new-password")).unwrap();
assert_eq!(out.password(), "new-password");
out.set_port(Some("4242")).unwrap();
assert_eq!(out.port(), "4242");
out.set_port(None).unwrap();
assert_eq!(out.port(), "");
out.set_hash(Some("#new-hash"));
assert_eq!(out.hash(), "#new-hash");
out.set_host(Some("yagiz.co:9999")).unwrap();
assert_eq!(out.host(), "yagiz.co:9999");
out.set_hostname(Some("domain.com")).unwrap();
assert_eq!(out.hostname(), "domain.com");
out.set_pathname(Some("/new-search")).unwrap();
assert_eq!(out.pathname(), "/new-search");
out.set_pathname(None).unwrap();
assert_eq!(out.pathname(), "/");
out.set_search(Some("updated-query"));
assert_eq!(out.search(), "?updated-query");
out.set_protocol("wss").unwrap();
assert_eq!(out.protocol(), "wss:");
assert_eq!(out.scheme_type(), SchemeType::Wss);
assert!(out.has_credentials());
assert!(out.has_non_empty_username());
assert!(out.has_non_empty_password());
assert!(out.has_search());
assert!(out.has_hash());
assert!(out.has_password());
assert_eq!(out.host_type(), HostType::Domain);
}
#[test]
fn scheme_types() {
assert_eq!(
Url::parse("file:///foo/bar", None)
.expect("bad url")
.scheme_type(),
SchemeType::File
);
assert_eq!(
Url::parse("ws://example.com/ws", None)
.expect("bad url")
.scheme_type(),
SchemeType::Ws
);
assert_eq!(
Url::parse("wss://example.com/wss", None)
.expect("bad url")
.scheme_type(),
SchemeType::Wss
);
assert_eq!(
Url::parse("ftp://example.com/file.txt", None)
.expect("bad url")
.scheme_type(),
SchemeType::Ftp
);
assert_eq!(
Url::parse("http://example.com/file.txt", None)
.expect("bad url")
.scheme_type(),
SchemeType::Http
);
assert_eq!(
Url::parse("https://example.com/file.txt", None)
.expect("bad url")
.scheme_type(),
SchemeType::Https
);
assert_eq!(
Url::parse("foo://example.com", None)
.expect("bad url")
.scheme_type(),
SchemeType::NotSpecial
);
}
#[test]
fn can_parse_simple_url() {
assert!(Url::can_parse("https://google.com", None));
assert!(Url::can_parse("/helo", Some("https://www.google.com")));
}
#[cfg(feature = "std")]
#[cfg(feature = "serde")]
#[test]
fn test_serde_serialize_deserialize() {
let input = "https://www.google.com";
let output = "\"https://www.google.com/\"";
let url = Url::parse(&input, None).unwrap();
assert_eq!(serde_json::to_string(&url).unwrap(), output);
let deserialized: Url = serde_json::from_str(&output).unwrap();
assert_eq!(deserialized.href(), "https://www.google.com/");
}
#[test]
fn should_clone() {
let first = Url::parse("https://lemire.me", None).unwrap();
let mut second = first.clone();
second.set_href("https://yagiz.co").unwrap();
assert_ne!(first.href(), second.href());
assert_eq!(first.href(), "https://lemire.me/");
assert_eq!(second.href(), "https://yagiz.co/");
}
#[test]
fn should_handle_empty_host() {
let url = Url::parse("file:///C:/Users/User/Documents/example.pdf", None).unwrap();
assert_eq!(url.host(), "");
assert_eq!(url.hostname(), "");
}
}