1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
use crate::{Domain, NameClass};
impl Domain {
//! Label Validation
/// The maximum length of a domain label.
pub const MAX_LABEL_LEN: usize = 63;
/// Classifies the domain `label`.
///
/// The accepted bytes are ASCII, so a non-`Invalid` class proves the label is valid UTF-8. The
/// parse impls rely on that to convert classified bytes without re-validating; widening the
/// byte set here would make those conversions unsound.
fn classify_label(label: &[u8]) -> NameClass {
if (label.is_empty() || label.len() > Self::MAX_LABEL_LEN)
|| (label[0] == b'-' || label[label.len() - 1] == b'-')
{
NameClass::Invalid
} else {
let mut class: NameClass = NameClass::Lowercase;
for c in label {
if c.is_ascii_uppercase() {
class = NameClass::MixedCase;
} else if !(c.is_ascii_lowercase() || c.is_ascii_digit() || *c == b'-') {
return NameClass::Invalid;
}
}
class
}
}
/// Checks if the domain `label` is valid, optionally ignoring case.
fn is_valid_label_op_ignore_case(label: &[u8], ignore_case: bool) -> bool {
match Self::classify_label(label) {
NameClass::Lowercase => true,
NameClass::MixedCase => ignore_case,
NameClass::Invalid => false,
}
}
/// Checks if the domain `label` is valid.
///
/// A valid label is 1 to 63 ([`Self::MAX_LABEL_LEN`]) bytes of ASCII lowercase letters, digits,
/// and dashes and must not start or end with a dash: the preferred syntax of
/// [RFC 1035](https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1), relaxed by
/// [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#section-2.1) to allow a leading digit.
/// Uppercase letters are only valid with [`Self::is_valid_label_ignore_case`]; see
/// [`Self::is_valid_name`] for how the crate diverges from those documents.
#[must_use]
pub fn is_valid_label(label: &[u8]) -> bool {
Self::is_valid_label_op_ignore_case(label, false)
}
/// Checks if the domain `label` is valid, accepting uppercase letters.
/// (see [`Self::is_valid_label`])
#[must_use]
pub fn is_valid_label_ignore_case(label: &[u8]) -> bool {
Self::is_valid_label_op_ignore_case(label, true)
}
/// Checks if the domain `label` is valid.
/// (see [`Self::is_valid_label`])
#[must_use]
pub fn is_valid_label_str(label: &str) -> bool {
Self::is_valid_label(label.as_bytes())
}
/// Checks if the domain `label` is valid, accepting uppercase letters.
/// (see [`Self::is_valid_label`])
#[must_use]
pub fn is_valid_label_ignore_case_str(label: &str) -> bool {
Self::is_valid_label_ignore_case(label.as_bytes())
}
}
impl Domain {
//! Domain Validation
/// The maximum length of a domain name.
pub const MAX_NAME_LEN: usize = 253;
/// Gets the final label of the domain `name`.
fn final_label(name: &[u8]) -> &[u8] {
match name.iter().rposition(|c| *c == b'.') {
Some(dot) => &name[dot + 1..],
None => name,
}
}
/// Classifies the domain `name`.
pub(crate) fn classify_name(name: &[u8]) -> NameClass {
if name.is_empty()
|| name.len() > Self::MAX_NAME_LEN
|| Self::final_label(name).iter().all(|c| c.is_ascii_digit())
{
NameClass::Invalid
} else {
let mut class: NameClass = NameClass::Lowercase;
for label in name.split(|c| *c == b'.') {
match Self::classify_label(label) {
NameClass::Invalid => return NameClass::Invalid,
NameClass::MixedCase => class = NameClass::MixedCase,
NameClass::Lowercase => {}
}
}
class
}
}
/// Checks if the domain `name` is valid, optionally ignoring case.
fn is_valid_name_op_ignore_case(name: &[u8], ignore_case: bool) -> bool {
match Self::classify_name(name) {
NameClass::Lowercase => true,
NameClass::MixedCase => ignore_case,
NameClass::Invalid => false,
}
}
/// Checks if the domain `name` is valid.
///
/// A valid name is 1 to 253 ([`Self::MAX_NAME_LEN`]) bytes of dot-separated valid labels: the
/// preferred syntax of [RFC 1035](https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1),
/// relaxed by [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#section-2.1) to allow a leading
/// digit, under the size limits of
/// [RFC 1035](https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4). The 253 is the
/// presentation form of the 255-octet wire limit. Labels cannot be empty, so leading, trailing,
/// and consecutive dots are invalid. The final label cannot be all-numeric
/// ([RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#section-2.1) &
/// [RFC 3696](https://www.rfc-editor.org/rfc/rfc3696#section-2)), so `9999.com` is a name but
/// `999.1.1.1` is not. Names are ASCII, so they are always valid UTF-8, and Unicode must first
/// be converted to its [RFC 5890](https://www.rfc-editor.org/rfc/rfc5890) A-label form.
///
/// It diverges from those documents in three ways. Case is canonicalized rather than matched
/// case-insensitively ([RFC 4343](https://www.rfc-editor.org/rfc/rfc4343)): this function
/// requires lowercase and [`Self::is_valid_name_ignore_case`] accepts either. The trailing root
/// dot of a fully-qualified name is rejected. Underscores are rejected, so the service labels
/// of [RFC 2782](https://www.rfc-editor.org/rfc/rfc2782) cannot be represented, even though
/// [RFC 2181](https://www.rfc-editor.org/rfc/rfc2181#section-11) permits any octet in a label.
#[must_use]
pub fn is_valid_name(name: &[u8]) -> bool {
Self::is_valid_name_op_ignore_case(name, false)
}
/// Checks if the domain `name` is valid, accepting uppercase letters.
/// (see [`Self::is_valid_name`])
#[must_use]
pub fn is_valid_name_ignore_case(name: &[u8]) -> bool {
Self::is_valid_name_op_ignore_case(name, true)
}
/// Checks if the domain `name` is valid.
/// (see [`Self::is_valid_name`])
#[must_use]
pub fn is_valid_name_str(name: &str) -> bool {
Self::is_valid_name(name.as_bytes())
}
/// Checks if the domain `name` is valid, accepting uppercase letters.
/// (see [`Self::is_valid_name`])
#[must_use]
pub fn is_valid_name_ignore_case_str(name: &str) -> bool {
Self::is_valid_name_ignore_case(name.as_bytes())
}
}
#[cfg(test)]
mod tests {
use crate::Domain;
#[test]
fn is_valid_label() {
let test_cases: &[(&str, bool, bool)] = &[
("", false, false),
("09", true, true),
("az", true, true),
("AZ", false, true),
("-a", false, false),
("a-", false, false),
("a--a", true, true),
("a-a", true, true),
("a-a-a", true, true),
];
for (label, expected, expected_ignore_case) in test_cases {
let result: bool = Domain::is_valid_label_str(label);
assert_eq!(result, *expected, "label={}", label);
let result: bool = Domain::is_valid_label_ignore_case_str(label);
assert_eq!(result, *expected_ignore_case, "label={}", label);
}
}
#[test]
fn label_length_boundaries() {
let test_cases: &[(usize, bool)] = &[
(Domain::MAX_LABEL_LEN, true),
(Domain::MAX_LABEL_LEN + 1, false),
];
for (len, expected) in test_cases {
let label: String = "a".repeat(*len);
let result: bool = Domain::is_valid_label_str(label.as_str());
assert_eq!(result, *expected, "len={}", len);
}
}
#[test]
fn is_valid_name() {
let test_cases: &[(&str, bool, bool)] = &[
("", false, false),
("09", false, false),
("az", true, true),
("AZ", false, true),
(".a", false, false),
("a.", false, false),
("a..a", false, false),
("a.a", true, true),
("a.a.a", true, true),
("a-a.a-a.a-a", true, true),
];
for (name, expected, expected_ignore_case) in test_cases {
let result: bool = Domain::is_valid_name_str(name);
assert_eq!(result, *expected, "name={}", name);
let result: bool = Domain::is_valid_name_ignore_case_str(name);
assert_eq!(result, *expected_ignore_case, "name={}", name);
}
}
#[test]
fn final_label_not_all_numeric() {
let test_cases: &[(&str, bool)] = &[
("9999.com", true),
("123.example.com", true),
("example.1-2", true),
("1.1", false),
("999.1.1.1", false),
("127.0.0.1", false),
("123", false),
("example.123", false),
];
for (name, expected) in test_cases {
let result: bool = Domain::is_valid_name_str(name);
assert_eq!(result, *expected, "name={}", name);
}
}
#[test]
fn name_length_boundaries() {
let test_cases: &[(usize, usize, bool)] = &[
(61, Domain::MAX_NAME_LEN, true),
(62, Domain::MAX_NAME_LEN + 1, false),
];
for (tail_len, expected_len, expected) in test_cases {
let label: String = "a".repeat(Domain::MAX_LABEL_LEN);
let name: String = format!("{}.{}.{}.{}", label, label, label, "a".repeat(*tail_len));
assert_eq!(name.len(), *expected_len, "tail_len={}", tail_len);
let result: bool = Domain::is_valid_name_str(name.as_str());
assert_eq!(result, *expected, "tail_len={}", tail_len);
}
}
}