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+ } ;
124127use thiserror:: Error ;
125128
126129mod 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> {
720710pub 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
727719impl 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