use super::error::HostnameError;
pub fn is_valid_hostname(hostname: &str) -> Result<(), HostnameError> {
if hostname.is_empty() {
return Err(HostnameError::Empty);
}
let name = hostname.strip_suffix('.').unwrap_or(hostname);
if name.len() > 253 {
return Err(HostnameError::TooLong);
}
let mut offset = 0;
for label in name.split('.') {
check_label(label, offset)?;
offset += label.len() + 1;
}
if is_number(name.rsplit('.').next().unwrap_or(name)) {
return Err(HostnameError::NumericLastLabel);
}
Ok(())
}
fn check_label(label: &str, offset: usize) -> Result<(), HostnameError> {
if label.is_empty() {
return Err(HostnameError::EmptyLabel);
}
if label.len() > 63 {
return Err(HostnameError::LabelTooLong);
}
if let Some((index, found)) = label
.char_indices()
.find(|&(_, c)| !(c.is_ascii_alphanumeric() || c == '-'))
{
return Err(HostnameError::InvalidChar {
index: offset + index,
found,
});
}
if label.starts_with('-') || label.ends_with('-') {
return Err(HostnameError::HyphenEdge);
}
Ok(())
}
fn is_number(label: &str) -> bool {
label.bytes().all(|b| b.is_ascii_digit())
|| label
.strip_prefix("0x")
.or_else(|| label.strip_prefix("0X"))
.is_some_and(|rest| rest.bytes().all(|b| b.is_ascii_hexdigit()))
}
#[cfg(test)]
#[path = "is_valid_hostname.test.rs"]
mod tests;
#[cfg(test)]
#[path = "is_valid_hostname.spec.rs"]
mod spec;