Skip to main content

use_udp/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use std::net::IpAddr;
5
6const UDP_SERVICES: &[(&str, u16)] = &[
7    ("dns", 53),
8    ("ntp", 123),
9    ("snmp", 161),
10    ("ldap", 389),
11    ("syslog", 514),
12    ("radius", 1812),
13];
14
15/// Stores a normalized UDP endpoint.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct UdpEndpoint {
18    /// Endpoint host component.
19    pub host: String,
20    /// Endpoint port component.
21    pub port: u16,
22}
23
24fn parse_port(input: &str) -> Option<u16> {
25    if input.is_empty() {
26        None
27    } else {
28        input.parse::<u16>().ok()
29    }
30}
31
32fn is_valid_label(label: &str) -> bool {
33    !label.is_empty()
34        && label.len() <= 63
35        && !label.starts_with('-')
36        && !label.ends_with('-')
37        && label
38            .chars()
39            .all(|character| character.is_ascii_alphanumeric() || character == '-')
40}
41
42fn normalize_host(input: &str) -> Option<String> {
43    if input.eq_ignore_ascii_case("localhost") {
44        return Some(String::from("localhost"));
45    }
46
47    if let Ok(address) = input.parse::<IpAddr>() {
48        return Some(address.to_string());
49    }
50
51    let normalized = input.trim_end_matches('.').to_ascii_lowercase();
52
53    if normalized.is_empty()
54        || normalized.len() > 253
55        || normalized.contains(':')
56        || normalized.contains('/')
57        || normalized.chars().any(char::is_whitespace)
58    {
59        return None;
60    }
61
62    if normalized.split('.').all(is_valid_label) {
63        Some(normalized)
64    } else {
65        None
66    }
67}
68
69fn split_raw_endpoint(input: &str) -> Option<(String, u16)> {
70    let trimmed = input.trim();
71
72    if trimmed.is_empty() {
73        return None;
74    }
75
76    if trimmed.starts_with('[') {
77        let closing_index = trimmed.find(']')?;
78        let host = &trimmed[1..closing_index];
79        let port = trimmed.get(closing_index + 2..)?;
80
81        if !trimmed[closing_index + 1..].starts_with(':') {
82            return None;
83        }
84
85        match host.parse::<IpAddr>() {
86            Ok(IpAddr::V6(address)) => Some((address.to_string(), parse_port(port)?)),
87            _ => None,
88        }
89    } else {
90        let (host, port) = trimmed.rsplit_once(':')?;
91
92        if host.is_empty() || host.contains(':') {
93            return None;
94        }
95
96        Some((normalize_host(host)?, parse_port(port)?))
97    }
98}
99
100/// Parses a UDP endpoint from a host-and-port string.
101pub fn parse_udp_endpoint(input: &str) -> Option<UdpEndpoint> {
102    let (host, port) = split_raw_endpoint(input)?;
103
104    Some(UdpEndpoint { host, port })
105}
106
107/// Formats a UDP endpoint with IPv6 bracket handling.
108pub fn format_udp_endpoint(endpoint: &UdpEndpoint) -> String {
109    match endpoint.host.parse::<IpAddr>() {
110        Ok(IpAddr::V6(address)) => format!("[{address}]:{}", endpoint.port),
111        _ => format!("{}:{}", endpoint.host, endpoint.port),
112    }
113}
114
115/// Looks up a default UDP port for a common service name.
116pub fn default_udp_port(service: &str) -> Option<u16> {
117    let normalized = service.trim().to_ascii_lowercase();
118
119    UDP_SERVICES
120        .iter()
121        .find(|(candidate_service, _)| *candidate_service == normalized)
122        .map(|(_, port)| *port)
123}
124
125/// Looks up a common UDP service name for a port.
126pub fn udp_service_name(port: u16) -> Option<&'static str> {
127    UDP_SERVICES
128        .iter()
129        .find(|(_, candidate_port)| *candidate_port == port)
130        .map(|(service, _)| *service)
131}
132
133/// Returns `true` when the port matches one of the known UDP services.
134pub fn is_common_udp_port(port: u16) -> bool {
135    udp_service_name(port).is_some()
136}