]>
git.proxmox.com Git - pve-installer.git/blob - proxmox-installer-common/src/utils.rs
3 net
::{AddrParseError, IpAddr}
,
8 use serde
::Deserialize
;
10 /// Possible errors that might occur when parsing CIDR addresses.
12 pub enum CidrAddressParseError
{
13 /// No delimiter for separating address and mask was found.
15 /// The IP address part could not be parsed.
16 InvalidAddr(AddrParseError
),
17 /// The mask could not be parsed.
18 InvalidMask(Option
<ParseIntError
>),
21 /// An IP address (IPv4 or IPv6), including network mask.
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.
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();
34 /// assert_eq!(ipv4.to_string(), "192.168.0.1/24");
35 /// assert_eq!(ipv6.to_string(), "2001:db8::c0a8:1/32");
37 #[derive(Clone, Debug, PartialEq)]
38 pub struct CidrAddress
{
44 /// Constructs a new CIDR address.
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();
50 if mask
> mask_limit(&addr
) {
51 Err(CidrAddressParseError
::InvalidMask(None
))
53 Ok(Self { addr, mask }
)
57 /// Returns only the IP address part of the address.
58 pub fn addr(&self) -> IpAddr
{
62 /// Returns `true` if this address is an IPv4 address, `false` otherwise.
63 pub fn is_ipv4(&self) -> bool
{
67 /// Returns `true` if this address is an IPv6 address, `false` otherwise.
68 pub fn is_ipv6(&self) -> bool
{
72 /// Returns only the mask part of the address.
73 pub fn mask(&self) -> usize {
78 impl FromStr
for CidrAddress
{
79 type Err
= CidrAddressParseError
;
81 fn from_str(s
: &str) -> Result
<Self, Self::Err
> {
84 .ok_or(CidrAddressParseError
::NoDelimiter
)?
;
86 let addr
= addr
.parse().map_err(CidrAddressParseError
::InvalidAddr
)?
;
90 .map_err(|err
| CidrAddressParseError
::InvalidMask(Some(err
)))?
;
92 if mask
> mask_limit(&addr
) {
93 Err(CidrAddressParseError
::InvalidMask(None
))
95 Ok(Self { addr, mask }
)
100 impl fmt
::Display
for CidrAddress
{
101 fn fmt(&self, f
: &mut fmt
::Formatter
<'_
>) -> fmt
::Result
{
102 write
!(f
, "{}/{}", self.addr
, self.mask
)
106 impl<'de
> Deserialize
<'de
> for CidrAddress
{
107 fn deserialize
<D
>(deserializer
: D
) -> Result
<Self, D
::Error
>
109 D
: serde
::Deserializer
<'de
>,
111 let s
: String
= Deserialize
::deserialize(deserializer
)?
;
113 .map_err(|_
| serde
::de
::Error
::custom("invalid CIDR"))
117 fn mask_limit(addr
: &IpAddr
) -> usize {
125 /// Possible errors that might occur when parsing FQDNs.
126 #[derive(Debug, Eq, PartialEq)]
127 pub enum FqdnParseError
{
134 impl fmt
::Display
for FqdnParseError
{
135 fn fmt(&self, f
: &mut fmt
::Formatter
<'_
>) -> fmt
::Result
{
136 use FqdnParseError
::*;
138 MissingHostname
=> write
!(f
, "missing hostname part"),
139 NumericHostname
=> write
!(f
, "hostname cannot be purely numeric"),
140 InvalidPart(part
) => write
!(
142 "FQDN must only consist of alphanumeric characters and dashes. Invalid part: '{part}'",
144 TooLong(len
) => write
!(f
, "FQDN too long: {len} > {}", Fqdn
::MAX_LENGTH
),
149 /// A type for safely representing fully-qualified domain names (FQDNs).
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
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
163 /// - It enforces the restriction as per Bugzilla #1054, in that
164 /// purely numeric hostnames are not allowed - against RFC1123 sec. 2.1.
166 /// Some terminology:
167 /// - "label" - a single part of a FQDN, e.g. <label>.<label>.<tld>
168 #[derive(Clone, Debug, Eq)]
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;
179 pub fn from(fqdn
: &str) -> Result
<Self, FqdnParseError
> {
180 if fqdn
.len() > Self::MAX_LENGTH
{
181 return Err(FqdnParseError
::TooLong(fqdn
.len()));
186 .map(ToOwned
::to_owned
)
187 .collect
::<Vec
<String
>>();
190 if !Self::validate_single(part
) {
191 return Err(FqdnParseError
::InvalidPart(part
.clone()));
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
)
206 pub fn host(&self) -> Option
<&str> {
207 self.has_host().then_some(&self.parts
[0])
210 pub fn domain(&self) -> String
{
211 let parts
= if self.has_host() {
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
{
225 fn validate_single(s
: &str) -> bool
{
227 && s
.len() <= Self::MAX_LABEL_LENGTH
228 // First character must be alphanumeric
231 .map(|c
| c
.is_ascii_alphanumeric())
233 // .. last character as well,
236 .map(|c
| c
.is_ascii_alphanumeric())
238 // and anything between must be alphanumeric or -
241 .take(s
.len().saturating_sub(2))
242 .all(|c
| c
.is_ascii_alphanumeric() || c
== '
-'
)
246 impl FromStr
for Fqdn
{
247 type Err
= FqdnParseError
;
249 fn from_str(value
: &str) -> Result
<Self, Self::Err
> {
254 impl fmt
::Display
for Fqdn
{
255 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
256 write
!(f
, "{}", self.parts
.join("."))
260 impl<'de
> Deserialize
<'de
> for Fqdn
{
261 fn deserialize
<D
>(deserializer
: D
) -> Result
<Self, D
::Error
>
263 D
: serde
::Deserializer
<'de
>,
265 let s
: String
= Deserialize
::deserialize(deserializer
)?
;
267 .map_err(|_
| serde
::de
::Error
::custom("invalid FQDN"))
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() {
281 .zip(other
.parts
.iter())
282 .all(|(a
, b
)| a
.to_lowercase() == b
.to_lowercase())
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());
297 assert_eq
!(Fqdn
::from("foo"), Err(MissingHostname
));
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())));
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());
309 assert
!(Fqdn
::from(&format
!("{}.com", "a".repeat(63))).is_ok());
311 Fqdn
::from(&format
!("{}.com", "a".repeat(250))),
315 Fqdn
::from(&format
!("{}.com", "a".repeat(64))),
316 Err(InvalidPart("a".repeat(64))),
319 // https://bugzilla.proxmox.com/show_bug.cgi?id=5230
321 Fqdn
::from("123@foo.com"),
322 Err(InvalidPart("123@foo".to_owned()))
328 let fqdn
= Fqdn
::from("pve.example.com").unwrap();
329 assert_eq
!(fqdn
.host().unwrap(), "pve");
330 assert_eq
!(fqdn
.domain(), "example.com");
333 &["pve".to_owned(), "example".to_owned(), "com".to_owned()]
340 Fqdn
::from("foo.example.com").unwrap().to_string(),
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"));
351 Fqdn
::from("subdomain.ExAmPle.Com"),
352 Fqdn
::from("example.com")
354 assert_ne
!(Fqdn
::from("foo.com"), Fqdn
::from("bar.com"));
355 assert_ne
!(Fqdn
::from("example.com"), Fqdn
::from("example.net"));