Coverage Report

Created: 2026-06-30 07:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/addr-0.15.6/src/matcher.rs
Line
Count
Source
1
use crate::error::{Kind, Result};
2
3
const MAX_DOMAIN_LEN: usize = 253;
4
const MAX_LABELS_COUNT: usize = 127;
5
const 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]
12
0
pub(crate) fn is_domain_name(domain: &str) -> Result<()> {
13
    // check total lengths
14
0
    if domain.chars().count() > MAX_DOMAIN_LEN {
15
0
        return Err(Kind::NameTooLong);
16
0
    }
17
18
0
    let dot_count = domain.matches('.').count();
19
20
0
    if dot_count + 1 > MAX_LABELS_COUNT {
21
0
        return Err(Kind::TooManyLabels);
22
0
    }
23
24
0
    for (i, label) in domain.split('.').enumerate() {
25
0
        is_label(label, i == dot_count)?;
26
    }
27
28
0
    Ok(())
29
0
}
30
31
0
pub(crate) fn is_label(label: &str, label_is_tld: bool) -> Result<()> {
32
0
    if label.is_empty() {
33
0
        return Err(Kind::EmptyLabel);
34
0
    }
35
36
0
    if label.chars().count() > MAX_LABEL_LEN {
37
0
        return Err(Kind::LabelTooLong);
38
0
    }
39
40
0
    if label_is_tld && is_num(label) {
41
0
        return Err(Kind::NumericTld);
42
0
    }
43
44
0
    if label.starts_with(|c: char| c.is_ascii() && !c.is_alphanumeric()) {
45
0
        return Err(Kind::LabelStartNotAlnum);
46
0
    }
47
48
0
    if label.ends_with(|c: char| c.is_ascii() && !c.is_alphanumeric()) {
49
0
        return Err(Kind::LabelEndNotAlnum);
50
0
    }
51
52
0
    if label.contains(|c: char| c != '-' && c.is_ascii() && !c.is_alphanumeric()) {
53
0
        return Err(Kind::IllegalCharacter);
54
0
    }
55
56
0
    Ok(())
57
0
}
58
59
0
pub(crate) fn is_num(label: &str) -> bool {
60
0
    label.parse::<f64>().is_ok()
61
0
}
62
63
// https://tools.ietf.org/html/rfc2181#section-11
64
#[inline]
65
0
pub(crate) fn is_dns_name(name: &str) -> Result<()> {
66
0
    if name.is_empty() {
67
0
        return Err(Kind::EmptyName);
68
0
    }
69
70
0
    if name.contains("..") {
71
0
        return Err(Kind::EmptyLabel);
72
0
    }
73
74
0
    let domain = if name.ends_with('.') {
75
0
        name.get(..name.len() - 1).unwrap_or_default()
76
    } else {
77
0
        name
78
    };
79
80
    // check total lengths
81
0
    if domain.len() > MAX_DOMAIN_LEN {
82
0
        return Err(Kind::NameTooLong);
83
0
    }
84
85
0
    for label in domain.split('.') {
86
0
        if label.len() > MAX_LABEL_LEN {
87
0
            return Err(Kind::LabelTooLong);
88
0
        }
89
    }
90
91
0
    Ok(())
92
0
}
93
94
0
pub(crate) fn is_email_local(local: &str) -> Result<()> {
95
0
    let mut chars = local.chars();
96
97
0
    let first = chars.next().ok_or(Kind::NoUserPart)?;
98
99
0
    let last_index = chars.clone().count().max(1) - 1;
100
101
0
    if last_index > MAX_LABEL_LEN {
102
0
        return Err(Kind::EmailLocalTooLong);
103
0
    }
104
105
0
    if first == '"' {
106
        // quoted
107
0
        if last_index == 0 {
108
0
            return Err(Kind::QuoteUnclosed);
109
0
        }
110
0
        for (index, c) in chars.enumerate() {
111
0
            if index == last_index {
112
0
                if c != '"' {
113
0
                    return Err(Kind::QuoteUnclosed);
114
0
                }
115
0
            } else if !is_combined(c) && !is_quoted(c) {
116
0
                return Err(Kind::IllegalCharacter);
117
0
            }
118
        }
119
    } else {
120
        // not quoted
121
0
        if first == ' ' || first == '.' || local.contains("..") {
122
0
            return Err(Kind::IllegalCharacter);
123
0
        }
124
0
        for (index, c) in chars.enumerate() {
125
0
            if !is_combined(c) && (index == last_index || c != '.') {
126
0
                return Err(Kind::IllegalCharacter);
127
0
            }
128
        }
129
    }
130
131
0
    Ok(())
132
0
}
133
134
// these characters can be anywhere in the expresion
135
// [[:alnum:]!#$%&'*+/=?^_`{|}~-]
136
0
fn is_global(c: char) -> bool {
137
0
    c.is_ascii_alphanumeric()
138
0
        || c == '-'
139
0
        || c == '!'
140
0
        || c == '#'
141
0
        || c == '$'
142
0
        || c == '%'
143
0
        || c == '&'
144
0
        || c == '\''
145
0
        || c == '*'
146
0
        || c == '+'
147
0
        || c == '/'
148
0
        || c == '='
149
0
        || c == '?'
150
0
        || c == '^'
151
0
        || c == '_'
152
0
        || c == '`'
153
0
        || c == '{'
154
0
        || c == '|'
155
0
        || c == '}'
156
0
        || c == '~'
157
0
}
158
159
0
fn is_non_ascii(c: char) -> bool {
160
0
    c as u32 > 0x7f // non-ascii characters (can also be unquoted)
161
0
}
162
163
0
fn is_quoted(c: char) -> bool {
164
    // ["(),\\:;<>@\[\]. ]
165
0
    c == '"'
166
0
        || c == '.'
167
0
        || c == ' '
168
0
        || c == '('
169
0
        || c == ')'
170
0
        || c == ','
171
0
        || c == '\\'
172
0
        || c == ':'
173
0
        || c == ';'
174
0
        || c == '<'
175
0
        || c == '>'
176
0
        || c == '@'
177
0
        || c == '['
178
0
        || c == ']'
179
0
}
180
181
0
fn is_combined(c: char) -> bool {
182
0
    is_global(c) || is_non_ascii(c)
183
0
}
184
185
#[cfg(test)]
186
mod 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
}