Skip to main content

tld/
types.rs

1//! Common types for the public suffix implementation crates
2//!
3//! The types in this crate assume that the input is valid
4//! UTF-8 encoded domain names. If input is potentially invalid,
5//! use a higher level crate like the `addr` crate.
6//!
7//! Some implentations may also assume that the domain name is
8//! in lowercase and/or may only support looking up unicode
9//! domain names.
10
11#![no_std]
12#![forbid(unsafe_code)]
13
14use core::{
15    cmp::Ordering,
16    hash::{Hash, Hasher},
17};
18
19/// A list of all public suffixes
20pub trait List {
21    /// Finds the suffix information of the given input labels
22    ///
23    /// *NB:* `labels` must be in reverse order
24    fn find<'a, T>(&self, labels: T) -> Info
25    where
26        T: Iterator<Item = &'a [u8]>;
27
28    /// Get the public suffix of the domain
29    #[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    /// Get the registrable domain
58    #[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/// Type of suffix
90#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
91pub enum Type {
92    Icann,
93    Private,
94}
95
96/// Information about the suffix
97#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
98pub struct Info {
99    pub len: usize,
100    pub typ: Option<Type>,
101}
102
103/// The suffix of a domain name
104#[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    /// Builds a new suffix
113    #[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    /// The suffix as bytes
125    #[inline]
126    #[must_use]
127    pub const fn as_bytes(&self) -> &'a [u8] {
128        self.bytes
129    }
130
131    /// Whether or not the suffix is fully qualified (i.e. it ends with a `.`)
132    #[inline]
133    #[must_use]
134    pub const fn is_fqdn(&self) -> bool {
135        self.fqdn
136    }
137
138    /// Whether this is an `ICANN`, `private` or unknown suffix
139    #[inline]
140    #[must_use]
141    pub const fn typ(&self) -> Option<Type> {
142        self.typ
143    }
144
145    /// Returns the suffix with a trailing `.` removed
146    #[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    /// Whether or not this is a known suffix (i.e. it is explicitly in the public suffix list)
157    // Could be const but Isahc needs support for Rust v1.41
158    #[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/// A registrable domain name
208#[derive(Copy, Clone, Eq, Debug)]
209pub struct Domain<'a> {
210    bytes: &'a [u8],
211    suffix: Suffix<'a>,
212}
213
214impl<'a> Domain<'a> {
215    /// Builds a root domain
216    #[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    /// The domain name as bytes
227    #[inline]
228    #[must_use]
229    pub const fn as_bytes(&self) -> &'a [u8] {
230        self.bytes
231    }
232
233    /// The public suffix of this domain name
234    #[inline]
235    #[must_use]
236    pub const fn suffix(&self) -> Suffix<'_> {
237        self.suffix
238    }
239
240    /// Returns the domain with a trailing `.` removed
241    #[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}