Skip to main content

sip_uri/
host.rs

1use std::fmt;
2use std::net::{Ipv4Addr, Ipv6Addr};
3
4use crate::parse;
5
6/// Host component of a SIP URI.
7///
8/// IPv6 addresses are stored without brackets; [`fmt::Display`] adds brackets
9/// when formatting in URI context via [`Host::fmt_uri`].
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum Host {
13    /// IPv4 address.
14    IPv4(Ipv4Addr),
15    /// IPv6 address (stored without brackets).
16    IPv6(Ipv6Addr),
17    /// DNS hostname.
18    Hostname(String),
19}
20
21impl Host {
22    /// Parse a host from a URI string fragment.
23    ///
24    /// Handles `[IPv6]`, dotted-decimal IPv4, and DNS hostnames.
25    /// Returns the parsed host and the number of bytes consumed.
26    pub(crate) fn parse_from_uri(s: &str) -> Result<(Self, usize), String> {
27        if s.is_empty() {
28            return Err("empty host".into());
29        }
30
31        if s.starts_with('[') {
32            // IPv6reference = "[" IPv6address "]"
33            let end = s
34                .find(']')
35                .ok_or_else(|| "unclosed IPv6 bracket".to_string())?;
36            let addr_str = &s[1..end];
37            let addr: Ipv6Addr = addr_str
38                .parse()
39                .map_err(|e| format!("invalid IPv6 address: {e}"))?;
40            Ok((Host::IPv6(addr), end + 1))
41        } else {
42            // Find end of host: terminated by `:`, `;`, `?`, `#`, `>`, or end of string
43            // Must skip percent-encoded sequences when scanning
44            let end = find_host_end(s);
45            let host_str = &s[..end];
46
47            if host_str.is_empty() {
48                return Err("empty host".into());
49            }
50
51            // Percent-decode the host for parsing (unreserved chars decoded)
52            let decoded = if host_str.contains('%') {
53                parse::percent_decode(host_str, parse::is_unreserved)
54            } else {
55                host_str.to_string()
56            };
57
58            // Try IPv4 first — must be all digits and dots
59            if decoded
60                .bytes()
61                .all(|b| b.is_ascii_digit() || b == b'.')
62            {
63                if let Ok(addr) = decoded.parse::<Ipv4Addr>() {
64                    return Ok((Host::IPv4(addr), end));
65                }
66            }
67
68            // Validate hostname characters: alphanum, '-', '.'
69            if !decoded
70                .bytes()
71                .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.'))
72            {
73                return Err(format!("invalid hostname character in '{decoded}'"));
74            }
75
76            Ok((Host::Hostname(decoded.to_ascii_lowercase()), end))
77        }
78    }
79}
80
81/// Find the end of a hostname in a URI string, handling percent-encoded sequences.
82fn find_host_end(s: &str) -> usize {
83    let bytes = s.as_bytes();
84    let mut i = 0;
85    while i < bytes.len() {
86        match bytes[i] {
87            b':' | b';' | b'?' | b'#' | b'>' => return i,
88            b'%' if i + 2 < bytes.len() => {
89                // Skip percent-encoded sequence
90                i += 3;
91            }
92            _ => i += 1,
93        }
94    }
95    i
96}
97
98impl Host {
99    /// Format the host for use inside a URI (brackets around IPv6).
100    pub fn fmt_uri(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Host::IPv4(addr) => write!(f, "{addr}"),
103            Host::IPv6(addr) => write!(f, "[{addr}]"),
104            Host::Hostname(name) => write!(f, "{name}"),
105        }
106    }
107}
108
109impl fmt::Display for Host {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        self.fmt_uri(f)
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn parse_ipv4() {
121        let (host, consumed) = Host::parse_from_uri("172.21.55.55:5060").unwrap();
122        assert_eq!(host, Host::IPv4(Ipv4Addr::new(172, 21, 55, 55)));
123        assert_eq!(consumed, 12);
124    }
125
126    #[test]
127    fn parse_ipv6() {
128        let (host, consumed) = Host::parse_from_uri("[::1]:56001").unwrap();
129        assert_eq!(host, Host::IPv6(Ipv6Addr::LOCALHOST));
130        assert_eq!(consumed, 5);
131    }
132
133    #[test]
134    fn parse_ipv6_full() {
135        let (host, consumed) = Host::parse_from_uri("[2001:db8::1]:5061").unwrap();
136        assert_eq!(
137            host,
138            Host::IPv6(
139                "2001:db8::1"
140                    .parse::<Ipv6Addr>()
141                    .unwrap()
142            )
143        );
144        assert_eq!(consumed, 13);
145    }
146
147    #[test]
148    fn parse_hostname() {
149        let (host, consumed) = Host::parse_from_uri("example.com;transport=tcp").unwrap();
150        assert_eq!(host, Host::Hostname("example.com".into()));
151        assert_eq!(consumed, 11);
152    }
153
154    #[test]
155    fn hostname_lowercased() {
156        let (host, _) = Host::parse_from_uri("MY.DOMAIN").unwrap();
157        assert_eq!(host, Host::Hostname("my.domain".into()));
158    }
159
160    #[test]
161    fn display_ipv6_has_brackets() {
162        let host = Host::IPv6(Ipv6Addr::LOCALHOST);
163        assert_eq!(host.to_string(), "[::1]");
164    }
165
166    #[test]
167    fn empty_host_fails() {
168        assert!(Host::parse_from_uri("").is_err());
169        assert!(Host::parse_from_uri(":5060").is_err());
170    }
171}