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#![forbid(unsafe_code)]
12
13use core::{
14    cmp::Ordering,
15    hash::{Hash, Hasher},
16};
17
18/// A list of all public suffixes
19pub trait List {
20    /// Finds the suffix information of the given input labels
21    ///
22    /// *NB:* `labels` must be in reverse order
23    fn find<'a, T>(&self, labels: T) -> Info
24    where
25        T: Iterator<Item = &'a [u8]>;
26
27    /// Get the public suffix of the domain
28    #[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    /// Get the registrable domain
57    #[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/// Type of suffix
89#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
90pub enum Type {
91    Icann,
92    Private,
93}
94
95/// Information about the suffix
96#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
97pub struct Info {
98    pub len: usize,
99    pub typ: Option<Type>,
100}
101
102/// The suffix of a domain name
103#[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    /// Builds a new suffix
112    #[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    /// The suffix as bytes
124    #[inline]
125    #[must_use]
126    pub const fn as_bytes(&self) -> &'a [u8] {
127        self.bytes
128    }
129
130    /// Whether or not the suffix is fully qualified (i.e. it ends with a `.`)
131    #[inline]
132    #[must_use]
133    pub const fn is_fqdn(&self) -> bool {
134        self.fqdn
135    }
136
137    /// Whether this is an `ICANN`, `private` or unknown suffix
138    #[inline]
139    #[must_use]
140    pub const fn typ(&self) -> Option<Type> {
141        self.typ
142    }
143
144    /// Returns the suffix with a trailing `.` removed
145    #[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    /// Whether or not this is a known suffix (i.e. it is explicitly in the public suffix list)
156    // Could be const but Isahc needs support for Rust v1.41
157    #[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/// A registrable domain name
207#[derive(Copy, Clone, Eq, Debug)]
208pub struct Domain<'a> {
209    bytes: &'a [u8],
210    suffix: Suffix<'a>,
211}
212
213impl<'a> Domain<'a> {
214    /// Builds a root domain
215    #[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    /// The domain name as bytes
226    #[inline]
227    #[must_use]
228    pub const fn as_bytes(&self) -> &'a [u8] {
229        self.bytes
230    }
231
232    /// The public suffix of this domain name
233    #[inline]
234    #[must_use]
235    pub const fn suffix(&self) -> Suffix<'_> {
236        self.suffix
237    }
238
239    /// Returns the domain with a trailing `.` removed
240    #[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}