Skip to main content

bindizr_core/dns/
name.rs

1#[derive(Debug, Clone, PartialEq, Eq)]
2pub enum NameError {
3    DanglingEscape,
4    InvalidEmail,
5}
6
7impl std::fmt::Display for NameError {
8    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9        match self {
10            NameError::DanglingEscape => write!(f, "domain name contains a dangling escape"),
11            NameError::InvalidEmail => write!(f, "email must contain exactly one @"),
12        }
13    }
14}
15
16impl std::error::Error for NameError {}
17
18pub fn split_presentation_labels(name: &str) -> Result<Vec<String>, NameError> {
19    let mut labels = Vec::new();
20    let mut label = String::new();
21    let mut escaped = false;
22
23    for c in name.chars() {
24        if escaped {
25            label.push(c);
26            escaped = false;
27            continue;
28        }
29
30        match c {
31            '\\' => escaped = true,
32            '.' => {
33                labels.push(label);
34                label = String::new();
35            }
36            _ => label.push(c),
37        }
38    }
39
40    if escaped {
41        return Err(NameError::DanglingEscape);
42    }
43
44    labels.push(label);
45    Ok(labels)
46}
47
48pub fn to_fqdn_lowercase(value: &str) -> String {
49    format!(
50        "{}.",
51        value.trim().trim_end_matches('.').to_ascii_lowercase()
52    )
53}
54
55pub fn to_fqdn(value: &str) -> String {
56    format!("{}.", value.trim_end_matches('.'))
57}
58
59pub fn is_same_or_subdomain_fqdn(name: &str, zone: &str) -> bool {
60    name == zone || name.ends_with(&format!(".{}", zone))
61}
62
63pub fn to_relative_domain(fqdn: &str, zone_name: &str) -> String {
64    let fqdn = to_fqdn(fqdn);
65    let zone = to_fqdn(zone_name);
66
67    if fqdn.eq_ignore_ascii_case(&zone) {
68        return "@".to_string();
69    }
70
71    let fqdn_lower = fqdn.to_ascii_lowercase();
72    let zone_lower = zone.to_ascii_lowercase();
73
74    if is_same_or_subdomain_fqdn(&fqdn_lower, &zone_lower) {
75        let relative_part = &fqdn[..fqdn.len() - zone.len()];
76        relative_part.trim_end_matches('.').to_string()
77    } else {
78        fqdn.trim_end_matches('.').to_string()
79    }
80}
81
82pub fn is_in_bailiwick(name: &str, zone_name: &str) -> bool {
83    let name = to_fqdn(name).to_ascii_lowercase();
84    let zone = to_fqdn(zone_name).to_ascii_lowercase();
85
86    is_same_or_subdomain_fqdn(&name, &zone)
87}
88
89pub fn is_apex_name(name: &str, zone_name: &str) -> bool {
90    name == "@" || to_fqdn(name).eq_ignore_ascii_case(&to_fqdn(zone_name))
91}
92
93pub fn email_to_soa_mailbox(value: &str) -> Result<String, NameError> {
94    if value.matches('@').count() != 1 {
95        return Err(NameError::InvalidEmail);
96    }
97
98    let (local, domain) = value.split_once('@').ok_or(NameError::InvalidEmail)?;
99
100    Ok(format!(
101        "{}.{}.",
102        escape_soa_local_part(local),
103        domain.trim_end_matches('.')
104    ))
105}
106
107fn escape_soa_local_part(local: &str) -> String {
108    let mut escaped = String::with_capacity(local.len());
109
110    for c in local.chars() {
111        if c == '.' || c == '\\' {
112            escaped.push('\\');
113        }
114        escaped.push(c);
115    }
116
117    escaped
118}
119
120#[cfg(test)]
121mod tests {
122    use super::{is_in_bailiwick, to_relative_domain};
123
124    #[test]
125    fn is_in_bailiwick_accepts_apex_and_subdomain() {
126        assert!(is_in_bailiwick("example.com.", "example.com."));
127        assert!(is_in_bailiwick("ns.example.com.", "example.com."));
128    }
129
130    #[test]
131    fn is_in_bailiwick_rejects_sibling_suffix_match() {
132        assert!(!is_in_bailiwick("notexample.com.", "example.com."));
133        assert!(!is_in_bailiwick("ns.notexample.com.", "example.com."));
134    }
135
136    #[test]
137    fn to_relative_domain_converts_only_zone_apex_and_subdomains() {
138        assert_eq!(to_relative_domain("example.com.", "example.com."), "@");
139        assert_eq!(to_relative_domain("ns.example.com.", "example.com."), "ns");
140        assert_eq!(
141            to_relative_domain("notexample.com.", "example.com."),
142            "notexample.com"
143        );
144    }
145}