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#[cfg(test)]
31mod tests {
32    use super::*;
33
34    #[test]
35    fn already_normalized_borrows() {
36        let s = "example.com";
37        let n = normalize_domain(s);
38        assert!(matches!(n, Cow::Borrowed(_)));
39        assert_eq!(n, "example.com");
40    }
41
42    #[test]
43    fn trims_trailing_dot_without_alloc() {
44        let n = normalize_domain("example.com.");
45        assert!(matches!(n, Cow::Borrowed(_)));
46        assert_eq!(n, "example.com");
47    }
48
49    #[test]
50    fn lowercases_when_needed() {
51        let n = normalize_domain("Example.COM");
52        assert!(matches!(n, Cow::Owned(_)));
53        assert_eq!(n, "example.com");
54    }
55
56    #[test]
57    fn trailing_dot_and_uppercase() {
58        let n = normalize_domain("Example.COM.");
59        assert_eq!(n, "example.com");
60    }
61
62    #[test]
63    fn empty() {
64        assert_eq!(normalize_domain(""), "");
65    }
66
67    #[test]
68    fn domain_str_after_normalize() {
69        let n = normalize_domain("foo.Example.COM.");
70        assert_eq!(domain_str(&n), Some("example.com"));
71    }
72}