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