362 lines
11 KiB
Rust
362 lines
11 KiB
Rust
use crate::error::{Error, ErrorType, Result};
|
|
use crate::context::CtxString;
|
|
|
|
use std::fmt;
|
|
use std::str::FromStr;
|
|
|
|
|
|
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
|
pub struct Name {
|
|
labels: Vec<String>,
|
|
absolute: bool,
|
|
}
|
|
|
|
|
|
trait DomainCheck {
|
|
fn is_domain_char(&self) -> bool;
|
|
fn is_outer_char(&self) -> bool;
|
|
fn is_inner_char(&self) -> bool;
|
|
}
|
|
|
|
|
|
impl DomainCheck for char {
|
|
#[inline]
|
|
fn is_domain_char(&self) -> bool {
|
|
match *self {
|
|
'\x21'..='\x7e' => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn is_outer_char(&self) -> bool {
|
|
self.is_ascii_alphanumeric()
|
|
}
|
|
|
|
#[inline]
|
|
fn is_inner_char(&self) -> bool {
|
|
self.is_outer_char() || *self == '-'
|
|
}
|
|
}
|
|
|
|
|
|
impl fmt::Display for Name {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let domain = self.labels.iter()
|
|
.map(|s| s.replace('.', "\\."))
|
|
.collect::<Vec<_>>()
|
|
.join(".");
|
|
write!(f, "{}", domain)
|
|
}
|
|
}
|
|
|
|
impl FromStr for Name {
|
|
type Err = Error<ErrorType>;
|
|
|
|
fn from_str(sname: &str) -> Result<Self> {
|
|
Name::from_ctx_string(&sname.into())
|
|
}
|
|
}
|
|
|
|
impl Name {
|
|
pub fn from_ctx_string(sname: &CtxString) -> Result<Self> {
|
|
if **sname == "." {
|
|
return Ok(Name::root());
|
|
}
|
|
|
|
if **sname == "@" || **sname == "" {
|
|
return Ok(Name::new());
|
|
}
|
|
|
|
let mut labels = Vec::new();
|
|
let mut label = String::new();
|
|
let mut chars = sname.chars();
|
|
let mut absolute = false;
|
|
|
|
let mut escaped = false;
|
|
let mut escaped_digits = false;
|
|
let mut escaped_digits_str = String::new();
|
|
|
|
loop {
|
|
match chars.next() {
|
|
Some(ch) if escaped && ch.is_ascii_digit() => {
|
|
escaped_digits_str = ch.to_string();
|
|
escaped = false;
|
|
escaped_digits = true;
|
|
},
|
|
Some(ch) if escaped => {
|
|
label.push(ch.to_ascii_lowercase());
|
|
escaped = false;
|
|
},
|
|
Some(ch) if escaped_digits => {
|
|
escaped_digits_str.push(ch);
|
|
if escaped_digits_str.len() == 3 {
|
|
let ch_num: u8 = escaped_digits_str.parse().map_err(|_| Error::from(ErrorType::BadEscape))?;
|
|
label.push((ch_num as char).to_ascii_lowercase());
|
|
escaped_digits = false;
|
|
}
|
|
},
|
|
Some('.') => {
|
|
if label.len() == 0 {
|
|
return Err(ErrorType::EmptyLabel.into());
|
|
}
|
|
if label.len() > 63 {
|
|
return Err(ErrorType::LabelTooLong.into());
|
|
}
|
|
labels.push(label);
|
|
label = String::new();
|
|
},
|
|
Some('\\') => {
|
|
escaped = true;
|
|
},
|
|
Some(ch) if ch.is_ascii() => {
|
|
label.push(ch.to_ascii_lowercase())
|
|
},
|
|
Some(_) => {
|
|
return Err(ErrorType::NonAsciiChar.into());
|
|
},
|
|
None => {
|
|
labels.push(label);
|
|
break;
|
|
},
|
|
}
|
|
}
|
|
|
|
if escaped || escaped_digits {
|
|
return Err(ErrorType::BadEscape.into());
|
|
}
|
|
|
|
if labels.last() == Some(&String::new()) {
|
|
absolute = true;
|
|
}
|
|
|
|
Ok(Name {
|
|
labels,
|
|
absolute,
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
impl Name {
|
|
pub fn new() -> Self {
|
|
Name {
|
|
labels: Vec::new(),
|
|
absolute: false,
|
|
}
|
|
}
|
|
|
|
pub fn root() -> Self {
|
|
Name {
|
|
labels: vec![String::new()],
|
|
absolute: true,
|
|
}
|
|
}
|
|
|
|
pub fn prefixed_by(&self, prefix: Name) -> Self {
|
|
if prefix.absolute {
|
|
prefix.clone()
|
|
} else {
|
|
let mut labels = prefix.labels.clone();
|
|
labels.extend_from_slice(self.labels.as_slice());
|
|
Name {
|
|
labels,
|
|
absolute: self.absolute
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.labels.iter().fold(0, |acc, label| acc + label.len() + 1)
|
|
}
|
|
|
|
pub fn ensure_is_valid(&self) -> Result<&Self> {
|
|
let domain_length_check = self.len() < 256 && self.labels.len() < 128;
|
|
let labels_length_check = self.labels.iter().all(|label| label.len() < 64);
|
|
|
|
let empty_label_check = if let Some(pos) = self.labels.iter().position(|l| l == "") {
|
|
pos == self.labels.len() - 1
|
|
} else {
|
|
true
|
|
};
|
|
|
|
let is_valid = domain_length_check && labels_length_check && empty_label_check;
|
|
|
|
if is_valid {
|
|
Ok(self)
|
|
} else {
|
|
Err(ErrorType::InvalidDomain.into())
|
|
}
|
|
}
|
|
|
|
pub fn ensure_is_mailbox(&self) -> Result<&Self> {
|
|
if !self.absolute || self.labels.len() == 0 {
|
|
return Err(ErrorType::InvalidMailbox.into());
|
|
}
|
|
|
|
if self.labels.len() == 1 {
|
|
return Ok(self);
|
|
}
|
|
|
|
let username = &self.labels[0];
|
|
|
|
let valid_username_lenth = username.len() < 64;
|
|
let valid_username_chars = username.chars().all(|c| c.is_domain_char());
|
|
|
|
let labels_check = self.labels.iter().skip(1).all(|label| Self::is_hostname_label(label));
|
|
|
|
let is_mailbox = valid_username_lenth && valid_username_chars && labels_check;
|
|
|
|
if is_mailbox {
|
|
Ok(self)
|
|
} else {
|
|
Err(ErrorType::InvalidMailbox.into())
|
|
}
|
|
}
|
|
|
|
pub fn ensure_is_hostname(&self, allow_wildcard: bool) -> Result<&Self> {
|
|
if !self.absolute || self.labels.len() == 0 {
|
|
return Err(ErrorType::InvalidHostname.into());
|
|
}
|
|
|
|
if self.labels.len() == 1 {
|
|
return Ok(self);
|
|
}
|
|
|
|
let mut label_iter = if allow_wildcard && self.labels[0] == "*" {
|
|
self.labels.iter().skip(1)
|
|
} else {
|
|
self.labels.iter().skip(0)
|
|
};
|
|
|
|
if label_iter.all(|label| Self::is_hostname_label(label)) {
|
|
Ok(self)
|
|
} else {
|
|
Err(ErrorType::InvalidHostname.into())
|
|
}
|
|
}
|
|
|
|
fn is_hostname_label(label: &String) -> bool {
|
|
if label.len() > 1 {
|
|
let first = label.chars().next().unwrap().is_outer_char();
|
|
let last = label.chars().last().unwrap().is_outer_char();
|
|
let inner = label[1..(label.len() - 1)].chars().all(|c| c.is_inner_char());
|
|
label.len() < 64 && inner && first && last
|
|
} else if label.len() == 1 {
|
|
label.chars().next().unwrap().is_outer_char()
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_create_names() {
|
|
assert_eq!(
|
|
Name {
|
|
labels: vec!["admin.domains".into(), "example".into(), "com".into(), "".into()],
|
|
absolute: true,
|
|
},
|
|
"admin\\.domains.example.com.".parse().unwrap()
|
|
);
|
|
assert_eq!(
|
|
Name {
|
|
labels: vec!["example".into(), "com".into()],
|
|
absolute: false,
|
|
},
|
|
"example.com".parse().unwrap()
|
|
);
|
|
assert_eq!(Name::new(), "".parse().unwrap());
|
|
assert_eq!(Name::new(), "@".parse().unwrap());
|
|
assert_eq!(Name::root(), ".".parse().unwrap());
|
|
|
|
assert_eq!(ErrorType::EmptyLabel, ".test".parse::<Name>().unwrap_err().error_type);
|
|
assert_eq!(ErrorType::EmptyLabel, "a...test".parse::<Name>().unwrap_err().error_type);
|
|
assert_eq!(ErrorType::LabelTooLong, "totototototototototototototototototototototototototototototototo.xyz".parse::<Name>().unwrap_err().error_type);
|
|
assert_eq!(ErrorType::NonAsciiChar, "😀".parse::<Name>().unwrap_err().error_type);
|
|
assert_eq!(ErrorType::BadEscape, "\\08".parse::<Name>().unwrap_err().error_type);
|
|
assert_eq!(ErrorType::BadEscape, "\\321".parse::<Name>().unwrap_err().error_type);
|
|
assert_eq!(ErrorType::BadEscape, "\\08A".parse::<Name>().unwrap_err().error_type);
|
|
assert_eq!(ErrorType::BadEscape, "\\".parse::<Name>().unwrap_err().error_type);
|
|
|
|
assert_eq!("example.com.", "ExaMpLe.Com.".parse::<Name>().unwrap().to_string());
|
|
assert_eq!("test", "\\084\\101\\115\\116".parse::<Name>().unwrap().to_string());
|
|
}
|
|
|
|
#[test]
|
|
fn test_prefixed_by() {
|
|
let origin = Name::root();
|
|
let res1 = origin.prefixed_by("example.com".parse::<Name>().unwrap());
|
|
let res2 = res1.prefixed_by("srv1".parse::<Name>().unwrap());
|
|
let res3 = res1.prefixed_by("not.example.com.".parse::<Name>().unwrap());
|
|
|
|
let origin2: Name = "com".parse().unwrap();
|
|
let res4 = origin2.prefixed_by("example".parse::<Name>().unwrap());
|
|
|
|
assert_eq!(res1.to_string(), "example.com.");
|
|
assert_eq!(res2.to_string(), "srv1.example.com.");
|
|
assert_eq!(res3.to_string(), "not.example.com.");
|
|
assert_eq!(res4.to_string(), "example.com");
|
|
}
|
|
|
|
#[test]
|
|
fn test_valid_domain() {
|
|
assert!("example.com.".parse::<Name>().unwrap().ensure_is_valid().is_ok());
|
|
assert!("example.com".parse::<Name>().unwrap().ensure_is_valid().is_ok());
|
|
assert!("".parse::<Name>().unwrap().ensure_is_valid().is_ok());
|
|
assert!(".".parse::<Name>().unwrap().ensure_is_valid().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_valid_hostname() {
|
|
assert!("1-23.xx.".parse::<Name>().unwrap().ensure_is_hostname(false).is_ok());
|
|
assert!(".".parse::<Name>().unwrap().ensure_is_hostname(false).is_ok());
|
|
assert!("*.test.".parse::<Name>().unwrap().ensure_is_hostname(true).is_ok());
|
|
assert!("1.b.".parse::<Name>().unwrap().ensure_is_hostname(true).is_ok());
|
|
|
|
assert_eq!(
|
|
ErrorType::InvalidHostname,
|
|
"example.com".parse::<Name>().unwrap().ensure_is_hostname(false).unwrap_err().error_type
|
|
);
|
|
assert_eq!(
|
|
ErrorType::InvalidHostname,
|
|
"-test.".parse::<Name>().unwrap().ensure_is_hostname(false).unwrap_err().error_type
|
|
);
|
|
assert_eq!(
|
|
ErrorType::InvalidHostname,
|
|
"12-.".parse::<Name>().unwrap().ensure_is_hostname(false).unwrap_err().error_type
|
|
);
|
|
assert_eq!(
|
|
ErrorType::InvalidHostname,
|
|
"_test.".parse::<Name>().unwrap().ensure_is_hostname(false).unwrap_err().error_type
|
|
);
|
|
assert_eq!(
|
|
ErrorType::InvalidHostname,
|
|
"*.test.".parse::<Name>().unwrap().ensure_is_hostname(false).unwrap_err().error_type
|
|
);
|
|
assert_eq!(
|
|
ErrorType::InvalidHostname,
|
|
"a.*.b.".parse::<Name>().unwrap().ensure_is_hostname(true).unwrap_err().error_type
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_valid_mailbox() {
|
|
assert!(".".parse::<Name>().unwrap().ensure_is_hostname(false).is_ok());
|
|
assert!("test.".parse::<Name>().unwrap().ensure_is_hostname(false).is_ok());
|
|
assert!("firstname\\.lastname+domain.example.com.".parse::<Name>().unwrap().ensure_is_mailbox().is_ok());
|
|
assert_eq!(
|
|
ErrorType::InvalidMailbox,
|
|
"test+abc.example\\.test.com".parse::<Name>().unwrap().ensure_is_mailbox().unwrap_err().error_type
|
|
);
|
|
assert_eq!(
|
|
ErrorType::InvalidMailbox,
|
|
"admin domain.example.com".parse::<Name>().unwrap().ensure_is_mailbox().unwrap_err().error_type
|
|
);
|
|
}
|
|
}
|