psl_utils/
lib.rs

1use std::borrow::Cow;
2
3/// Normalize a domain for use with [`domain_str`] / [`suffix_str`]:
4/// strip a single trailing dot (FQDN form) and lowercase any ASCII upper
5/// bytes. Borrows when the input is already normalized; only allocates
6/// when uppercase bytes are present.
7pub fn normalize_domain(s: &str) -> Cow<'_, str> {
8    let trimmed = s.strip_suffix('.').unwrap_or(s);
9    if trimmed.bytes().any(|b| b.is_ascii_uppercase()) {
10        Cow::Owned(trimmed.to_ascii_lowercase())
11    } else {
12        Cow::Borrowed(trimmed)
13    }
14}
15
16/// Look up the organizational (registrable) domain.
17/// The caller is expected to pass an already-normalized domain (typically
18/// via [`normalize_domain`]); the returned slice borrows from `s`.
19pub fn domain_str(s: &str) -> Option<&str> {
20    psl::domain_str(s)
21}
22
23/// Look up the public suffix.
24/// The caller is expected to pass an already-normalized domain (typically
25/// via [`normalize_domain`]); the returned slice borrows from `s`.
26pub fn suffix_str(s: &str) -> Option<&str> {
27    psl::suffix_str(s)
28}
29
30/// Check whether the public suffix is explicitly present in the public
31/// suffix list (as opposed to being resolved via the fallback rule for an
32/// unknown top-level domain). Returns `false` when the input has no public
33/// suffix. The caller is expected to pass an already-normalized domain
34/// (typically via [`normalize_domain`]).
35pub fn suffix_is_known(s: &str) -> bool {
36    psl::suffix(s.as_bytes())
37        .map(|suffix| suffix.is_known())
38        .unwrap_or(false)
39}
40
41/// Check whether the registrable domain's public suffix is explicitly
42/// present in the public suffix list. Returns `false` when the input has no
43/// registrable domain. The caller is expected to pass an already-normalized
44/// domain (typically via [`normalize_domain`]).
45pub fn domain_is_known(s: &str) -> bool {
46    psl::domain(s.as_bytes())
47        .map(|domain| domain.suffix().is_known())
48        .unwrap_or(false)
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn already_normalized_borrows() {
57        let s = "example.com";
58        let n = normalize_domain(s);
59        assert!(matches!(n, Cow::Borrowed(_)));
60        assert_eq!(n, "example.com");
61    }
62
63    #[test]
64    fn trims_trailing_dot_without_alloc() {
65        let n = normalize_domain("example.com.");
66        assert!(matches!(n, Cow::Borrowed(_)));
67        assert_eq!(n, "example.com");
68    }
69
70    #[test]
71    fn lowercases_when_needed() {
72        let n = normalize_domain("Example.COM");
73        assert!(matches!(n, Cow::Owned(_)));
74        assert_eq!(n, "example.com");
75    }
76
77    #[test]
78    fn trailing_dot_and_uppercase() {
79        let n = normalize_domain("Example.COM.");
80        assert_eq!(n, "example.com");
81    }
82
83    #[test]
84    fn empty() {
85        assert_eq!(normalize_domain(""), "");
86    }
87
88    #[test]
89    fn domain_str_after_normalize() {
90        let n = normalize_domain("foo.Example.COM.");
91        assert_eq!(domain_str(&n), Some("example.com"));
92    }
93
94    #[test]
95    fn suffix_is_known_known() {
96        let n = normalize_domain("www.example.com");
97        assert_eq!(suffix_is_known(&n), true);
98    }
99
100    #[test]
101    fn suffix_is_known_unknown_fallback() {
102        let n = normalize_domain("domain.invalid");
103        assert_eq!(suffix_is_known(&n), false);
104    }
105
106    #[test]
107    fn suffix_is_known_empty() {
108        let n = normalize_domain("");
109        assert_eq!(suffix_is_known(&n), false);
110    }
111
112    #[test]
113    fn domain_is_known_known() {
114        let n = normalize_domain("www.example.com");
115        assert_eq!(domain_is_known(&n), true);
116    }
117
118    #[test]
119    fn domain_is_known_unknown_fallback() {
120        let n = normalize_domain("domain.invalid");
121        assert_eq!(domain_is_known(&n), false);
122    }
123
124    #[test]
125    fn domain_is_known_empty() {
126        let n = normalize_domain("");
127        assert_eq!(domain_is_known(&n), false);
128    }
129}