]>
Commit | Line | Data |
---|---|---|
5362c05c AL |
1 | use std::{ |
2 | fmt, | |
3 | net::{AddrParseError, IpAddr}, | |
4 | num::ParseIntError, | |
5 | str::FromStr, | |
6 | }; | |
7 | ||
8 | use serde::Deserialize; | |
9 | ||
10 | /// Possible errors that might occur when parsing CIDR addresses. | |
11 | #[derive(Debug)] | |
12 | pub enum CidrAddressParseError { | |
13 | /// No delimiter for separating address and mask was found. | |
14 | NoDelimiter, | |
15 | /// The IP address part could not be parsed. | |
16 | InvalidAddr(AddrParseError), | |
17 | /// The mask could not be parsed. | |
18 | InvalidMask(Option<ParseIntError>), | |
19 | } | |
20 | ||
21 | /// An IP address (IPv4 or IPv6), including network mask. | |
22 | /// | |
23 | /// See the [`IpAddr`] type for more information how IP addresses are handled. | |
24 | /// The mask is appropriately enforced to be `0 <= mask <= 32` for IPv4 or | |
25 | /// `0 <= mask <= 128` for IPv6 addresses. | |
26 | /// | |
27 | /// # Examples | |
28 | /// ``` | |
29 | /// use std::net::{Ipv4Addr, Ipv6Addr}; | |
eeb06272 | 30 | /// use proxmox_installer_common::utils::CidrAddress; |
5362c05c AL |
31 | /// let ipv4 = CidrAddress::new(Ipv4Addr::new(192, 168, 0, 1), 24).unwrap(); |
32 | /// let ipv6 = CidrAddress::new(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0xc0a8, 1), 32).unwrap(); | |
33 | /// | |
34 | /// assert_eq!(ipv4.to_string(), "192.168.0.1/24"); | |
35 | /// assert_eq!(ipv6.to_string(), "2001:db8::c0a8:1/32"); | |
36 | /// ``` | |
37 | #[derive(Clone, Debug, PartialEq)] | |
38 | pub struct CidrAddress { | |
39 | addr: IpAddr, | |
40 | mask: usize, | |
41 | } | |
42 | ||
43 | impl CidrAddress { | |
44 | /// Constructs a new CIDR address. | |
45 | /// | |
46 | /// It fails if the mask is invalid for the given IP address. | |
47 | pub fn new<T: Into<IpAddr>>(addr: T, mask: usize) -> Result<Self, CidrAddressParseError> { | |
48 | let addr = addr.into(); | |
49 | ||
50 | if mask > mask_limit(&addr) { | |
51 | Err(CidrAddressParseError::InvalidMask(None)) | |
52 | } else { | |
53 | Ok(Self { addr, mask }) | |
54 | } | |
55 | } | |
56 | ||
57 | /// Returns only the IP address part of the address. | |
58 | pub fn addr(&self) -> IpAddr { | |
59 | self.addr | |
60 | } | |
61 | ||
62 | /// Returns `true` if this address is an IPv4 address, `false` otherwise. | |
63 | pub fn is_ipv4(&self) -> bool { | |
64 | self.addr.is_ipv4() | |
65 | } | |
66 | ||
67 | /// Returns `true` if this address is an IPv6 address, `false` otherwise. | |
68 | pub fn is_ipv6(&self) -> bool { | |
69 | self.addr.is_ipv6() | |
70 | } | |
71 | ||
72 | /// Returns only the mask part of the address. | |
73 | pub fn mask(&self) -> usize { | |
74 | self.mask | |
75 | } | |
76 | } | |
77 | ||
78 | impl FromStr for CidrAddress { | |
79 | type Err = CidrAddressParseError; | |
80 | ||
81 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
82 | let (addr, mask) = s | |
83 | .split_once('/') | |
84 | .ok_or(CidrAddressParseError::NoDelimiter)?; | |
85 | ||
86 | let addr = addr.parse().map_err(CidrAddressParseError::InvalidAddr)?; | |
87 | ||
88 | let mask = mask | |
89 | .parse() | |
90 | .map_err(|err| CidrAddressParseError::InvalidMask(Some(err)))?; | |
91 | ||
92 | if mask > mask_limit(&addr) { | |
93 | Err(CidrAddressParseError::InvalidMask(None)) | |
94 | } else { | |
95 | Ok(Self { addr, mask }) | |
96 | } | |
97 | } | |
98 | } | |
99 | ||
100 | impl fmt::Display for CidrAddress { | |
101 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
102 | write!(f, "{}/{}", self.addr, self.mask) | |
103 | } | |
104 | } | |
105 | ||
106 | fn mask_limit(addr: &IpAddr) -> usize { | |
107 | if addr.is_ipv4() { | |
108 | 32 | |
109 | } else { | |
110 | 128 | |
111 | } | |
112 | } | |
113 | ||
114 | /// Possible errors that might occur when parsing FQDNs. | |
115 | #[derive(Debug, Eq, PartialEq)] | |
116 | pub enum FqdnParseError { | |
117 | MissingHostname, | |
118 | NumericHostname, | |
119 | InvalidPart(String), | |
120 | } | |
121 | ||
122 | impl fmt::Display for FqdnParseError { | |
123 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
124 | use FqdnParseError::*; | |
125 | match self { | |
126 | MissingHostname => write!(f, "missing hostname part"), | |
127 | NumericHostname => write!(f, "hostname cannot be purely numeric"), | |
128 | InvalidPart(part) => write!( | |
129 | f, | |
130 | "FQDN must only consist of alphanumeric characters and dashes. Invalid part: '{part}'", | |
131 | ), | |
132 | } | |
133 | } | |
134 | } | |
135 | ||
136 | #[derive(Clone, Debug, Eq, PartialEq)] | |
137 | pub struct Fqdn { | |
138 | parts: Vec<String>, | |
139 | } | |
140 | ||
141 | impl Fqdn { | |
142 | pub fn from(fqdn: &str) -> Result<Self, FqdnParseError> { | |
143 | let parts = fqdn | |
144 | .split('.') | |
145 | .map(ToOwned::to_owned) | |
146 | .collect::<Vec<String>>(); | |
147 | ||
148 | for part in &parts { | |
149 | if !Self::validate_single(part) { | |
150 | return Err(FqdnParseError::InvalidPart(part.clone())); | |
151 | } | |
152 | } | |
153 | ||
154 | if parts.len() < 2 { | |
155 | Err(FqdnParseError::MissingHostname) | |
156 | } else if parts[0].chars().all(|c| c.is_ascii_digit()) { | |
157 | // Not allowed/supported on Debian systems. | |
158 | Err(FqdnParseError::NumericHostname) | |
159 | } else { | |
160 | Ok(Self { parts }) | |
161 | } | |
162 | } | |
163 | ||
164 | pub fn host(&self) -> Option<&str> { | |
165 | self.has_host().then_some(&self.parts[0]) | |
166 | } | |
167 | ||
168 | pub fn domain(&self) -> String { | |
169 | let parts = if self.has_host() { | |
170 | &self.parts[1..] | |
171 | } else { | |
172 | &self.parts | |
173 | }; | |
174 | ||
175 | parts.join(".") | |
176 | } | |
177 | ||
178 | /// Checks whether the FQDN has a hostname associated with it, i.e. is has more than 1 part. | |
179 | fn has_host(&self) -> bool { | |
180 | self.parts.len() > 1 | |
181 | } | |
182 | ||
183 | fn validate_single(s: &String) -> bool { | |
184 | !s.is_empty() | |
185 | // First character must be alphanumeric | |
186 | && s.chars() | |
187 | .next() | |
188 | .map(|c| c.is_ascii_alphanumeric()) | |
189 | .unwrap_or_default() | |
190 | // .. last character as well, | |
191 | && s.chars() | |
192 | .last() | |
193 | .map(|c| c.is_ascii_alphanumeric()) | |
194 | .unwrap_or_default() | |
195 | // and anything between must be alphanumeric or - | |
196 | && s.chars() | |
197 | .skip(1) | |
198 | .take(s.len().saturating_sub(2)) | |
199 | .all(|c| c.is_ascii_alphanumeric() || c == '-') | |
200 | } | |
201 | } | |
202 | ||
203 | impl FromStr for Fqdn { | |
204 | type Err = FqdnParseError; | |
205 | ||
206 | fn from_str(value: &str) -> Result<Self, Self::Err> { | |
207 | Self::from(value) | |
208 | } | |
209 | } | |
210 | ||
211 | impl fmt::Display for Fqdn { | |
212 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
213 | write!(f, "{}", self.parts.join(".")) | |
214 | } | |
215 | } | |
216 | ||
217 | impl<'de> Deserialize<'de> for Fqdn { | |
218 | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | |
219 | where | |
220 | D: serde::Deserializer<'de>, | |
221 | { | |
222 | let s: String = Deserialize::deserialize(deserializer)?; | |
223 | s.parse() | |
224 | .map_err(|_| serde::de::Error::custom("invalid FQDN")) | |
225 | } | |
226 | } | |
227 | ||
228 | #[cfg(test)] | |
229 | mod tests { | |
230 | use super::*; | |
231 | ||
232 | #[test] | |
233 | fn fqdn_construct() { | |
234 | use FqdnParseError::*; | |
235 | assert!(Fqdn::from("foo.example.com").is_ok()); | |
236 | assert!(Fqdn::from("foo-bar.com").is_ok()); | |
237 | assert!(Fqdn::from("a-b.com").is_ok()); | |
238 | ||
239 | assert_eq!(Fqdn::from("foo"), Err(MissingHostname)); | |
240 | ||
241 | assert_eq!(Fqdn::from("-foo.com"), Err(InvalidPart("-foo".to_owned()))); | |
242 | assert_eq!(Fqdn::from("foo-.com"), Err(InvalidPart("foo-".to_owned()))); | |
243 | assert_eq!(Fqdn::from("foo.com-"), Err(InvalidPart("com-".to_owned()))); | |
244 | assert_eq!(Fqdn::from("-o-.com"), Err(InvalidPart("-o-".to_owned()))); | |
245 | ||
246 | assert_eq!(Fqdn::from("123.com"), Err(NumericHostname)); | |
247 | assert!(Fqdn::from("foo123.com").is_ok()); | |
248 | assert!(Fqdn::from("123foo.com").is_ok()); | |
249 | } | |
250 | ||
251 | #[test] | |
252 | fn fqdn_parts() { | |
253 | let fqdn = Fqdn::from("pve.example.com").unwrap(); | |
254 | assert_eq!(fqdn.host().unwrap(), "pve"); | |
255 | assert_eq!(fqdn.domain(), "example.com"); | |
256 | assert_eq!( | |
257 | fqdn.parts, | |
258 | &["pve".to_owned(), "example".to_owned(), "com".to_owned()] | |
259 | ); | |
260 | } | |
261 | ||
262 | #[test] | |
263 | fn fqdn_display() { | |
264 | assert_eq!( | |
265 | Fqdn::from("foo.example.com").unwrap().to_string(), | |
266 | "foo.example.com" | |
267 | ); | |
268 | } | |
269 | } |