use super::Res;
use anyhow::anyhow;
use nom::{
branch::alt,
bytes::complete::tag,
character::complete::{alphanumeric1, digit1, hex_digit1, one_of, u16},
combinator::{map, map_res, opt, recognize},
multi::{count, many0, many1, separated_list0},
sequence::{delimited, pair, preceded, terminated, tuple},
Finish,
};
use std::{
fmt::Display,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
path::PathBuf,
str::FromStr,
};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct HostSpec {
pub host: Option<Host>,
pub port: Option<u16>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Host {
Path(PathBuf),
Name(String),
Ip(IpAddr),
}
impl Display for Host {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Host::Path(path) => {
write!(f, "{}", path.to_str().unwrap_or("<invalid>"))
},
Host::Name(name) => write!(f, "{name}"),
Host::Ip(IpAddr::V4(ip)) => write!(f, "{ip}"),
Host::Ip(IpAddr::V6(ip)) => write!(f, "[{ip}]"),
}
}
}
impl FromStr for Host {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
host(s)
.finish()
.map(|(_, host)| host)
.map_err(|e| anyhow!(e.to_string()))
}
}
pub(crate) fn hostspecs(i: &str) -> Res<&str, Vec<HostSpec>> {
many0(hostspec)(i)
}
pub(crate) fn hostspec(i: &str) -> Res<&str, HostSpec> {
terminated(
alt((
map(pair(host, port), |(host, port)| HostSpec {
host: Some(host),
port: Some(port),
}),
map(pair(host, opt(port)), |(host, port)| HostSpec {
host: Some(host),
port,
}),
map(pair(opt(host), port), |(host, port)| HostSpec {
host: host.map(std::convert::Into::into),
port: Some(port),
}),
)),
opt(tag(",")),
)(i)
}
fn port(i: &str) -> Res<&str, u16> {
preceded(tag(":"), u16)(i)
}
fn host(i: &str) -> Res<&str, Host> {
alt((
map(ipv4, |ip| Host::Ip(IpAddr::V4(ip))),
map(ipv6, |ip| Host::Ip(IpAddr::V6(ip))),
map(
recognize(many1(alt((alphanumeric1, recognize(one_of("._-")))))),
|s: &str| Host::Name(s.to_string()),
),
))(i)
}
fn ipv6(i: &str) -> Res<&str, Ipv6Addr> {
delimited(
tag("["),
map_res(
recognize(tuple((
separated_list0(tag(":"), hex_digit1),
tag("::"),
hex_digit1,
))),
Ipv6Addr::from_str,
),
tag("]"),
)(i)
}
fn ipv4(i: &str) -> Res<&str, Ipv4Addr> {
map_res(
recognize(pair(count(pair(digit1, tag(".")), 3), digit1)),
Ipv4Addr::from_str,
)(i)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_port() {
for (input, rem, output) in vec![(":123", "", 123)] {
assert_eq!(
port(input).unwrap(),
(rem, output),
"input: {:?}; rem: {:?}",
input,
rem
);
}
for input in vec![",123", ",:123", ":port"] {
assert!(port(input).is_err());
}
}
#[test]
fn test_host() {
for (input, rem, output) in vec![
("myhost", "", Host::Name("myhost".into())),
("myhost.mydomain", "", Host::Name("myhost.mydomain".into())),
(
"subdomain.myhost.com",
"",
Host::Name("subdomain.myhost.com".into()),
),
(
"sub-domain.my_host.com",
"",
Host::Name("sub-domain.my_host.com".into()),
),
(
"sub-domain.my_host.com/db",
"/db",
Host::Name("sub-domain.my_host.com".into()),
),
(
"[2001:db8::1234]/database",
"/database",
Host::Ip(IpAddr::from_str("2001:db8::1234").unwrap()),
),
] {
assert_eq!(
host(input),
Ok((rem, output)),
"input: {input:?}; rem: {rem:?}",
);
}
for input in [",host", "/db"] {
assert!(host(input).is_err());
}
}
#[test]
fn test_ipv6() {
for (input, rem, output) in [(
"[2001:db8::1234]",
"",
Ipv6Addr::from_str("2001:db8::1234").unwrap(),
)] {
assert_eq!(
ipv6(input),
Ok((rem, output)),
"input: {input:?}; rem: {rem:?}",
);
}
}
#[test]
fn test_ipv4() {
for (input, rem, output) in
vec![("192.168.0.1", "", Ipv4Addr::new(192, 168, 0, 1))]
{
assert_eq!(
ipv4(input).unwrap(),
(rem, output),
"input: {:?}; rem: {:?}",
input,
rem
);
}
}
macro_rules! host {
($s:expr) => {
Some(host($s).unwrap().1)
};
}
#[test]
fn test_hostspec() {
for (input, rem, output) in vec![(
"myhost:123",
"",
HostSpec {
host: host!("myhost"),
port: Some(123),
},
)] {
assert_eq!(
hostspec(input).unwrap(),
(rem, output),
"input: {input:?}; rem: {rem:?}",
);
}
for input in &["", ",", ",host:123"] {
assert!(hostspec(input).is_err());
}
}
#[test]
fn test_hostspecs() {
for (input, rem, output) in vec![
("", "", vec![]),
(", ", ", ", vec![]),
(",host:123", ",host:123", vec![]),
(
"myhost:123",
"",
vec![HostSpec {
host: host!("myhost"),
port: Some(123),
}],
),
(
"myhost:123,secondhost:65535",
"",
vec![
HostSpec {
host: host!("myhost"),
port: Some(123),
},
HostSpec {
host: host!("secondhost"),
port: Some(65535),
},
],
),
] {
assert_eq!(
hostspecs(input).unwrap(),
(rem, output),
"input: {input:?}; rem: {rem:?}",
);
}
for input in &[",", ",host:123"] {}
}
}