Skip to content

Commit eec7fba

Browse files
committed
feat: ipv6 zoneid
1 parent 37d816c commit eec7fba

File tree

3 files changed

+144
-73
lines changed

3 files changed

+144
-73
lines changed

faup/src/grammar.pest

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ userinfo = ${ username ~ (":" ~ password)? ~ "@" }
88
// this rule is used to check if the hostname is actually
99
// a valid ipv4 address as hostname rule matches any ipv4
1010
ipv4 = ${ SOI ~ (ASCII_DIGIT{1, 3} ~ "."){3} ~ ASCII_DIGIT{1, 3} ~ EOI }
11-
host = ${ "[" ~ ipv6 ~ "]" | hostname }
11+
zone_id = ${ "%" ~ (!"]" ~ ANY)* }
12+
host = ${ ("[" ~ ipv6 ~ zone_id? ~ "]") | hostname }
1213
checked_host = _{ SOI ~ (ipv6 | hostname) ~ EOI }
1314
domain_part = ${ (!(":" | "?" | "/" | "#" | "." | WHITE_SPACE) ~ ANY)+ }
1415
tld = ${ (!(":" | "?" | "/" | "#" | "." | WHITE_SPACE) ~ ANY)+ }
1516
hostname = ${ (((domain_part ~ ".")+ ~ tld) | domain_part) }
1617

17-
ipv6 = ${ (ASCII_HEX_DIGIT{,4} ~ ":"){2, 7} ~ ASCII_HEX_DIGIT{,4} }
18+
ipv6 = ${ (ASCII_HEX_DIGIT{,4}? ~ ":"){1, 7} ~ ASCII_HEX_DIGIT{,4} }
1819

1920
encoded_char = ${ "%" ~ ASCII_DIGIT{2} }
2021

faup/src/lib.rs

Lines changed: 117 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
//! use faup_rs::Url;
6666
//!
6767
//! let url = Url::parse("http://[::1]").unwrap();
68-
//! assert!(matches!(url.host().unwrap(), faup_rs::Host::Ip(ip) if ip.is_loopback()));
68+
//! assert!(matches!(url.host().unwrap(), faup_rs::Host::IpV6(ip, _) if ip.is_loopback()));
6969
//!```
7070
//!
7171
//! ### User Info (UTF-8 Support)
@@ -120,7 +120,10 @@ use std::{
120120
str::FromStr,
121121
};
122122

123-
use pest::{Parser, iterators::Pair};
123+
use pest::{
124+
Parser,
125+
iterators::{Pair, Pairs},
126+
};
124127
use thiserror::Error;
125128

126129
mod parser;
@@ -164,13 +167,6 @@ pub enum Error {
164167
#[error("invalid host")]
165168
InvalidHost,
166169

167-
/// Generic error for other parsing issues.
168-
///
169-
/// This error is used for various parsing problems that don't fit
170-
/// the more specific error categories.
171-
#[error("{0}")]
172-
Other(String),
173-
174170
/// Parsing error from the underlying pest parser.
175171
///
176172
/// This error occurs when the URL string doesn't conform to
@@ -179,12 +175,6 @@ pub enum Error {
179175
Parse(#[from] Box<pest::error::Error<Rule>>),
180176
}
181177

182-
impl Error {
183-
fn other<S: AsRef<str>>(s: S) -> Self {
184-
Error::Other(s.as_ref().to_string())
185-
}
186-
}
187-
188178
/// Classification of a suffix (Top-Level Domain) based on its origin and validity.
189179
///
190180
/// This enum categorizes suffixes according to their source and compliance status.
@@ -720,15 +710,21 @@ impl<'url> Hostname<'url> {
720710
pub enum Host<'url> {
721711
/// A hostname (domain name).
722712
Hostname(Hostname<'url>),
723-
/// An IP address (either IPv4 or IPv6).
724-
Ip(IpAddr),
713+
/// An IPv4 address
714+
IpV4(IpAddr),
715+
/// An IPv6 address
716+
IpV6(Ipv6Addr, Option<Cow<'url, str>>),
725717
}
726718

727719
impl fmt::Display for Host<'_> {
728720
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729721
match self {
730722
Host::Hostname(hostname) => write!(f, "{}", hostname.full_name()),
731-
Host::Ip(ip) => write!(f, "{ip}"),
723+
Host::IpV4(ip) => write!(f, "{ip}"),
724+
Host::IpV6(ip, ozid) => match ozid {
725+
Some(zid) => write!(f, "{ip}%{zid}"),
726+
None => write!(f, "{ip}"),
727+
},
732728
}
733729
}
734730
}
@@ -737,37 +733,49 @@ impl<'host> Host<'host> {
737733
fn into_owned<'owned>(self) -> Host<'owned> {
738734
match self {
739735
Host::Hostname(h) => Host::Hostname(h.into_owned()),
740-
Host::Ip(ip) => Host::Ip(ip),
736+
Host::IpV4(ip) => Host::IpV4(ip),
737+
Host::IpV6(ip, zone_id) => {
738+
Host::IpV6(ip, zone_id.map(|zid| Cow::Owned(zid.into_owned())))
739+
}
741740
}
742741
}
743742

744743
#[inline(always)]
745-
fn from_pair(host_pair: Pair<'host, Rule>) -> Result<Self, Error> {
746-
match host_pair.as_rule() {
747-
Rule::hostname => {
748-
if let Ok(ipv4) =
749-
UrlParser::parse(Rule::ipv4, host_pair.as_str()).map(|p| p.as_str())
750-
{
751-
Ok(Ipv4Addr::from_str(ipv4)
752-
.map(IpAddr::from)
753-
.map(Host::Ip)
754-
.map_err(|_| Error::InvalidIPv4)?)
755-
} else {
756-
Ok(Host::Hostname(Hostname::from_str(host_pair.as_str())))
744+
fn from_pairs(mut pairs: Pairs<'host, Rule>) -> Result<Self, Error> {
745+
let mut host = None;
746+
747+
while let Some(pair) = pairs.next() {
748+
match pair.as_rule() {
749+
Rule::hostname => {
750+
if let Ok(ipv4) =
751+
UrlParser::parse(Rule::ipv4, pair.as_str()).map(|p| p.as_str())
752+
{
753+
host = Some(
754+
Ipv4Addr::from_str(ipv4)
755+
.map(IpAddr::from)
756+
.map(Host::IpV4)
757+
.map_err(|_| Error::InvalidIPv4)?,
758+
);
759+
} else {
760+
host = Some(Host::Hostname(Hostname::from_str(pair.as_str())));
761+
}
757762
}
758-
}
759763

760-
Rule::ipv6 => Ok(Ipv6Addr::from_str(
761-
host_pair.as_str().trim_matches(|c| c == '[' || c == ']'),
762-
)
763-
.map(IpAddr::from)
764-
.map(Host::Ip)
765-
.map_err(|_| Error::InvalidIPv6)?),
766-
_ => Err(Error::other(format!(
767-
"unexpected parsing rule: {:?}",
768-
host_pair.as_rule()
769-
))),
764+
Rule::ipv6 => {
765+
let ip_addr =
766+
Ipv6Addr::from_str(pair.as_str().trim_matches(|c| c == '[' || c == ']'))
767+
.map_err(|_| Error::InvalidIPv6)?;
768+
769+
let zone_id = pairs.next().map(|p| Cow::Borrowed(p.as_str()));
770+
771+
host = Some(Host::IpV6(ip_addr, zone_id));
772+
}
773+
774+
_ => {}
775+
}
770776
}
777+
778+
Ok(host.unwrap())
771779
}
772780

773781
/// Parses a string into a `Host` enum.
@@ -790,15 +798,15 @@ impl<'host> Host<'host> {
790798
///
791799
/// // Parse an IPv4 address
792800
/// let host = Host::parse("127.0.0.1").unwrap();
793-
/// assert!(matches!(host, Host::Ip(std::net::IpAddr::V4(_))));
801+
/// assert!(host.is_ipv4());
794802
///
795803
/// // Parse an IPv6 address
796804
/// let host = Host::parse("::1").unwrap();
797-
/// assert!(matches!(host, Host::Ip(std::net::IpAddr::V6(_))));
805+
/// assert!(host.is_ipv6());
798806
///
799807
/// // Parse a hostname
800808
/// let host = Host::parse("example.com").unwrap();
801-
/// assert!(matches!(host, Host::Hostname(_)));
809+
/// assert!(host.is_hostname());
802810
///
803811
/// // Parse a hostname with a subdomain
804812
/// let host = Host::parse("sub.example.com").unwrap();
@@ -814,12 +822,8 @@ impl<'host> Host<'host> {
814822
/// ```
815823
#[inline]
816824
pub fn parse(host: &'host str) -> Result<Self, Error> {
817-
Self::from_pair(
818-
UrlParser::parse(Rule::checked_host, host)
819-
.map_err(|_| Error::InvalidHost)?
820-
.next()
821-
// this should not panic as parser guarantee some pair exist
822-
.expect("expecting host pair"),
825+
Self::from_pairs(
826+
UrlParser::parse(Rule::checked_host, host).map_err(|_| Error::InvalidHost)?,
823827
)
824828
}
825829

@@ -834,6 +838,56 @@ impl<'host> Host<'host> {
834838
_ => None,
835839
}
836840
}
841+
/// Returns `true` if this host is a hostname (domain name).
842+
///
843+
/// # Examples
844+
///
845+
/// ```
846+
/// use faup_rs::Host;
847+
///
848+
/// let host = Host::parse("example.com").unwrap();
849+
/// assert!(host.is_hostname());
850+
///
851+
/// let host = Host::parse("192.168.1.1").unwrap();
852+
/// assert!(!host.is_hostname());
853+
/// ```
854+
pub fn is_hostname(&self) -> bool {
855+
matches!(self, Host::Hostname(_))
856+
}
857+
858+
/// Returns `true` if this host is an IPv4 address.
859+
///
860+
/// # Examples
861+
///
862+
/// ```
863+
/// use faup_rs::Host;
864+
///
865+
/// let host = Host::parse("192.168.1.1").unwrap();
866+
/// assert!(host.is_ipv4());
867+
///
868+
/// let host = Host::parse("example.com").unwrap();
869+
/// assert!(!host.is_ipv4());
870+
/// ```
871+
pub fn is_ipv4(&self) -> bool {
872+
matches!(self, Host::IpV4(_))
873+
}
874+
875+
/// Returns `true` if this host is an IPv6 address.
876+
///
877+
/// # Examples
878+
///
879+
/// ```
880+
/// use faup_rs::Host;
881+
///
882+
/// let host = Host::parse("::1").unwrap();
883+
/// assert!(host.is_ipv6());
884+
///
885+
/// let host = Host::parse("example.com").unwrap();
886+
/// assert!(!host.is_ipv6());
887+
/// ```
888+
pub fn is_ipv6(&self) -> bool {
889+
matches!(self, Host::IpV6(_, _))
890+
}
837891
}
838892

839893
/// Represents user information (username and password) in a URL.
@@ -1012,11 +1066,7 @@ impl<'url> Url<'url> {
10121066
scheme = Some(Cow::Borrowed(p.as_str()));
10131067
}
10141068
Rule::userinfo => userinfo = Some(UserInfo::from_pair(p)),
1015-
Rule::host => {
1016-
// cannot panic guarantee by parser
1017-
let host_pair = p.into_inner().next().unwrap();
1018-
host = Some(Host::from_pair(host_pair)?)
1019-
}
1069+
Rule::host => host = Some(Host::from_pairs(p.into_inner())?),
10201070
Rule::port => {
10211071
port = Some(u16::from_str(p.as_str()).map_err(|_| Error::InvalidPort)?)
10221072
}
@@ -1641,14 +1691,14 @@ mod tests {
16411691
// IPv4
16421692
let url = Url::parse("http://127.0.0.1").unwrap();
16431693
match url.host().unwrap() {
1644-
Host::Ip(IpAddr::V4(ip)) => assert_eq!(ip, &Ipv4Addr::new(127, 0, 0, 1)),
1694+
Host::IpV4(IpAddr::V4(ip)) => assert_eq!(ip, &Ipv4Addr::new(127, 0, 0, 1)),
16451695
_ => panic!("Expected IPv4 address"),
16461696
}
16471697

16481698
// IPv6
16491699
let url = Url::parse("http://[::1]").unwrap();
16501700
match url.host().unwrap() {
1651-
Host::Ip(IpAddr::V6(ip)) => assert_eq!(ip, &Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
1701+
Host::IpV6(ip, _) => assert_eq!(ip, &Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
16521702
_ => panic!("Expected IPv6 address"),
16531703
}
16541704

@@ -1703,11 +1753,11 @@ mod tests {
17031753
fn test_host_from_str() {
17041754
// Valid IPv4
17051755
let host = Host::parse("127.0.0.1").unwrap();
1706-
assert!(matches!(host, Host::Ip(std::net::IpAddr::V4(_))));
1756+
assert!(matches!(host, Host::IpV4(std::net::IpAddr::V4(_))));
17071757

17081758
// Valid IPv6
17091759
let host = Host::parse("::1").unwrap();
1710-
assert!(matches!(host, Host::Ip(std::net::IpAddr::V6(_))));
1760+
assert!(matches!(host, Host::IpV6(_, _)));
17111761

17121762
let host = Host::parse("[::1]");
17131763
assert!(matches!(host, Err(Error::InvalidHost)));
@@ -1832,4 +1882,11 @@ mod tests {
18321882
assert!(u.host().is_none());
18331883
assert_eq!(u.path(), Some("/tmp/thank you @claudex.txt"));
18341884
}
1885+
1886+
#[test]
1887+
fn test_url_zone_id() {
1888+
Url::parse("imap://user:password;crazy@[ff00::1234%hello]:1234/path?a=b&c=d#fragment")
1889+
.inspect_err(|e| println!("{e}"))
1890+
.unwrap();
1891+
}
18351892
}

0 commit comments

Comments
 (0)