]> git.proxmox.com Git - pve-installer.git/blob - proxmox-installer-common/src/utils.rs
remaining clippy fixes
[pve-installer.git] / proxmox-installer-common / src / utils.rs
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};
30 /// use proxmox_installer_common::utils::CidrAddress;
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 impl<'de> Deserialize<'de> for CidrAddress {
107 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108 where
109 D: serde::Deserializer<'de>,
110 {
111 let s: String = Deserialize::deserialize(deserializer)?;
112 s.parse()
113 .map_err(|_| serde::de::Error::custom("invalid CIDR"))
114 }
115 }
116
117 fn mask_limit(addr: &IpAddr) -> usize {
118 if addr.is_ipv4() {
119 32
120 } else {
121 128
122 }
123 }
124
125 /// Possible errors that might occur when parsing FQDNs.
126 #[derive(Debug, Eq, PartialEq)]
127 pub enum FqdnParseError {
128 MissingHostname,
129 NumericHostname,
130 InvalidPart(String),
131 TooLong(usize),
132 }
133
134 impl fmt::Display for FqdnParseError {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 use FqdnParseError::*;
137 match self {
138 MissingHostname => write!(f, "missing hostname part"),
139 NumericHostname => write!(f, "hostname cannot be purely numeric"),
140 InvalidPart(part) => write!(
141 f,
142 "FQDN must only consist of alphanumeric characters and dashes. Invalid part: '{part}'",
143 ),
144 TooLong(len) => write!(f, "FQDN too long: {len} > {}", Fqdn::MAX_LENGTH),
145 }
146 }
147 }
148
149 /// A type for safely representing fully-qualified domain names (FQDNs).
150 ///
151 /// It considers following RFCs:
152 /// https://www.ietf.org/rfc/rfc952.txt (sec. "ASSUMPTIONS", 1.)
153 /// https://www.ietf.org/rfc/rfc1035.txt (sec. 2.3. "Conventions")
154 /// https://www.ietf.org/rfc/rfc1123.txt (sec. 2.1. "Host Names and Numbers")
155 /// https://www.ietf.org/rfc/rfc3492.txt
156 /// https://www.ietf.org/rfc/rfc4343.txt
157 ///
158 /// .. and applies some restriction given by Debian, e.g. 253 instead of 255
159 /// maximum total length and maximum 63 characters per label.
160 /// https://manpages.debian.org/stable/manpages/hostname.7.en.html
161 ///
162 /// Additionally:
163 /// - It enforces the restriction as per Bugzilla #1054, in that
164 /// purely numeric hostnames are not allowed - against RFC1123 sec. 2.1.
165 ///
166 /// Some terminology:
167 /// - "label" - a single part of a FQDN, e.g. <label>.<label>.<tld>
168 #[derive(Clone, Debug, Eq)]
169 pub struct Fqdn {
170 parts: Vec<String>,
171 }
172
173 impl Fqdn {
174 /// Maximum length of a single label of the FQDN
175 const MAX_LABEL_LENGTH: usize = 63;
176 /// Maximum total length of the FQDN
177 const MAX_LENGTH: usize = 253;
178
179 pub fn from(fqdn: &str) -> Result<Self, FqdnParseError> {
180 if fqdn.len() > Self::MAX_LENGTH {
181 return Err(FqdnParseError::TooLong(fqdn.len()));
182 }
183
184 let parts = fqdn
185 .split('.')
186 .map(ToOwned::to_owned)
187 .collect::<Vec<String>>();
188
189 for part in &parts {
190 if !Self::validate_single(part) {
191 return Err(FqdnParseError::InvalidPart(part.clone()));
192 }
193 }
194
195 if parts.len() < 2 {
196 Err(FqdnParseError::MissingHostname)
197 } else if parts[0].chars().all(|c| c.is_ascii_digit()) {
198 // Do not allow a purely numeric hostname, see:
199 // https://bugzilla.proxmox.com/show_bug.cgi?id=1054
200 Err(FqdnParseError::NumericHostname)
201 } else {
202 Ok(Self { parts })
203 }
204 }
205
206 pub fn host(&self) -> Option<&str> {
207 self.has_host().then_some(&self.parts[0])
208 }
209
210 pub fn domain(&self) -> String {
211 let parts = if self.has_host() {
212 &self.parts[1..]
213 } else {
214 &self.parts
215 };
216
217 parts.join(".")
218 }
219
220 /// Checks whether the FQDN has a hostname associated with it, i.e. is has more than 1 part.
221 fn has_host(&self) -> bool {
222 self.parts.len() > 1
223 }
224
225 fn validate_single(s: &str) -> bool {
226 !s.is_empty()
227 && s.len() <= Self::MAX_LABEL_LENGTH
228 // First character must be alphanumeric
229 && s.chars()
230 .next()
231 .map(|c| c.is_ascii_alphanumeric())
232 .unwrap_or_default()
233 // .. last character as well,
234 && s.chars()
235 .last()
236 .map(|c| c.is_ascii_alphanumeric())
237 .unwrap_or_default()
238 // and anything between must be alphanumeric or -
239 && s.chars()
240 .skip(1)
241 .take(s.len().saturating_sub(2))
242 .all(|c| c.is_ascii_alphanumeric() || c == '-')
243 }
244 }
245
246 impl FromStr for Fqdn {
247 type Err = FqdnParseError;
248
249 fn from_str(value: &str) -> Result<Self, Self::Err> {
250 Self::from(value)
251 }
252 }
253
254 impl fmt::Display for Fqdn {
255 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
256 write!(f, "{}", self.parts.join("."))
257 }
258 }
259
260 impl<'de> Deserialize<'de> for Fqdn {
261 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
262 where
263 D: serde::Deserializer<'de>,
264 {
265 let s: String = Deserialize::deserialize(deserializer)?;
266 s.parse()
267 .map_err(|_| serde::de::Error::custom("invalid FQDN"))
268 }
269 }
270
271 impl PartialEq for Fqdn {
272 // Case-insensitive comparison, as per RFC 952 "ASSUMPTIONS", RFC 1035 sec. 2.3.3. "Character
273 // Case" and RFC 4343 as a whole
274 fn eq(&self, other: &Self) -> bool {
275 if self.parts.len() != other.parts.len() {
276 return false;
277 }
278
279 self.parts
280 .iter()
281 .zip(other.parts.iter())
282 .all(|(a, b)| a.to_lowercase() == b.to_lowercase())
283 }
284 }
285
286 #[cfg(test)]
287 mod tests {
288 use super::*;
289
290 #[test]
291 fn fqdn_construct() {
292 use FqdnParseError::*;
293 assert!(Fqdn::from("foo.example.com").is_ok());
294 assert!(Fqdn::from("foo-bar.com").is_ok());
295 assert!(Fqdn::from("a-b.com").is_ok());
296
297 assert_eq!(Fqdn::from("foo"), Err(MissingHostname));
298
299 assert_eq!(Fqdn::from("-foo.com"), Err(InvalidPart("-foo".to_owned())));
300 assert_eq!(Fqdn::from("foo-.com"), Err(InvalidPart("foo-".to_owned())));
301 assert_eq!(Fqdn::from("foo.com-"), Err(InvalidPart("com-".to_owned())));
302 assert_eq!(Fqdn::from("-o-.com"), Err(InvalidPart("-o-".to_owned())));
303
304 // https://bugzilla.proxmox.com/show_bug.cgi?id=1054
305 assert_eq!(Fqdn::from("123.com"), Err(NumericHostname));
306 assert!(Fqdn::from("foo123.com").is_ok());
307 assert!(Fqdn::from("123foo.com").is_ok());
308
309 assert!(Fqdn::from(&format!("{}.com", "a".repeat(63))).is_ok());
310 assert_eq!(
311 Fqdn::from(&format!("{}.com", "a".repeat(250))),
312 Err(TooLong(254)),
313 );
314 assert_eq!(
315 Fqdn::from(&format!("{}.com", "a".repeat(64))),
316 Err(InvalidPart("a".repeat(64))),
317 );
318
319 // https://bugzilla.proxmox.com/show_bug.cgi?id=5230
320 assert_eq!(
321 Fqdn::from("123@foo.com"),
322 Err(InvalidPart("123@foo".to_owned()))
323 );
324 }
325
326 #[test]
327 fn fqdn_parts() {
328 let fqdn = Fqdn::from("pve.example.com").unwrap();
329 assert_eq!(fqdn.host().unwrap(), "pve");
330 assert_eq!(fqdn.domain(), "example.com");
331 assert_eq!(
332 fqdn.parts,
333 &["pve".to_owned(), "example".to_owned(), "com".to_owned()]
334 );
335 }
336
337 #[test]
338 fn fqdn_display() {
339 assert_eq!(
340 Fqdn::from("foo.example.com").unwrap().to_string(),
341 "foo.example.com"
342 );
343 }
344
345 #[test]
346 fn fqdn_compare() {
347 assert_eq!(Fqdn::from("example.com"), Fqdn::from("example.com"));
348 assert_eq!(Fqdn::from("example.com"), Fqdn::from("ExAmPle.Com"));
349 assert_eq!(Fqdn::from("ExAmPle.Com"), Fqdn::from("example.com"));
350 assert_ne!(
351 Fqdn::from("subdomain.ExAmPle.Com"),
352 Fqdn::from("example.com")
353 );
354 assert_ne!(Fqdn::from("foo.com"), Fqdn::from("bar.com"));
355 assert_ne!(Fqdn::from("example.com"), Fqdn::from("example.net"));
356 }
357 }