#![no_std]
#[cfg(feature = "defmt")]
mod defmt_impl;
mod error;
pub use crate::error::Error;
use core::{
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
str::FromStr,
};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Url<'a> {
scheme: UrlScheme,
host: &'a str,
is_host_ipv6: bool,
scope_id: Option<u32>,
port: Option<u16>,
path: &'a str,
}
impl core::fmt::Debug for Url<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}://", self.scheme.as_str())?;
if self.is_host_ipv6 {
write!(f, "[{}", self.host)?;
if let Some(scope_id) = self.scope_id {
write!(f, "%{}", scope_id)?;
}
write!(f, "]")?;
} else {
write!(f, "{}", self.host)?;
}
if let Some(port) = self.port {
write!(f, ":{}", port)?
}
write!(f, "{}", self.path)
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Url<'_> {
fn format(&self, f: defmt::Formatter) {
use defmt::write;
write!(f, "{}://", self.scheme.as_str());
if self.is_host_ipv6 {
write!(f, "[{}", self.host);
if let Some(scope_id) = self.scope_id {
write!(f, "%{}", scope_id);
}
write!(f, "]");
} else {
write!(f, "{}", self.host);
}
if let Some(port) = self.port {
write!(f, ":{}", port)
}
write!(f, "{}", self.path)
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum UrlScheme {
HTTP,
HTTPS,
MQTT,
MQTTS,
}
impl UrlScheme {
pub fn as_str(&self) -> &str {
match self {
UrlScheme::HTTP => "http",
UrlScheme::HTTPS => "https",
UrlScheme::MQTT => "mqtt",
UrlScheme::MQTTS => "mqtts",
}
}
pub const fn default_port(&self) -> u16 {
match self {
UrlScheme::HTTP => 80,
UrlScheme::HTTPS => 443,
UrlScheme::MQTT => 1883,
UrlScheme::MQTTS => 8883,
}
}
}
impl<'a> Url<'a> {
pub fn parse(url: &'a str) -> Result<Url<'a>, Error> {
let mut parts = url.split("://");
let scheme = parts.next().unwrap();
let host_port_path = parts.next().ok_or(Error::NoScheme)?;
let scheme = if scheme.eq_ignore_ascii_case("http") {
Ok(UrlScheme::HTTP)
} else if scheme.eq_ignore_ascii_case("https") {
Ok(UrlScheme::HTTPS)
} else {
Err(Error::UnsupportedScheme)
}?;
let (host_port, path) = if let Some(path_delim) = host_port_path.find('/') {
let host_port = &host_port_path[..path_delim];
let path = &host_port_path[path_delim..];
let path = if path.is_empty() { "/" } else { path };
(host_port, path)
} else {
(host_port_path, "/")
};
let (host, port, is_host_ipv6, scope_id) = if host_port.starts_with('[') {
let address_block_end = host_port.find(']').ok_or(Error::Ipv6AddressInvalid)?;
let mut address_range = 1..address_block_end;
let scope_id = if let Some(scope_id_start) = host_port[address_range.clone()].find('%')
{
address_range = 1..scope_id_start + 1;
Some(&host_port[scope_id_start + 2..address_block_end])
} else {
None
};
let port = if let Some(port) = host_port
.get(address_block_end + 1..)
.filter(|port| !port.is_empty())
{
Some(
port.strip_prefix(':')
.ok_or(Error::LeftoverTokensAfterIpv6)?,
)
} else {
None
};
(&host_port[address_range], port, true, scope_id)
} else if let Some(port_delim) = host_port.find(':') {
(
&host_port[..port_delim],
host_port.get(port_delim + 1..),
false,
None,
)
} else {
(host_port, None, false, None)
};
if port == Some("") {
return Err(Error::NoPortAfterColon);
}
if scope_id == Some("") {
return Err(Error::NoScopeIdAfterPercent);
}
let port = port
.map(|port| port.parse::<u16>())
.transpose()
.map_err(|_| Error::InvalidPort)?;
let scope_id = scope_id
.map(|scope_id| scope_id.parse::<u32>())
.transpose()
.map_err(|_| Error::InvalidScopeId)?;
Ok(Self {
scheme,
host,
scope_id,
is_host_ipv6,
path,
port,
})
}
pub fn scheme(&self) -> UrlScheme {
self.scheme
}
pub fn host(&self) -> &'a str {
self.host
}
pub fn host_ip(&self) -> Option<IpAddr> {
if self.is_host_ipv6 {
Ipv6Addr::from_str(self.host).ok().map(|ip| ip.into())
} else {
Ipv4Addr::from_str(self.host).ok().map(|ip| ip.into())
}
}
pub fn host_socket_address(&self) -> Option<SocketAddr> {
Some(match self.host_ip()? {
IpAddr::V4(address) => {
SocketAddr::V4(SocketAddrV4::new(address, self.port_or_default()))
}
IpAddr::V6(address) => SocketAddr::V6(SocketAddrV6::new(
address,
self.port_or_default(),
0,
self.scope_id_or_default(),
)),
})
}
pub fn port(&self) -> Option<u16> {
self.port
}
pub fn port_or_default(&self) -> u16 {
self.port.unwrap_or_else(|| self.scheme.default_port())
}
pub fn scope_id(&self) -> Option<u32> {
self.scope_id
}
pub fn scope_id_or_default(&self) -> u32 {
self.scope_id.unwrap_or(0)
}
pub fn path(&self) -> &'a str {
self.path
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
#[test]
fn test_parse_no_scheme() {
assert_eq!(Error::NoScheme, Url::parse("").err().unwrap());
assert_eq!(Error::NoScheme, Url::parse("http:/").err().unwrap());
}
#[test]
fn test_parse_unsupported_scheme() {
assert_eq!(
Error::UnsupportedScheme,
Url::parse("something://").err().unwrap()
);
}
#[test]
fn test_parse_no_host() {
let url = Url::parse("http://").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTP);
assert_eq!(url.host(), "");
assert_eq!(url.port_or_default(), 80);
assert_eq!(url.path(), "/");
}
#[test]
fn test_parse_minimal() {
let url = Url::parse("http://localhost").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTP);
assert_eq!(url.host(), "localhost");
assert_eq!(url.port_or_default(), 80);
assert_eq!(url.path(), "/");
assert_eq!("http://localhost/", std::format!("{:?}", url));
}
#[test]
fn test_parse_path() {
let url = Url::parse("http://localhost/foo/bar").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTP);
assert_eq!(url.host(), "localhost");
assert_eq!(url.port_or_default(), 80);
assert_eq!(url.path(), "/foo/bar");
assert_eq!("http://localhost/foo/bar", std::format!("{:?}", url));
}
#[test]
fn test_parse_path_with_colon() {
let url = Url::parse("http://localhost/foo/bar:123").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTP);
assert_eq!(url.host(), "localhost");
assert_eq!(url.port_or_default(), 80);
assert_eq!(url.path(), "/foo/bar:123");
assert_eq!("http://localhost/foo/bar:123", std::format!("{:?}", url));
}
#[test]
fn test_parse_port() {
let url = Url::parse("http://localhost:8088").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTP);
assert_eq!(url.host(), "localhost");
assert_eq!(url.port().unwrap(), 8088);
assert_eq!(url.path(), "/");
assert_eq!("http://localhost:8088/", std::format!("{:?}", url));
}
#[test]
fn test_parse_port_path() {
let url = Url::parse("http://localhost:8088/foo/bar").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTP);
assert_eq!(url.host(), "localhost");
assert_eq!(url.port().unwrap(), 8088);
assert_eq!(url.path(), "/foo/bar");
assert_eq!("http://localhost:8088/foo/bar", std::format!("{:?}", url));
}
#[test]
fn test_parse_scheme() {
let url = Url::parse("https://localhost/").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTPS);
assert_eq!(url.host(), "localhost");
assert_eq!(url.port_or_default(), 443);
assert_eq!(url.path(), "/");
assert_eq!("https://localhost/", std::format!("{:?}", url));
}
#[test]
fn test_parse_ipv4() {
let url = Url::parse("https://127.0.0.1:1337/foo/bar").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTPS);
assert_eq!(url.host(), "127.0.0.1");
assert_eq!(
url.host_socket_address().unwrap(),
SocketAddr::from_str("127.0.0.1:1337").unwrap()
);
assert_eq!(url.port_or_default(), 1337);
assert_eq!(url.path(), "/foo/bar");
assert_eq!("https://127.0.0.1:1337/foo/bar", std::format!("{:?}", url));
}
#[test]
fn test_parse_ipv6() {
let url = Url::parse("https://[fe80::%1]/foo/bar").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTPS);
assert_eq!(url.host(), "fe80::");
assert_eq!(
url.host_socket_address().unwrap(),
SocketAddr::from_str("[fe80::%1]:443").unwrap()
);
assert_eq!(url.port_or_default(), 443);
assert_eq!(url.path(), "/foo/bar");
assert_eq!("https://[fe80::%1]/foo/bar", std::format!("{:?}", url));
}
#[test]
fn test_parse_ipv6_port() {
let url = Url::parse("https://[fe80::%1]:1337/foo/bar").unwrap();
assert_eq!(url.scheme(), UrlScheme::HTTPS);
assert_eq!(url.host(), "fe80::");
assert_eq!(
url.host_socket_address().unwrap(),
SocketAddr::from_str("[fe80::%1]:1337").unwrap()
);
assert_eq!(url.port_or_default(), 1337);
assert_eq!(url.path(), "/foo/bar");
assert_eq!("https://[fe80::%1]:1337/foo/bar", std::format!("{:?}", url));
}
#[test]
fn test_invalid_ipv6() {
assert_eq!(
Url::parse("http://[fe80::/"),
Err(Error::Ipv6AddressInvalid)
);
}
#[test]
fn test_leftover_tokens_ipv6() {
assert_eq!(
Url::parse("http://[fe80]a/"),
Err(Error::LeftoverTokensAfterIpv6)
);
}
#[test]
fn test_no_port_after_colon() {
assert_eq!(
Url::parse("http://localhost:/"),
Err(Error::NoPortAfterColon)
);
assert_eq!(
Url::parse("http://[fe80::]:/"),
Err(Error::NoPortAfterColon)
);
}
#[test]
fn test_invalid_port() {
assert_eq!(
Url::parse("http://localhost:12E4/"),
Err(Error::InvalidPort)
);
assert_eq!(Url::parse("http://[fe80::]:12E4/"), Err(Error::InvalidPort));
}
}