1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::Error;
7
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
9#[serde(transparent)]
10pub struct Suffix(String);
11
12impl<'de> Deserialize<'de> for Suffix {
13 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
15 where
16 D: serde::Deserializer<'de>,
17 {
18 let raw = String::deserialize(deserializer)?;
19 Self::parse(&raw).map_err(serde::de::Error::custom)
20 }
21}
22
23impl Suffix {
24 pub fn parse(value: &str) -> Result<Self, Error> {
25 let trimmed = value.trim().trim_start_matches('.').trim_end_matches('.');
26 if trimmed.is_empty() {
27 return Err(Error::ExtensionInvalid {
28 extension: value.to_owned(),
29 });
30 }
31
32 let lowered = trimmed.to_lowercase();
33 let ascii = idna::domain_to_ascii(&lowered).map_err(|_| Error::ExtensionInvalid {
34 extension: value.to_owned(),
35 })?;
36
37 if ascii.len() > 253 {
38 return Err(Error::ExtensionInvalid {
39 extension: value.to_owned(),
40 });
41 }
42
43 for label in ascii.split('.') {
44 if check_label(label).is_err() || label.bytes().all(|b| b.is_ascii_digit()) {
45 return Err(Error::ExtensionInvalid {
46 extension: value.to_owned(),
47 });
48 }
49 }
50
51 Ok(Self(ascii))
52 }
53
54 pub(crate) fn from_raw(value: &str) -> Self {
56 Self(value.to_lowercase())
57 }
58
59 #[must_use]
60 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63
64 #[must_use]
65 pub fn label_count(&self) -> usize {
66 self.0.split('.').count()
67 }
68
69 #[must_use]
71 pub fn delegated_label(&self) -> &str {
72 self.0.rsplit('.').next().unwrap_or(&self.0)
73 }
74
75 #[must_use]
76 pub fn is_country_code(&self) -> bool {
77 let root = self.delegated_label();
78 root.len() == 2 && root.bytes().all(|b| b.is_ascii_alphabetic())
79 }
80
81 #[must_use]
83 pub fn ancestors(&self) -> Vec<String> {
84 let mut chain = Vec::new();
85 let mut rest: &str = &self.0;
86 loop {
87 chain.push(rest.to_owned());
88 match rest.split_once('.') {
89 Some((_, tail)) if !tail.is_empty() => rest = tail,
90 _ => break,
91 }
92 }
93 chain
94 }
95}
96
97fn check_label(label: &str) -> Result<(), &'static str> {
98 if label.is_empty() {
99 return Err("it has an empty label");
100 }
101 if label.len() > 63 {
102 return Err("a label is longer than 63 characters");
103 }
104 if label.starts_with('-') || label.ends_with('-') {
105 return Err("a label starts or ends with a hyphen");
106 }
107 if !label
108 .bytes()
109 .all(|b| b.is_ascii_alphanumeric() || b == b'-')
110 {
111 return Err("it has a character that is not a letter, digit, or hyphen");
112 }
113 Ok(())
114}
115
116pub fn parse_name(value: &str) -> Result<String, Error> {
118 let trimmed = value.trim();
119 let refuse = |reason: &str| Error::NameInvalid {
120 name: value.to_owned(),
121 reason: reason.to_owned(),
122 };
123
124 if trimmed.is_empty() {
125 return Err(refuse("it is empty"));
126 }
127
128 let ascii = idna::domain_to_ascii(&trimmed.to_lowercase())
129 .map_err(|_| refuse("it is not a usable domain name"))?;
130
131 if ascii.len() > 253 {
132 return Err(refuse("it is longer than 253 characters"));
133 }
134 for label in ascii.split('.') {
135 check_label(label).map_err(refuse)?;
136 }
137
138 Ok(ascii)
139}
140
141impl fmt::Display for Suffix {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 f.write_str(&self.0)
144 }
145}
146
147impl FromStr for Suffix {
148 type Err = Error;
149
150 fn from_str(s: &str) -> Result<Self, Self::Err> {
151 Self::parse(s)
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
156#[serde(rename_all = "kebab-case")]
157pub enum ExtensionKind {
158 Generic,
159 Country,
160 Sponsored,
161}
162
163impl ExtensionKind {
164 #[must_use]
165 pub const fn label(self) -> &'static str {
166 match self {
167 Self::Generic => "generic",
168 Self::Country => "country",
169 Self::Sponsored => "sponsored",
170 }
171 }
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct Extension {
176 pub suffix: Suffix,
177 pub kind: ExtensionKind,
178 #[serde(default)]
180 pub rank: Option<u32>,
181 #[serde(default)]
182 pub industries: Vec<String>,
183 #[serde(default)]
184 pub region: Option<String>,
185 #[serde(default)]
186 pub country: Option<String>,
187 #[serde(default = "default_registrable")]
188 pub registrable: bool,
189 #[serde(default)]
190 pub repurposed: bool,
191}
192
193const fn default_registrable() -> bool {
194 true
195}
196
197impl Extension {
198 #[must_use]
199 pub fn is_in_industry(&self, key: &str) -> bool {
200 self.industries.iter().any(|i| i == key)
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn a_leading_dot_is_accepted_and_stripped() {
210 assert_eq!(Suffix::parse(".com").unwrap().as_str(), "com");
211 assert_eq!(Suffix::parse("com").unwrap().as_str(), "com");
212 assert_eq!(Suffix::parse(" .COM ").unwrap().as_str(), "com");
213 }
214
215 #[test]
216 fn rubbish_is_refused_rather_than_guessed() {
217 for bad in ["", ".", "..", "-com", "com-", "a..b", "9", "co m", "*"] {
218 assert!(Suffix::parse(bad).is_err(), "{bad} should be refused");
219 }
220 }
221
222 #[test]
223 fn a_label_of_sixty_three_characters_is_the_longest_one_allowed() {
224 let longest = "a".repeat(63);
225 assert_eq!(Suffix::parse(&longest).unwrap().as_str(), longest);
226 assert!(Suffix::parse(&"a".repeat(64)).is_err());
227 }
228
229 #[test]
230 fn an_extension_of_two_hundred_and_fifty_three_characters_is_the_longest_one_allowed() {
231 let label = "a".repeat(63);
232 let at_the_cap = [
233 label.as_str(),
234 label.as_str(),
235 label.as_str(),
236 &"b".repeat(61),
237 ]
238 .join(".");
239 assert_eq!(at_the_cap.len(), 253);
240 assert!(Suffix::parse(&at_the_cap).is_ok());
241
242 let over_the_cap = format!("{at_the_cap}b");
243 assert_eq!(over_the_cap.len(), 254);
244 assert!(Suffix::parse(&over_the_cap).is_err());
245 }
246
247 #[test]
248 fn a_suffix_read_from_json_goes_through_the_same_parser_as_a_typed_one() {
249 let parsed: Suffix = serde_json::from_str("\".CO.UK\"").unwrap();
250 assert_eq!(parsed.as_str(), "co.uk");
251 assert_eq!(parsed, Suffix::parse(".CO.UK").unwrap());
252 }
253
254 #[test]
255 fn an_unusable_suffix_in_json_is_refused_rather_than_loaded_unchecked() {
256 for bad in [
257 "\"\"", "\".\"", "\"-com\"", "\"com-\"", "\"a..b\"", "\"9\"", "\"co m\"",
258 ] {
259 assert!(
260 serde_json::from_str::<Suffix>(bad).is_err(),
261 "{bad} should be refused"
262 );
263 }
264 }
265
266 #[test]
267 fn a_suffix_survives_a_round_trip_through_json() {
268 let suffix = Suffix::parse("com.bd").unwrap();
269 let text = serde_json::to_string(&suffix).unwrap();
270 assert_eq!(text, "\"com.bd\"");
271 assert_eq!(serde_json::from_str::<Suffix>(&text).unwrap(), suffix);
272 }
273
274 #[test]
275 fn label_count_separates_second_level_from_third() {
276 assert_eq!(Suffix::parse("com").unwrap().label_count(), 1);
277 assert_eq!(Suffix::parse("co.uk").unwrap().label_count(), 2);
278 }
279
280 #[test]
281 fn the_country_test_reads_the_delegated_label_not_the_whole_string() {
282 assert!(Suffix::parse("uk").unwrap().is_country_code());
283 assert!(Suffix::parse("co.uk").unwrap().is_country_code());
284 assert!(Suffix::parse("bd").unwrap().is_country_code());
285 assert!(!Suffix::parse("com").unwrap().is_country_code());
286 assert!(!Suffix::parse("dev").unwrap().is_country_code());
287 }
288
289 #[test]
290 fn the_parent_chain_runs_longest_first() {
291 let suffix = Suffix::parse("com.bd").unwrap();
292 assert_eq!(
293 suffix.ancestors(),
294 vec!["com.bd".to_owned(), "bd".to_owned()]
295 );
296 let plain = Suffix::parse("dev").unwrap();
297 assert_eq!(plain.ancestors(), vec!["dev".to_owned()]);
298 }
299
300 #[test]
301 fn a_control_byte_in_a_name_is_refused() {
302 for bad in [
303 "x\rdomain google.com",
304 "x\ndomain google.com",
305 "x\r\ndomain google.com",
306 "x\0y",
307 "x y",
308 "x\ty",
309 "x\u{1b}[2Ky",
310 ] {
311 assert!(
312 parse_name(bad).is_err(),
313 "{bad:?} must never reach a request line"
314 );
315 }
316 }
317
318 #[test]
319 fn a_usable_name_survives_validation() {
320 assert_eq!(parse_name("example").unwrap(), "example");
321 assert_eq!(parse_name(" Example ").unwrap(), "example");
322 assert_eq!(parse_name("shop.example").unwrap(), "shop.example");
323 assert_eq!(parse_name("123").unwrap(), "123");
324 assert_eq!(parse_name("a-b").unwrap(), "a-b");
325 }
326
327 #[test]
328 fn a_unicode_name_is_normalized_before_it_reaches_the_wire() {
329 assert_eq!(parse_name("münchen").unwrap(), "xn--mnchen-3ya");
330 }
331
332 #[test]
333 fn a_malformed_name_is_refused() {
334 for bad in ["", " ", "-lead", "trail-", "a..b", &"x".repeat(64)] {
335 assert!(parse_name(bad).is_err(), "{bad:?} should be refused");
336 }
337 }
338
339 #[test]
340 fn a_unicode_extension_is_normalized_to_its_ascii_form() {
341 let suffix = Suffix::parse("বাংলা").unwrap();
342 assert!(suffix.as_str().starts_with("xn--"));
343 }
344}