Skip to main content

use_socket/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use std::net::IpAddr;
5
6/// Stores a normalized host-and-port endpoint.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct SocketEndpoint {
9    /// Endpoint host component.
10    pub host: String,
11    /// Endpoint port component.
12    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
91/// Parses a socket endpoint from common host-and-port forms.
92pub fn parse_socket_endpoint(input: &str) -> Option<SocketEndpoint> {
93    let (host, port) = split_raw_endpoint(input)?;
94
95    Some(SocketEndpoint { host, port })
96}
97
98/// Formats a socket endpoint, adding IPv6 brackets when needed.
99pub 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
106/// Returns `true` when the endpoint uses an IPv4 host.
107pub 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
112/// Returns `true` when the endpoint uses an IPv6 host.
113pub 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
118/// Returns `true` when the input is a valid host-and-port endpoint.
119pub fn is_host_port(input: &str) -> bool {
120    parse_socket_endpoint(input).is_some()
121}
122
123/// Splits a valid endpoint into host and port components.
124pub fn split_host_port(input: &str) -> Option<(String, u16)> {
125    split_raw_endpoint(input)
126}