Thanks for your review - comments inline On 4/4/25 09:31, Wolfgang Bumiller wrote: > On Tue, Apr 01, 2025 at 04:52:44PM +0200, Stefan Hanreich wrote: >> Add a type for representing Linux hostnames. These are the same >> constraints as the installer enforces [1]. Lowercasing is fine as >> well, since practically everything treats hostnames case-insensitively >> as RFC 952 stipulates: >> >>> No distinction is made between upper and lower case. >> >> [1] >> https://git.proxmox.com/?p=pve-installer.git;a=blob;f=Proxmox/Sys/Net.pm;h=81cb15f0042b195461324fffeca53d732133629e;hb=HEAD#l11 >> [2] https://www.rfc-editor.org/rfc/rfc952.txt >> >> Signed-off-by: Stefan Hanreich <s.hanre...@proxmox.com> >> --- >> >> Notes: >> sending this separately because this contains the new types, that >> haven't been a part of proxmox-ve-rs before. >> >> Changes from v2: >> * improved hostname validation (thanks @Maximiliano @Christoph) >> * added additional unit tests >> >> Changes from v1: >> * added unit tests >> >> proxmox-network-types/src/hostname.rs | 103 ++++++++++++++++++++++++++ >> proxmox-network-types/src/lib.rs | 1 + >> 2 files changed, 104 insertions(+) >> create mode 100644 proxmox-network-types/src/hostname.rs >> >> diff --git a/proxmox-network-types/src/hostname.rs >> b/proxmox-network-types/src/hostname.rs >> new file mode 100644 >> index 00000000..4b2f7ede >> --- /dev/null >> +++ b/proxmox-network-types/src/hostname.rs >> @@ -0,0 +1,103 @@ >> +use std::fmt::Display; >> + >> +use serde::{Deserialize, Serialize}; >> +use thiserror::Error; >> + >> +#[derive(Error, Debug)] >> +pub enum HostnameError { >> + #[error("the hostname must be from 1 to 63 characters long")] >> + InvalidLength, >> + #[error("the hostname has an invalid format")] >> + InvalidFormat, >> +} >> + >> +/// Hostname of a Debian system > > ^ Why debian specific? Should this then not be in a different namespace > or have a different name?
As Christoph mentioned, Debian explicitly forbids numeric-only hostnames. They are technically valid, just *strongly* discouraged since depending on the implementation they are interpreted as IPs rather than hostnames. 0 or 1 or 234728934 for instance is a valid IPv4 after all (ping accepts them as input for instance). Since we're debian-based I figured I'd add it to the validation since it's very much relevant for our use-case here. I have nothing against changing the name though to DebianHostname. >> +/// >> +/// It checks for the following conditions: >> +/// * At most 63 characters long. >> +/// * It must not start or end with a hyphen. >> +/// * Must only contain ASCII alphanumeric characters as well as hyphens. >> +/// * It must not be purely numerical. >> +#[derive(Debug, Deserialize, Serialize, Clone, Eq, Hash, PartialOrd, Ord, >> PartialEq)] >> +pub struct Hostname(String); >> + >> +impl std::str::FromStr for Hostname { >> + type Err = HostnameError; >> + >> + fn from_str(hostname: &str) -> Result<Self, Self::Err> { >> + Self::new(hostname) >> + } >> +} >> + >> +impl AsRef<str> for Hostname { >> + fn as_ref(&self) -> &str { >> + &self.0 >> + } >> +} >> + >> +impl Display for Hostname { >> + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { >> + self.0.fmt(f) >> + } >> +} >> + >> +impl Hostname { >> + /// Constructs a new hostname from a string >> + /// >> + /// This function accepts characters in any case, but the resulting >> hostname will be >> + /// lowercased. >> + pub fn new(name_ref: impl AsRef<str>) -> Result<Self, HostnameError> { > > Nit: I'd recommend using a `check()` function which does not create the > `Hostname` itself, because then: > > - in `FromStr` we know we have a reference (&str) and need to clone. > - We could add a `TryFrom<&str>` wich just uses `.parse()` > - We could add a `TryFrom<String>` which avoids the clone. > > But... > >> + let name: &str = name_ref.as_ref(); >> + >> + if name.is_empty() || name.len() > 63 { >> + return Err(HostnameError::InvalidLength); >> + } >> + >> + if !(name.starts_with(|c: char| c.is_ascii_alphanumeric()) >> + && name.ends_with(|c: char| c.is_ascii_alphanumeric())) { >> + return Err(HostnameError::InvalidFormat); >> + } >> + >> + if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') { >> + return Err(HostnameError::InvalidFormat); >> + } >> + >> + if name.chars().all(|c| c.is_ascii_digit()) { >> + return Err(HostnameError::InvalidFormat); >> + } >> + >> + Ok(Self(name.to_lowercase())) > > ...do we really want/need to do this? (Note that if we really do this, > it should IMO be documented on the *type*, too, not just this method.) The idea was that, since hostnames are treated case-insensitively by basically every application, it would make no sense to create two *different* hostnames, that would be the same in practice. It's something we could maybe move to a (Partial)Eq implementation and remove the lowercasing here? > I mean, I'm not completely against it, but if we "normalize", would we > not technically also have to punycode non-ascii hostnames? > > (But at the very least it seems that punycode does case-folding... at > least a quick online-punycode conversion tool seems to convert both 'Ө' > and 'ө' to 'xn--j6a') I see your point, but I decided to stick to what the installer allows for hostnames, which is ASCII-only characters without doing any punycode conversion (correct me if I'm wrong there, but I don't remember this being the case). >> + } >> +} >> + >> +#[cfg(test)] >> +mod tests { >> + use super::*; >> + >> + #[test] >> + fn test_parse_hostname() { >> + for valid_hostname in [ >> + "debian", >> + "0host", >> + "some-host-123", >> + >> "63characterlonghostnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" >> + ] { >> + Hostname::new(valid_hostname).expect("valid hostname"); >> + } >> + >> + for invalid_hostname in [ >> + "-debian", >> + "0host-", >> + "some/host", >> + "", >> + "123", >> + >> "64characterlonghostnamexxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", >> + "🆒" >> + ] { >> + Hostname::new(invalid_hostname).expect_err("invalid hostname"); >> + } >> + >> + let uppercased_hostname = Hostname::new("UPPERCASE").expect("valid >> hostname"); >> + assert_eq!(uppercased_hostname.as_ref(), "uppercase"); >> + } >> +} >> diff --git a/proxmox-network-types/src/lib.rs >> b/proxmox-network-types/src/lib.rs >> index b952d71c..f4812146 100644 >> --- a/proxmox-network-types/src/lib.rs >> +++ b/proxmox-network-types/src/lib.rs >> @@ -1,2 +1,3 @@ >> +pub mod hostname; >> pub mod ip_address; >> pub mod mac_address; >> -- >> 2.39.5 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel