1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use std::net::IpAddr;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct SocketEndpoint {
9 pub host: String,
11 pub port: u16,
13}
14
15fn parse_port(input: &str) -> Option<u16> {
16 if input.is_empty() {
17 None
18 } else {
19 input.parse::<u16>().ok()
20 }
21}
22
23fn is_valid_label(label: &str) -> bool {
24 !label.is_empty()
25 && label.len() <= 63
26 && !label.starts_with('-')
27 && !label.ends_with('-')
28 && label
29 .chars()
30 .all(|character| character.is_ascii_alphanumeric() || character == '-')
31}
32
33fn normalize_host(input: &str) -> Option<String> {
34 if input.eq_ignore_ascii_case("localhost") {
35 return Some(String::from("localhost"));
36 }
37
38 if let Ok(address) = input.parse::<IpAddr>() {
39 return Some(address.to_string());
40 }
41
42 let normalized = input.trim_end_matches('.').to_ascii_lowercase();
43
44 if normalized.is_empty()
45 || normalized.len() > 253
46 || normalized.contains(':')
47 || normalized.contains('/')
48 || normalized.chars().any(char::is_whitespace)
49 {
50 return None;
51 }
52
53 if normalized.split('.').all(is_valid_label) {
54 Some(normalized)
55 } else {
56 None
57 }
58}
59
60fn split_raw_endpoint(input: &str) -> Option<(String, u16)> {
61 let trimmed = input.trim();
62
63 if trimmed.is_empty() {
64 return None;
65 }
66
67 if trimmed.starts_with('[') {
68 let closing_index = trimmed.find(']')?;
69 let host = &trimmed[1..closing_index];
70 let port = trimmed.get(closing_index + 2..)?;
71
72 if !trimmed[closing_index + 1..].starts_with(':') {
73 return None;
74 }
75
76 match host.parse::<IpAddr>() {
77 Ok(IpAddr::V6(address)) => Some((address.to_string(), parse_port(port)?)),
78 _ => None,
79 }
80 } else {
81 let (host, port) = trimmed.rsplit_once(':')?;
82
83 if host.is_empty() || host.contains(':') {
84 return None;
85 }
86
87 Some((normalize_host(host)?, parse_port(port)?))
88 }
89}
90
91pub fn parse_socket_endpoint(input: &str) -> Option<SocketEndpoint> {
93 let (host, port) = split_raw_endpoint(input)?;
94
95 Some(SocketEndpoint { host, port })
96}
97
98pub fn format_socket_endpoint(endpoint: &SocketEndpoint) -> String {
100 match endpoint.host.parse::<IpAddr>() {
101 Ok(IpAddr::V6(address)) => format!("[{address}]:{}", endpoint.port),
102 _ => format!("{}:{}", endpoint.host, endpoint.port),
103 }
104}
105
106pub fn is_ipv4_socket_endpoint(input: &str) -> bool {
108 parse_socket_endpoint(input)
109 .is_some_and(|endpoint| matches!(endpoint.host.parse::<IpAddr>(), Ok(IpAddr::V4(_))))
110}
111
112pub fn is_ipv6_socket_endpoint(input: &str) -> bool {
114 parse_socket_endpoint(input)
115 .is_some_and(|endpoint| matches!(endpoint.host.parse::<IpAddr>(), Ok(IpAddr::V6(_))))
116}
117
118pub fn is_host_port(input: &str) -> bool {
120 parse_socket_endpoint(input).is_some()
121}
122
123pub fn split_host_port(input: &str) -> Option<(String, u16)> {
125 split_raw_endpoint(input)
126}