addr/
matcher.rs

1use crate::error::{Kind, Result};
2
3const MAX_DOMAIN_LEN: usize = 253;
4const MAX_LABELS_COUNT: usize = 127;
5const MAX_LABEL_LEN: usize = 63;
6
7/// Check if a domain has valid syntax
8// https://en.wikipedia.org/wiki/Domain_name#Domain_name_syntax
9// http://blog.sacaluta.com/2011/12/dns-domain-names-253-or-255-bytesoctets.html
10// https://blogs.msdn.microsoft.com/oldnewthing/20120412-00/?p=7873/
11#[inline]
12pub(crate) fn is_domain_name(domain: &str) -> Result<()> {
13    // check total lengths
14    if domain.chars().count() > MAX_DOMAIN_LEN {
15        return Err(Kind::NameTooLong);
16    }
17
18    let dot_count = domain.matches('.').count();
19
20    if dot_count + 1 > MAX_LABELS_COUNT {
21        return Err(Kind::TooManyLabels);
22    }
23
24    for (i, label) in domain.split('.').enumerate() {
25        is_label(label, i == dot_count)?;
26    }
27
28    Ok(())
29}
30
31pub(crate) fn is_label(label: &str, label_is_tld: bool) -> Result<()> {
32    if label.is_empty() {
33        return Err(Kind::EmptyLabel);
34    }
35
36    if label.chars().count() > MAX_LABEL_LEN {
37        return Err(Kind::LabelTooLong);
38    }
39
40    if label_is_tld && is_num(label) {
41        return Err(Kind::NumericTld);
42    }
43
44    if label.starts_with(|c: char| c.is_ascii() && !c.is_alphanumeric()) {
45        return Err(Kind::LabelStartNotAlnum);
46    }
47
48    if label.ends_with(|c: char| c.is_ascii() && !c.is_alphanumeric()) {
49        return Err(Kind::LabelEndNotAlnum);
50    }
51
52    if label.contains(|c: char| c != '-' && c.is_ascii() && !c.is_alphanumeric()) {
53        return Err(Kind::IllegalCharacter);
54    }
55
56    Ok(())
57}
58
59pub(crate) fn is_num(label: &str) -> bool {
60    label.parse::<f64>().is_ok()
61}
62
63// https://tools.ietf.org/html/rfc2181#section-11
64#[inline]
65pub(crate) fn is_dns_name(name: &str) -> Result<()> {
66    if name.is_empty() {
67        return Err(Kind::EmptyName);
68    }
69
70    if name.contains("..") {
71        return Err(Kind::EmptyLabel);
72    }
73
74    let domain = if name.ends_with('.') {
75        name.get(..name.len() - 1).unwrap_or_default()
76    } else {
77        name
78    };
79
80    // check total lengths
81    if domain.len() > MAX_DOMAIN_LEN {
82        return Err(Kind::NameTooLong);
83    }
84
85    for label in domain.split('.') {
86        if label.len() > MAX_LABEL_LEN {
87            return Err(Kind::LabelTooLong);
88        }
89    }
90
91    Ok(())
92}
93
94pub(crate) fn is_email_local(local: &str) -> Result<()> {
95    let mut chars = local.chars();
96
97    let first = chars.next().ok_or(Kind::NoUserPart)?;
98
99    let last_index = chars.clone().count().max(1) - 1;
100
101    if last_index > MAX_LABEL_LEN {
102        return Err(Kind::EmailLocalTooLong);
103    }
104
105    if first == '"' {
106        // quoted
107        if last_index == 0 {
108            return Err(Kind::QuoteUnclosed);
109        }
110        for (index, c) in chars.enumerate() {
111            if index == last_index {
112                if c != '"' {
113                    return Err(Kind::QuoteUnclosed);
114                }
115            } else if !is_combined(c) && !is_quoted(c) {
116                return Err(Kind::IllegalCharacter);
117            }
118        }
119    } else {
120        // not quoted
121        if first == ' ' || first == '.' || local.contains("..") {
122            return Err(Kind::IllegalCharacter);
123        }
124        for (index, c) in chars.enumerate() {
125            if !is_combined(c) && (index == last_index || c != '.') {
126                return Err(Kind::IllegalCharacter);
127            }
128        }
129    }
130
131    Ok(())
132}
133
134// these characters can be anywhere in the expresion
135// [[:alnum:]!#$%&'*+/=?^_`{|}~-]
136fn is_global(c: char) -> bool {
137    c.is_ascii_alphanumeric()
138        || c == '-'
139        || c == '!'
140        || c == '#'
141        || c == '$'
142        || c == '%'
143        || c == '&'
144        || c == '\''
145        || c == '*'
146        || c == '+'
147        || c == '/'
148        || c == '='
149        || c == '?'
150        || c == '^'
151        || c == '_'
152        || c == '`'
153        || c == '{'
154        || c == '|'
155        || c == '}'
156        || c == '~'
157}
158
159fn is_non_ascii(c: char) -> bool {
160    c as u32 > 0x7f // non-ascii characters (can also be unquoted)
161}
162
163fn is_quoted(c: char) -> bool {
164    // ["(),\\:;<>@\[\]. ]
165    c == '"'
166        || c == '.'
167        || c == ' '
168        || c == '('
169        || c == ')'
170        || c == ','
171        || c == '\\'
172        || c == ':'
173        || c == ';'
174        || c == '<'
175        || c == '>'
176        || c == '@'
177        || c == '['
178        || c == ']'
179}
180
181fn is_combined(c: char) -> bool {
182    is_global(c) || is_non_ascii(c)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn single_label_domain() {
191        assert!(is_domain_name("xn--example").is_ok());
192    }
193
194    #[test]
195    fn plain_domain() {
196        assert!(is_domain_name("example.com").is_ok());
197    }
198
199    #[test]
200    fn subdomains() {
201        assert!(is_domain_name("a.b.c.d.e.f").is_ok());
202    }
203}