Skip to content

Commit c891f10

Browse files
committed
add: Host::from_pair
1 parent 94b48f6 commit c891f10

File tree

3 files changed

+140
-35
lines changed

3 files changed

+140
-35
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

faup/src/grammar.pest

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ username = ${ (!(":" | "@" | WHITE_SPACE) ~ ANY)+ }
55
password = ${ (!("@" | WHITE_SPACE) ~ ANY)+ }
66
userinfo = ${ username ~ (":" ~ password)? ~ "@" }
77

8-
host = ${ ipv4 | ipv6 | hostname }
9-
domain_part = ${ (!(":" | "?" | "/" | "#" | "." | WHITE_SPACE) ~ ANY)+ }
10-
tld = ${ (!(":" | "?" | "/" | "#" | WHITE_SPACE) ~ ANY)+ }
11-
hostname = ${ (((domain_part ~ ".")+ ~ tld) | domain_part) }
12-
13-
ipv4 = ${ (ASCII_DIGIT{1, 3} ~ "."){3} ~ ASCII_DIGIT{1, 3} }
14-
ipv6 = ${ "[" ~ (ASCII_HEX_DIGIT{,4} ~ ":"){2, 7} ~ ASCII_HEX_DIGIT{,4} ~ "]" }
8+
// this rule is used to check if the hostname is actually
9+
// a valid ipv4 address as hostname rule matches any ipv4
10+
ipv4 = ${ SOI ~ (ASCII_DIGIT{1, 3} ~ "."){3} ~ ASCII_DIGIT{1, 3} ~ EOI }
11+
host = ${ "[" ~ ipv6 ~ "]" | hostname }
12+
checked_host = _{ SOI ~ (ipv6 | hostname) ~ EOI }
13+
domain_part = ${ (!(":" | "?" | "/" | "#" | "." | WHITE_SPACE) ~ ANY)+ }
14+
tld = ${ (!(":" | "?" | "/" | "#" | "." | WHITE_SPACE) ~ ANY)+ }
15+
hostname = ${ (((domain_part ~ ".")+ ~ tld) | domain_part) }
16+
17+
ipv6 = ${ (ASCII_HEX_DIGIT{,4} ~ ":"){2, 7} ~ ASCII_HEX_DIGIT{,4} }
1518

1619
encoded_char = ${ "%" ~ ASCII_DIGIT{2} }
1720

faup/src/lib.rs

Lines changed: 128 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,20 @@ pub enum Error {
133133
InvalidIPv4,
134134
#[error("invalid ipv6 address")]
135135
InvalidIPv6,
136+
#[error("invalid host")]
137+
InvalidHost,
138+
#[error("{0}")]
139+
Other(String),
136140
#[error("parser error: {0}")]
137141
Parse(#[from] Box<pest::error::Error<Rule>>),
138142
}
139143

144+
impl Error {
145+
fn other<S: AsRef<str>>(s: S) -> Self {
146+
Error::Other(s.as_ref().to_string())
147+
}
148+
}
149+
140150
#[derive(Parser)]
141151
#[grammar = "grammar.pest"]
142152
pub(crate) struct UrlParser;
@@ -395,14 +405,96 @@ impl fmt::Display for Host<'_> {
395405
}
396406
}
397407

398-
impl<'url> Host<'url> {
408+
impl<'host> Host<'host> {
399409
fn into_owned<'owned>(self) -> Host<'owned> {
400410
match self {
401411
Host::Hostname(h) => Host::Hostname(h.into_owned()),
402412
Host::Ip(ip) => Host::Ip(ip),
403413
}
404414
}
405415

416+
#[inline(always)]
417+
fn from_pair(host_pair: Pair<'host, Rule>) -> Result<Self, Error> {
418+
match host_pair.as_rule() {
419+
Rule::hostname => {
420+
if let Ok(ipv4) =
421+
UrlParser::parse(Rule::ipv4, host_pair.as_str()).map(|p| p.as_str())
422+
{
423+
Ok(Ipv4Addr::from_str(ipv4)
424+
.map(IpAddr::from)
425+
.map(Host::Ip)
426+
.map_err(|_| Error::InvalidIPv4)?)
427+
} else {
428+
Ok(Host::Hostname(Hostname::from_str(host_pair.as_str())))
429+
}
430+
}
431+
432+
Rule::ipv6 => Ok(Ipv6Addr::from_str(
433+
host_pair.as_str().trim_matches(|c| c == '[' || c == ']'),
434+
)
435+
.map(IpAddr::from)
436+
.map(Host::Ip)
437+
.map_err(|_| Error::InvalidIPv6)?),
438+
_ => Err(Error::other(format!(
439+
"unexpected parsing rule: {:?}",
440+
host_pair.as_rule()
441+
))),
442+
}
443+
}
444+
445+
/// Parses a string into a `Host` enum.
446+
///
447+
/// This function expects the input string to be a URL host, which can be either
448+
/// an IPv4 address, an IPv6 address, or a hostname.
449+
///
450+
/// # Arguments
451+
///
452+
/// * `host` - A string slice that holds the host to parse (e.g., `"example.com"`, `"127.0.0.1"`, `"::1"`).
453+
///
454+
/// # Returns
455+
///
456+
/// * `Result<Host, Error>` - A [`Host`] enum if parsing is successful, or an [`Error`] if parsing fails.
457+
///
458+
/// # Examples
459+
///
460+
/// ```
461+
/// use faup_rs::Host;
462+
///
463+
/// // Parse an IPv4 address
464+
/// let host = Host::parse("127.0.0.1").unwrap();
465+
/// assert!(matches!(host, Host::Ip(std::net::IpAddr::V4(_))));
466+
///
467+
/// // Parse an IPv6 address
468+
/// let host = Host::parse("::1").unwrap();
469+
/// assert!(matches!(host, Host::Ip(std::net::IpAddr::V6(_))));
470+
///
471+
/// // Parse a hostname
472+
/// let host = Host::parse("example.com").unwrap();
473+
/// assert!(matches!(host, Host::Hostname(_)));
474+
///
475+
/// // Parse a hostname with a subdomain
476+
/// let host = Host::parse("sub.example.com").unwrap();
477+
/// assert!(matches!(host, Host::Hostname(_)));
478+
///
479+
/// // Parse a hostname with a custom TLD
480+
/// let host = Host::parse("example.b32.i2p").unwrap();
481+
/// assert!(matches!(host, Host::Hostname(_)));
482+
///
483+
/// // Attempt to parse an invalid host
484+
/// let result = Host::parse("invalid..host");
485+
/// assert!(matches!(result, Err(faup_rs::Error::InvalidHost)));
486+
/// ```
487+
#[inline]
488+
pub fn parse(host: &'host str) -> Result<Self, Error> {
489+
Self::from_pair(
490+
UrlParser::parse(Rule::checked_host, host)
491+
.map_err(|_| Error::InvalidHost)?
492+
.next()
493+
// this should not panic as parser guarantee some pair exist
494+
.expect("expecting host pair"),
495+
)
496+
}
497+
406498
/// Returns the hostname component if this is a `Host::Hostname` variant.
407499
///
408500
/// # Returns
@@ -595,31 +687,7 @@ impl<'url> Url<'url> {
595687
Rule::host => {
596688
// cannot panic guarantee by parser
597689
let host_pair = p.into_inner().next().unwrap();
598-
match host_pair.as_rule() {
599-
Rule::hostname => {
600-
host = Some(Host::Hostname(Hostname::from_str(host_pair.as_str())))
601-
}
602-
Rule::ipv4 => {
603-
host = Some(
604-
Ipv4Addr::from_str(host_pair.as_str())
605-
.map(IpAddr::from)
606-
.map(Host::Ip)
607-
.map_err(|_| Error::InvalidIPv4)?,
608-
);
609-
}
610-
611-
Rule::ipv6 => {
612-
host = Some(
613-
Ipv6Addr::from_str(
614-
host_pair.as_str().trim_matches(|c| c == '[' || c == ']'),
615-
)
616-
.map(IpAddr::from)
617-
.map(Host::Ip)
618-
.map_err(|_| Error::InvalidIPv6)?,
619-
);
620-
}
621-
_ => {}
622-
}
690+
host = Some(Host::from_pair(host_pair)?)
623691
}
624692
Rule::port => {
625693
port = Some(u16::from_str(p.as_str()).map_err(|_| Error::InvalidPort)?)
@@ -1251,4 +1319,38 @@ mod tests {
12511319
let url = Url::parse("https://example.com/../../..some/directory/traversal/../").unwrap();
12521320
assert_eq!(url.path(), Some("/../../..some/directory/traversal/../"));
12531321
}
1322+
1323+
#[test]
1324+
fn test_host_from_str() {
1325+
// Valid IPv4
1326+
let host = Host::parse("127.0.0.1").unwrap();
1327+
assert!(matches!(host, Host::Ip(std::net::IpAddr::V4(_))));
1328+
1329+
// Valid IPv6
1330+
let host = Host::parse("::1").unwrap();
1331+
assert!(matches!(host, Host::Ip(std::net::IpAddr::V6(_))));
1332+
1333+
let host = Host::parse("[::1]");
1334+
assert!(matches!(host, Err(Error::InvalidHost)));
1335+
1336+
// Invalid IPv6
1337+
let result = Host::parse("::::");
1338+
assert!(matches!(result, Err(Error::InvalidIPv6)));
1339+
1340+
// Valid hostname
1341+
let host = Host::parse("example.com").unwrap();
1342+
assert!(matches!(host, Host::Hostname(_)));
1343+
1344+
// Hostname with subdomain
1345+
let host = Host::parse("sub.example.com").unwrap();
1346+
assert!(matches!(host, Host::Hostname(_)));
1347+
1348+
// Hostname with custom TLD
1349+
let host = Host::parse("example.b32.i2p").unwrap();
1350+
assert!(matches!(host, Host::Hostname(_)));
1351+
1352+
// Invalid hostname (placeholder logic)
1353+
let result = Host::parse("example..com");
1354+
assert!(matches!(result, Err(Error::InvalidHost)));
1355+
}
12541356
}

0 commit comments

Comments
 (0)