use crate::error::DomainError;
pub(crate) const MAX_NAME_LEN: usize = 253;
pub(crate) const MAX_LABEL_LEN: usize = 63;
pub(crate) const ACE_PREFIX: &str = "xn--";
pub(crate) fn to_ascii(input: &str) -> std::result::Result<String, DomainError> {
if input.is_ascii() {
return Ok(input.to_ascii_lowercase());
}
idna::domain_to_ascii(input).map_err(|error| DomainError::InvalidIdn {
label: input.to_string(),
detail: error.to_string(),
})
}
pub(crate) fn to_unicode_lossy(input: &str) -> String {
if !input.to_ascii_lowercase().contains(ACE_PREFIX) {
return input.to_ascii_lowercase();
}
let (unicode, result) = idna::domain_to_unicode(input);
if result.is_ok() {
unicode
} else {
input.to_ascii_lowercase()
}
}
pub(crate) fn both_forms(input: &str) -> std::result::Result<(String, String), DomainError> {
let ascii = to_ascii(input)?;
let unicode = to_unicode_lossy(&ascii);
Ok((ascii, unicode))
}
pub(crate) fn validate_label(label: &str) -> std::result::Result<(), DomainError> {
let invalid = || DomainError::InvalidLabel {
label: label.to_string(),
};
if label.is_empty() || label.len() > MAX_LABEL_LEN {
return Err(invalid());
}
if label.starts_with('-') || label.ends_with('-') {
return Err(invalid());
}
if !label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
{
return Err(invalid());
}
Ok(())
}
pub(crate) fn is_ip_literal(input: &str) -> bool {
input.parse::<std::net::IpAddr>().is_ok()
|| (input.starts_with('[') && input.ends_with(']'))
|| input.matches(':').count() >= 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_input_is_only_lowercased() {
assert_eq!(to_ascii("Example.COM").unwrap(), "example.com");
}
#[test]
fn unicode_round_trips() {
let ascii = to_ascii("münchen.de").unwrap();
assert_eq!(ascii, "xn--mnchen-3ya.de");
assert_eq!(to_unicode_lossy(&ascii), "münchen.de");
}
#[test]
fn undecodable_ace_is_passed_through() {
assert_eq!(to_unicode_lossy("xn--"), "xn--");
assert_eq!(to_unicode_lossy("XN--ZZZZZZZZZ"), "xn--zzzzzzzzz");
}
#[test]
fn label_rules() {
assert!(validate_label("example").is_ok());
assert!(validate_label("xn--mnchen-3ya").is_ok());
assert!(validate_label("a-b-c").is_ok());
assert!(validate_label("").is_err());
assert!(validate_label("-lead").is_err());
assert!(validate_label("trail-").is_err());
assert!(validate_label("under_score").is_err());
assert!(validate_label(&"a".repeat(64)).is_err());
assert!(validate_label(&"a".repeat(63)).is_ok());
}
#[test]
fn detects_ip_literals() {
assert!(is_ip_literal("192.0.2.1"));
assert!(is_ip_literal("::1"));
assert!(is_ip_literal("2001:db8::1"));
assert!(is_ip_literal("[2001:db8::1]"));
assert!(!is_ip_literal("example.com"));
assert!(!is_ip_literal("example.com:oops"));
}
}