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