use crate::{Domain, NameClass};
impl Domain {
pub const MAX_LABEL_LEN: usize = 63;
pub(crate) fn classify_label(label: &[u8]) -> NameClass {
if (label.is_empty() || label.len() > Self::MAX_LABEL_LEN)
|| (label[0] == b'-' || label[label.len() - 1] == b'-')
{
NameClass::Invalid
} else {
let mut class: NameClass = NameClass::Lowercase;
for c in label {
if c.is_ascii_uppercase() {
class = NameClass::MixedCase;
} else if !(c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'-') {
return NameClass::Invalid;
}
}
class
}
}
#[must_use]
pub fn is_valid_label(label: &[u8], ignore_case: bool) -> bool {
match Self::classify_label(label) {
NameClass::Lowercase => true,
NameClass::MixedCase => ignore_case,
NameClass::Invalid => false,
}
}
#[must_use]
pub fn is_valid_label_str(label: &str, ignore_case: bool) -> bool {
Self::is_valid_label(label.as_bytes(), ignore_case)
}
}
impl Domain {
pub const MAX_NAME_LEN: usize = 253;
pub(crate) fn classify_name(name: &[u8]) -> NameClass {
if name.is_empty() || name.len() > Self::MAX_NAME_LEN {
NameClass::Invalid
} else {
let mut class: NameClass = NameClass::Lowercase;
for label in name.split(|c| *c == b'.') {
match Self::classify_label(label) {
NameClass::Invalid => return NameClass::Invalid,
NameClass::MixedCase => class = NameClass::MixedCase,
NameClass::Lowercase => {}
}
}
class
}
}
#[must_use]
pub fn is_valid_name(name: &[u8], ignore_case: bool) -> bool {
match Self::classify_name(name) {
NameClass::Lowercase => true,
NameClass::MixedCase => ignore_case,
NameClass::Invalid => false,
}
}
#[must_use]
pub fn is_valid_name_str(name: &str, ignore_case: bool) -> bool {
Self::is_valid_name(name.as_bytes(), ignore_case)
}
}
#[cfg(test)]
mod tests {
use crate::Domain;
#[test]
fn is_valid_label() {
let test_cases: &[(&str, bool, bool)] = &[
("", false, false),
("09", true, true),
("az", true, true),
("AZ", false, true),
("-a", false, false),
("a-", false, false),
("a--a", true, true),
("a-a", true, true),
("a-a-a", true, true),
];
for (label, expected, expected_ignore_case) in test_cases {
let result: bool = Domain::is_valid_label_str(label, false);
assert_eq!(result, *expected, "label={}", label);
let result: bool = Domain::is_valid_label_str(label, true);
assert_eq!(result, *expected_ignore_case, "label={}", label);
}
}
#[test]
fn label_length_boundaries() {
let test_cases: &[(usize, bool)] = &[(Domain::MAX_LABEL_LEN, true), (Domain::MAX_LABEL_LEN + 1, false)];
for (len, expected) in test_cases {
let label: String = "a".repeat(*len);
let result: bool = Domain::is_valid_label_str(label.as_str(), false);
assert_eq!(result, *expected, "len={}", len);
}
}
#[test]
fn is_valid_name() {
let test_cases: &[(&str, bool, bool)] = &[
("", false, false),
("09", true, true),
("az", true, true),
("AZ", false, true),
(".a", false, false),
("a.", false, false),
("a..a", false, false),
("a.a", true, true),
("a.a.a", true, true),
("a-a.a-a.a-a", true, true),
];
for (name, expected, expected_ignore_case) in test_cases {
let result: bool = Domain::is_valid_name_str(name, false);
assert_eq!(result, *expected, "name={}", name);
let result: bool = Domain::is_valid_name_str(name, true);
assert_eq!(result, *expected_ignore_case, "name={}", name);
}
}
#[test]
fn name_length_boundaries() {
let test_cases: &[(usize, usize, bool)] =
&[(61, Domain::MAX_NAME_LEN, true), (62, Domain::MAX_NAME_LEN + 1, false)];
for (tail_len, expected_len, expected) in test_cases {
let label: String = "a".repeat(Domain::MAX_LABEL_LEN);
let name: String = format!("{}.{}.{}.{}", label, label, label, "a".repeat(*tail_len));
assert_eq!(name.len(), *expected_len, "tail_len={}", tail_len);
let result: bool = Domain::is_valid_name_str(name.as_str(), false);
assert_eq!(result, *expected, "tail_len={}", tail_len);
}
}
}