]> git.proxmox.com Git - pve-installer.git/blame - proxmox-installer-common/src/utils.rs
sys: wait a second after sending TERM signal before going for KILL
[pve-installer.git] / proxmox-installer-common / src / utils.rs
CommitLineData
5362c05c
AL
1use std::{
2 fmt,
3 net::{AddrParseError, IpAddr},
4 num::ParseIntError,
5 str::FromStr,
6};
7
8use serde::Deserialize;
9
10/// Possible errors that might occur when parsing CIDR addresses.
11#[derive(Debug)]
12pub 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)]
38pub struct CidrAddress {
39 addr: IpAddr,
40 mask: usize,
41}
42
43impl 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
78impl 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
100impl fmt::Display for CidrAddress {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "{}/{}", self.addr, self.mask)
103 }
104}
105
106fn 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)]
116pub enum FqdnParseError {
117 MissingHostname,
118 NumericHostname,
119 InvalidPart(String),
120}
121
122impl 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)]
137pub struct Fqdn {
138 parts: Vec<String>,
139}
140
141impl 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
203impl 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
211impl fmt::Display for Fqdn {
212 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
213 write!(f, "{}", self.parts.join("."))
214 }
215}
216
217impl<'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)]
229mod 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}