use std::{convert::TryFrom, fmt::Display, net::IpAddr};
use email_address::EmailAddress;
use ip_network::Ipv6Network;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{ErrorKind, Result};
use super::raw::RawUri;
#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Uri {
pub(crate) url: Url,
}
impl Uri {
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
self.url.as_ref()
}
#[inline]
#[must_use]
pub fn scheme(&self) -> &str {
self.url.scheme()
}
#[inline]
pub(crate) fn set_scheme(&mut self, scheme: &str) -> std::result::Result<(), ()> {
self.url.set_scheme(scheme)
}
#[inline]
#[must_use]
pub fn domain(&self) -> Option<&str> {
self.url.domain()
}
#[inline]
#[must_use]
pub fn path(&self) -> &str {
self.url.path()
}
#[inline]
#[must_use]
pub fn path_segments(&self) -> Option<std::str::Split<'_, char>> {
self.url.path_segments()
}
#[must_use]
pub fn host_ip(&self) -> Option<IpAddr> {
match self.url.host()? {
url::Host::Domain(_) => None,
url::Host::Ipv4(v4_addr) => Some(v4_addr.into()),
url::Host::Ipv6(v6_addr) => Some(v6_addr.into()),
}
}
pub(crate) fn to_https(&self) -> Result<Uri> {
let mut https_uri = self.clone();
https_uri
.set_scheme("https")
.map_err(|()| ErrorKind::InvalidURI(self.clone()))?;
Ok(https_uri)
}
#[inline]
#[must_use]
pub fn is_mail(&self) -> bool {
self.scheme() == "mailto"
}
#[inline]
#[must_use]
pub fn is_tel(&self) -> bool {
self.scheme() == "tel"
}
#[inline]
#[must_use]
pub fn is_file(&self) -> bool {
self.scheme() == "file"
}
#[inline]
#[must_use]
pub fn is_data(&self) -> bool {
self.scheme() == "data"
}
#[inline]
#[must_use]
pub fn is_loopback(&self) -> bool {
match self.url.host() {
Some(url::Host::Ipv4(addr)) => addr.is_loopback(),
Some(url::Host::Ipv6(addr)) => addr.is_loopback(),
_ => false,
}
}
#[inline]
#[must_use]
pub fn is_private(&self) -> bool {
match self.url.host() {
Some(url::Host::Ipv4(addr)) => addr.is_private(),
Some(url::Host::Ipv6(addr)) => Ipv6Network::from(addr).is_unique_local(),
_ => false,
}
}
#[inline]
#[must_use]
pub fn is_link_local(&self) -> bool {
match self.url.host() {
Some(url::Host::Ipv4(addr)) => addr.is_link_local(),
Some(url::Host::Ipv6(addr)) => Ipv6Network::from(addr).is_unicast_link_local(),
_ => false,
}
}
}
impl AsRef<str> for Uri {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<Url> for Uri {
fn from(url: Url) -> Self {
Self { url }
}
}
impl TryFrom<String> for Uri {
type Error = ErrorKind;
fn try_from(s: String) -> Result<Self> {
Uri::try_from(s.as_ref())
}
}
impl TryFrom<&str> for Uri {
type Error = ErrorKind;
fn try_from(s: &str) -> Result<Self> {
if s.is_empty() {
return Err(ErrorKind::EmptyUrl);
}
match Url::parse(s) {
Ok(uri) => Ok(uri.into()),
Err(err) => {
if EmailAddress::is_valid(s) {
if let Ok(uri) = Url::parse(&format!("mailto:{s}")) {
return Ok(uri.into());
}
}
Err(ErrorKind::ParseUrl(err, s.to_owned()))
}
}
}
}
impl TryFrom<RawUri> for Uri {
type Error = ErrorKind;
fn try_from(raw_uri: RawUri) -> Result<Self> {
let s = raw_uri.text;
Uri::try_from(s.as_ref())
}
}
impl Display for Uri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
convert::TryFrom,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
};
use test_utils::mail;
use test_utils::website;
#[test]
fn test_ipv4_uri_is_loopback() {
let uri = Uri::try_from("http://127.0.0.0").unwrap();
assert!(uri.is_loopback());
}
#[test]
fn test_ipv6_uri_is_loopback() {
let uri = Uri::try_from("https://[::1]").unwrap();
assert!(uri.is_loopback());
}
#[test]
fn test_uri_from_url() {
assert!(Uri::try_from("").is_err());
assert_eq!(
Uri::try_from("https://example.com"),
Ok(website!("https://example.com"))
);
assert_eq!(
Uri::try_from("https://example.com/@test/testing"),
Ok(website!("https://example.com/@test/testing"))
);
}
#[test]
fn test_uri_from_email_str() {
assert_eq!(
Uri::try_from("mail@example.com"),
Ok(mail!("mail@example.com"))
);
assert_eq!(
Uri::try_from("mailto:mail@example.com"),
Ok(mail!("mail@example.com"))
);
assert_eq!(
Uri::try_from("mail@example.com?foo=bar"),
Ok(mail!("mail@example.com?foo=bar"))
);
}
#[test]
fn test_uri_tel() {
assert_eq!(
Uri::try_from("tel:1234567890"),
Ok(Uri::try_from("tel:1234567890").unwrap())
);
}
#[test]
fn test_uri_host_ip_v4() {
assert_eq!(
website!("http://127.0.0.1").host_ip(),
Some(IpAddr::V4(Ipv4Addr::LOCALHOST))
);
}
#[test]
fn test_uri_host_ip_v6() {
assert_eq!(
website!("https://[2020::0010]").host_ip(),
Some(IpAddr::V6(Ipv6Addr::new(0x2020, 0, 0, 0, 0, 0, 0, 0x10)))
);
}
#[test]
fn test_uri_host_ip_no_ip() {
assert!(website!("https://some.cryptic/url").host_ip().is_none());
}
#[test]
fn test_localhost() {
assert_eq!(
website!("http://127.0.0.1").host_ip(),
Some(IpAddr::V4(Ipv4Addr::LOCALHOST))
);
}
#[test]
fn test_convert_to_https() {
assert_eq!(
website!("http://example.com").to_https().unwrap(),
website!("https://example.com")
);
assert_eq!(
website!("https://example.com").to_https().unwrap(),
website!("https://example.com")
);
}
#[test]
fn test_file_uri() {
assert!(Uri::try_from("file:///path/to/file").unwrap().is_file());
}
}