Skip to main content

network_device/
interface.rs

1//! Neutral interface and IP-prefix value types.
2
3use std::fmt;
4use std::net::IpAddr;
5use std::str::FromStr;
6
7/// A validated, vendor-neutral interface identifier.
8///
9/// The value is preserved as supplied because interface naming conventions
10/// vary across vendors and platforms.
11#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
12pub struct InterfaceName(String);
13
14impl InterfaceName {
15    /// Creates an interface name, rejecting empty and control-character values.
16    pub fn new(value: impl Into<String>) -> Result<Self, InterfaceNameError> {
17        let value = value.into();
18        if value.trim().is_empty() {
19            return Err(InterfaceNameError::Empty);
20        }
21        if value.chars().any(char::is_control) {
22            return Err(InterfaceNameError::ControlCharacter);
23        }
24        Ok(Self(value))
25    }
26
27    /// Returns the original interface identifier.
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31}
32
33impl FromStr for InterfaceName {
34    type Err = InterfaceNameError;
35
36    fn from_str(value: &str) -> Result<Self, Self::Err> {
37        Self::new(value)
38    }
39}
40
41impl TryFrom<&str> for InterfaceName {
42    type Error = InterfaceNameError;
43
44    fn try_from(value: &str) -> Result<Self, Self::Error> {
45        Self::new(value)
46    }
47}
48
49impl fmt::Display for InterfaceName {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        formatter.write_str(self.as_str())
52    }
53}
54
55/// Validation errors for [`InterfaceName`].
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum InterfaceNameError {
58    /// The name is empty or contains only whitespace.
59    Empty,
60    /// The name contains a control character.
61    ControlCharacter,
62}
63
64impl fmt::Display for InterfaceNameError {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        formatter.write_str(match self {
67            Self::Empty => "interface name must not be empty or whitespace-only",
68            Self::ControlCharacter => "interface name must not contain control characters",
69        })
70    }
71}
72
73impl std::error::Error for InterfaceNameError {}
74
75/// A validated IP address and network prefix length.
76#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
77pub struct IpPrefix {
78    /// Address portion of the prefix.
79    address: IpAddr,
80    /// Prefix length, from zero through 32 or 128 according to address family.
81    prefix_length: u8,
82}
83
84impl IpPrefix {
85    /// Creates a prefix after validating its length against the address family.
86    pub fn new(address: IpAddr, prefix_length: u8) -> Result<Self, IpPrefixError> {
87        let maximum = match address {
88            IpAddr::V4(_) => 32,
89            IpAddr::V6(_) => 128,
90        };
91        if prefix_length > maximum {
92            return Err(IpPrefixError {
93                prefix_length,
94                maximum,
95            });
96        }
97        Ok(Self {
98            address,
99            prefix_length,
100        })
101    }
102
103    /// Returns the address portion of this prefix.
104    pub const fn address(self) -> IpAddr {
105        self.address
106    }
107
108    /// Returns the prefix length.
109    pub const fn prefix_length(self) -> u8 {
110        self.prefix_length
111    }
112}
113
114impl FromStr for IpPrefix {
115    type Err = IpPrefixParseError;
116
117    fn from_str(value: &str) -> Result<Self, Self::Err> {
118        let (address, length) = value
119            .rsplit_once('/')
120            .ok_or(IpPrefixParseError::MissingPrefixLength)?;
121        let address = address
122            .parse::<IpAddr>()
123            .map_err(|_| IpPrefixParseError::InvalidAddress)?;
124        let length = length
125            .parse::<u8>()
126            .map_err(|_| IpPrefixParseError::InvalidPrefixLength)?;
127        Self::new(address, length).map_err(IpPrefixParseError::OutOfRange)
128    }
129}
130
131impl fmt::Display for IpPrefix {
132    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(formatter, "{}/{}", self.address, self.prefix_length)
134    }
135}
136
137/// Error returned when an IP prefix length exceeds its address family's limit.
138#[derive(Clone, Copy, Debug, Eq, PartialEq)]
139pub struct IpPrefixError {
140    /// Supplied prefix length.
141    pub prefix_length: u8,
142    /// Maximum permitted prefix length.
143    pub maximum: u8,
144}
145
146impl fmt::Display for IpPrefixError {
147    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148        write!(
149            formatter,
150            "prefix length {} exceeds maximum {}",
151            self.prefix_length, self.maximum
152        )
153    }
154}
155
156impl std::error::Error for IpPrefixError {}
157
158/// Parsing errors for [`IpPrefix`].
159#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160pub enum IpPrefixParseError {
161    /// The input did not contain a slash and prefix length.
162    MissingPrefixLength,
163    /// The address portion is not a valid IPv4 or IPv6 address.
164    InvalidAddress,
165    /// The prefix portion is not an unsigned integer.
166    InvalidPrefixLength,
167    /// The parsed prefix length is invalid for the address family.
168    OutOfRange(IpPrefixError),
169}
170
171impl fmt::Display for IpPrefixParseError {
172    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            Self::MissingPrefixLength => formatter.write_str("IP prefix must include /length"),
175            Self::InvalidAddress => formatter.write_str("invalid IP address in prefix"),
176            Self::InvalidPrefixLength => formatter.write_str("invalid IP prefix length"),
177            Self::OutOfRange(error) => error.fmt(formatter),
178        }
179    }
180}
181
182impl std::error::Error for IpPrefixParseError {}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn interface_names_reject_empty_and_control_characters() {
190        assert_eq!(InterfaceName::new("  "), Err(InterfaceNameError::Empty));
191        assert_eq!(
192            InterfaceName::new("eth\n0"),
193            Err(InterfaceNameError::ControlCharacter)
194        );
195        assert_eq!(
196            InterfaceName::new("Ethernet 1/1").unwrap().as_str(),
197            "Ethernet 1/1"
198        );
199    }
200
201    #[test]
202    fn prefixes_validate_both_address_families() {
203        assert_eq!(
204            "192.0.2.1/32".parse::<IpPrefix>().unwrap().prefix_length(),
205            32
206        );
207        assert_eq!(
208            "2001:db8::1/128"
209                .parse::<IpPrefix>()
210                .unwrap()
211                .prefix_length(),
212            128
213        );
214        assert_eq!(
215            "192.0.2.1/33".parse::<IpPrefix>(),
216            Err(IpPrefixParseError::OutOfRange(IpPrefixError {
217                prefix_length: 33,
218                maximum: 32,
219            }))
220        );
221    }
222}