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#[inline]
12pub(crate) fn is_domain_name(domain: &str) -> Result<()> {
13 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#[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 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 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 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
134fn 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 }
162
163fn is_quoted(c: char) -> bool {
164 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}