1#![forbid(unsafe_code)]
12
13use core::{
14 cmp::Ordering,
15 hash::{Hash, Hasher},
16};
17
18pub trait List {
20 fn find<'a, T>(&self, labels: T) -> Info
24 where
25 T: Iterator<Item = &'a [u8]>;
26
27 #[inline]
29 fn suffix<'a>(&self, name: &'a [u8]) -> Option<Suffix<'a>> {
30 let mut labels = name.rsplit(|x| *x == b'.');
31 let fqdn = if name.ends_with(b".") {
32 labels.next();
33 true
34 } else {
35 false
36 };
37 let Info {
38 mut len,
39 typ,
40 } = self.find(labels);
41 if fqdn {
42 len += 1;
43 }
44 if len == 0 {
45 return None;
46 }
47 let offset = name.len() - len;
48 let bytes = name.get(offset..)?;
49 Some(Suffix {
50 bytes,
51 fqdn,
52 typ,
53 })
54 }
55
56 #[inline]
58 fn domain<'a>(&self, name: &'a [u8]) -> Option<Domain<'a>> {
59 let suffix = self.suffix(name)?;
60 let name_len = name.len();
61 let suffix_len = suffix.bytes.len();
62 if name_len < suffix_len + 2 {
63 return None;
64 }
65 let offset = name_len - (1 + suffix_len);
66 let subdomain = name.get(..offset)?;
67 let root_label = subdomain.rsplitn(2, |x| *x == b'.').next()?;
68 let registrable_len = root_label.len() + 1 + suffix_len;
69 let offset = name_len - registrable_len;
70 let bytes = name.get(offset..)?;
71 Some(Domain {
72 bytes,
73 suffix,
74 })
75 }
76}
77
78impl<L: List> List for &'_ L {
79 #[inline]
80 fn find<'a, T>(&self, labels: T) -> Info
81 where
82 T: Iterator<Item = &'a [u8]>,
83 {
84 (*self).find(labels)
85 }
86}
87
88#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
90pub enum Type {
91 Icann,
92 Private,
93}
94
95#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
97pub struct Info {
98 pub len: usize,
99 pub typ: Option<Type>,
100}
101
102#[derive(Copy, Clone, Eq, Debug)]
104pub struct Suffix<'a> {
105 bytes: &'a [u8],
106 fqdn: bool,
107 typ: Option<Type>,
108}
109
110impl<'a> Suffix<'a> {
111 #[inline]
113 #[must_use]
114 #[doc(hidden)]
115 pub fn new(bytes: &[u8], typ: Option<Type>) -> Suffix<'_> {
116 Suffix {
117 bytes,
118 typ,
119 fqdn: bytes.ends_with(b"."),
120 }
121 }
122
123 #[inline]
125 #[must_use]
126 pub const fn as_bytes(&self) -> &'a [u8] {
127 self.bytes
128 }
129
130 #[inline]
132 #[must_use]
133 pub const fn is_fqdn(&self) -> bool {
134 self.fqdn
135 }
136
137 #[inline]
139 #[must_use]
140 pub const fn typ(&self) -> Option<Type> {
141 self.typ
142 }
143
144 #[inline]
146 #[must_use]
147 pub fn trim(mut self) -> Self {
148 if self.fqdn {
149 self.bytes = &self.bytes[..self.bytes.len() - 1];
150 self.fqdn = false;
151 }
152 self
153 }
154
155 #[inline]
158 #[must_use]
159 pub fn is_known(&self) -> bool {
160 self.typ.is_some()
161 }
162}
163
164impl PartialEq for Suffix<'_> {
165 #[inline]
166 fn eq(&self, other: &Self) -> bool {
167 self.trim().bytes == strip_dot(other.bytes)
168 }
169}
170
171impl PartialEq<&[u8]> for Suffix<'_> {
172 #[inline]
173 fn eq(&self, other: &&[u8]) -> bool {
174 self.trim().bytes == strip_dot(other)
175 }
176}
177
178impl PartialEq<&str> for Suffix<'_> {
179 #[inline]
180 fn eq(&self, other: &&str) -> bool {
181 self.trim().bytes == strip_dot(other.as_bytes())
182 }
183}
184
185impl Ord for Suffix<'_> {
186 #[inline]
187 fn cmp(&self, other: &Self) -> Ordering {
188 self.trim().bytes.cmp(strip_dot(other.bytes))
189 }
190}
191
192impl PartialOrd for Suffix<'_> {
193 #[inline]
194 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
195 Some(self.trim().bytes.cmp(strip_dot(other.bytes)))
196 }
197}
198
199impl Hash for Suffix<'_> {
200 #[inline]
201 fn hash<H: Hasher>(&self, state: &mut H) {
202 self.trim().bytes.hash(state);
203 }
204}
205
206#[derive(Copy, Clone, Eq, Debug)]
208pub struct Domain<'a> {
209 bytes: &'a [u8],
210 suffix: Suffix<'a>,
211}
212
213impl<'a> Domain<'a> {
214 #[inline]
216 #[must_use]
217 #[doc(hidden)]
218 pub const fn new(bytes: &'a [u8], suffix: Suffix<'a>) -> Domain<'a> {
219 Domain {
220 bytes,
221 suffix,
222 }
223 }
224
225 #[inline]
227 #[must_use]
228 pub const fn as_bytes(&self) -> &'a [u8] {
229 self.bytes
230 }
231
232 #[inline]
234 #[must_use]
235 pub const fn suffix(&self) -> Suffix<'_> {
236 self.suffix
237 }
238
239 #[inline]
241 #[must_use]
242 pub fn trim(mut self) -> Self {
243 if self.suffix.fqdn {
244 self.bytes = &self.bytes[..self.bytes.len() - 1];
245 self.suffix = self.suffix.trim();
246 }
247 self
248 }
249}
250
251impl PartialEq for Domain<'_> {
252 #[inline]
253 fn eq(&self, other: &Self) -> bool {
254 self.trim().bytes == strip_dot(other.bytes)
255 }
256}
257
258impl PartialEq<&[u8]> for Domain<'_> {
259 #[inline]
260 fn eq(&self, other: &&[u8]) -> bool {
261 self.trim().bytes == strip_dot(other)
262 }
263}
264
265impl PartialEq<&str> for Domain<'_> {
266 #[inline]
267 fn eq(&self, other: &&str) -> bool {
268 self.trim().bytes == strip_dot(other.as_bytes())
269 }
270}
271
272impl Ord for Domain<'_> {
273 #[inline]
274 fn cmp(&self, other: &Self) -> Ordering {
275 self.trim().bytes.cmp(strip_dot(other.bytes))
276 }
277}
278
279impl PartialOrd for Domain<'_> {
280 #[inline]
281 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
282 Some(self.trim().bytes.cmp(strip_dot(other.bytes)))
283 }
284}
285
286impl Hash for Domain<'_> {
287 #[inline]
288 fn hash<H: Hasher>(&self, state: &mut H) {
289 self.trim().bytes.hash(state);
290 }
291}
292
293#[inline]
294fn strip_dot(bytes: &[u8]) -> &[u8] {
295 if bytes.ends_with(b".") {
296 &bytes[..bytes.len() - 1]
297 } else {
298 bytes
299 }
300}
301
302#[cfg(test)]
303mod test {
304 use super::{Info, List as Psl};
305
306 struct List;
307
308 impl Psl for List {
309 fn find<'a, T>(&self, mut labels: T) -> Info
310 where
311 T: Iterator<Item = &'a [u8]>,
312 {
313 match labels.next() {
314 Some(label) => Info {
315 len: label.len(),
316 typ: None,
317 },
318 None => Info {
319 len: 0,
320 typ: None,
321 },
322 }
323 }
324 }
325
326 #[test]
327 fn www_example_com() {
328 let domain = List.domain(b"www.example.com").expect("domain name");
329 assert_eq!(domain, "example.com");
330 assert_eq!(domain.suffix(), "com");
331 }
332
333 #[test]
334 fn example_com() {
335 let domain = List.domain(b"example.com").expect("domain name");
336 assert_eq!(domain, "example.com");
337 assert_eq!(domain.suffix(), "com");
338 }
339
340 #[test]
341 fn example_com_() {
342 let domain = List.domain(b"example.com.").expect("domain name");
343 assert_eq!(domain, "example.com.");
344 assert_eq!(domain.suffix(), "com.");
345 }
346
347 #[test]
348 fn fqdn_comparisons() {
349 let domain = List.domain(b"example.com.").expect("domain name");
350 assert_eq!(domain, "example.com");
351 assert_eq!(domain.suffix(), "com");
352 }
353
354 #[test]
355 fn non_fqdn_comparisons() {
356 let domain = List.domain(b"example.com").expect("domain name");
357 assert_eq!(domain, "example.com.");
358 assert_eq!(domain.suffix(), "com.");
359 }
360
361 #[test]
362 fn self_comparisons() {
363 let fqdn = List.domain(b"example.com.").expect("domain name");
364 let non_fqdn = List.domain(b"example.com").expect("domain name");
365 assert_eq!(fqdn, non_fqdn);
366 assert_eq!(fqdn.suffix(), non_fqdn.suffix());
367 }
368
369 #[test]
370 fn btreemap_comparisons() {
371 extern crate alloc;
372 use alloc::collections::BTreeSet;
373
374 let mut domain = BTreeSet::new();
375 let mut suffix = BTreeSet::new();
376
377 let fqdn = List.domain(b"example.com.").expect("domain name");
378 domain.insert(fqdn);
379 suffix.insert(fqdn.suffix());
380
381 let non_fqdn = List.domain(b"example.com").expect("domain name");
382 assert!(domain.contains(&non_fqdn));
383 assert!(suffix.contains(&non_fqdn.suffix()));
384 }
385
386 #[test]
387 fn hashmap_comparisons() {
388 extern crate std;
389 use std::collections::HashSet;
390
391 let mut domain = HashSet::new();
392 let mut suffix = HashSet::new();
393
394 let fqdn = List.domain(b"example.com.").expect("domain name");
395 domain.insert(fqdn);
396 suffix.insert(fqdn.suffix());
397
398 let non_fqdn = List.domain(b"example.com").expect("domain name");
399 assert!(domain.contains(&non_fqdn));
400 assert!(suffix.contains(&non_fqdn.suffix()));
401 }
402
403 #[test]
404 fn com() {
405 let domain = List.domain(b"com");
406 assert_eq!(domain, None);
407
408 let suffix = List.suffix(b"com").expect("public suffix");
409 assert_eq!(suffix, "com");
410 }
411
412 #[test]
413 fn root() {
414 let domain = List.domain(b".");
415 assert_eq!(domain, None);
416
417 let suffix = List.suffix(b".").expect("public suffix");
418 assert_eq!(suffix, ".");
419 }
420
421 #[test]
422 fn empty_string() {
423 let domain = List.domain(b"");
424 assert_eq!(domain, None);
425
426 let suffix = List.suffix(b"");
427 assert_eq!(suffix, None);
428 }
429
430 #[test]
431 #[allow(dead_code)]
432 fn accessors_borrow_correctly() {
433 fn return_suffix(domain: &str) -> &[u8] {
434 let suffix = List.suffix(domain.as_bytes()).unwrap();
435 suffix.as_bytes()
436 }
437
438 fn return_domain(name: &str) -> &[u8] {
439 let domain = List.domain(name.as_bytes()).unwrap();
440 domain.as_bytes()
441 }
442 }
443}