Skip to main content

tld/
lib.rs

1//! # `tld`
2//!
3//! A compile-time static perfect hash map of all official top-level domains (TLDs),
4//! automatically synchronized from the [IANA Root Zone Database](https://data.iana.org/TLD/tlds-alpha-by-domain.txt).
5//!
6//! ## Features
7//!
8//! - **Zero Runtime Allocation / O(1) Lookups**: Backed by [`phf::Set`].
9//! - **`no_std` Compatible**: Can be used in embedded or `no_std` crates.
10//! - **Case-insensitive helper**: [`exist_case_insensitive`] for convenience.
11//!
12//! ## Example
13//!
14//! ```rust
15//! use tld::{exist, exist_case_insensitive, TLD};
16//!
17//! // Fast exact lookup (requires lowercase ASCII string)
18//! assert!(exist("com"));
19//! assert!(exist("org"));
20//! assert!(exist("io"));
21//! assert!(exist("uk"));
22//! assert!(!exist("invalidtld"));
23//!
24//! // Case-insensitive lookup helper
25//! assert!(exist_case_insensitive("COM"));
26//! assert!(exist_case_insensitive("Xn--FIQS8S"));
27//!
28//! // Direct access to the compile-time phf::Set
29//! assert!(TLD.contains("net"));
30//! assert!(TLD.len() > 1400);
31//! ```
32
33#![no_std]
34#![deny(missing_docs)]
35#![warn(
36    missing_debug_implementations,
37    missing_copy_implementations,
38    trivial_casts,
39    trivial_numeric_casts,
40    unused_import_braces,
41    unused_qualifications
42)]
43
44include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
45
46/// Checks if the given ASCII lowercase string is a valid Top-Level Domain (TLD)
47/// in the official IANA database.
48///
49/// Note: This function performs an exact match and expects lowercased input.
50/// For case-insensitive lookup, see [`exist_case_insensitive`].
51///
52/// # Examples
53///
54/// ```
55/// assert!(tld::exist("com"));
56/// assert!(tld::exist("io"));
57/// assert!(tld::exist("lt"));
58/// assert!(!exist_invalid("example"));
59///
60/// fn exist_invalid(s: &str) -> bool {
61///     tld::exist(s)
62/// }
63/// ```
64#[inline]
65#[must_use]
66pub fn exist(s: &str) -> bool {
67    TLD.contains(s)
68}
69
70/// Checks if the given string is a valid Top-Level Domain (TLD), ignoring ASCII case.
71///
72/// This performs an O(1) lookup without allocating memory on the heap.
73///
74/// # Examples
75///
76/// ```
77/// assert!(tld::exist_case_insensitive("COM"));
78/// assert!(tld::exist_case_insensitive("cOm"));
79/// assert!(tld::exist_case_insensitive("io"));
80/// assert!(!tld::exist_case_insensitive("nonexistent"));
81/// ```
82#[inline]
83#[must_use]
84pub fn exist_case_insensitive(s: &str) -> bool {
85    // If all characters are already lowercase ASCII, avoid buffer entirely
86    if s.bytes().all(|b| !b.is_ascii_uppercase()) {
87        return TLD.contains(s);
88    }
89
90    // Maximum TLD length is 63 octets per RFC 1035 / RFC 1123
91    let bytes = s.as_bytes();
92    if bytes.len() > 63 || bytes.is_empty() {
93        return false;
94    }
95
96    let mut buf = [0u8; 63];
97    for (i, &b) in bytes.iter().enumerate() {
98        buf[i] = b.to_ascii_lowercase();
99    }
100
101    if let Ok(lower_str) = core::str::from_utf8(&buf[..bytes.len()]) {
102        TLD.contains(lower_str)
103    } else {
104        false
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_tld_set() {
114        assert!(TLD.get_key("aaa").is_some());
115        assert!(TLD.get_key("#33dawaaa").is_none());
116        assert!(TLD.get_key("aco").is_some());
117        assert!(TLD.get_key("uk").is_some());
118        assert!(TLD.get_key("ye").is_some());
119        assert!(TLD.get_key("com").is_some());
120        assert!(TLD.get_key("de").is_some());
121        assert!(TLD.get_key("fr").is_some());
122        assert!(TLD.get_key("ag").is_some());
123        assert!(TLD.get_key("ru").is_some());
124        assert!(TLD.get_key("nl").is_some());
125        assert!(TLD.get_key("lt").is_some());
126        assert!(TLD.get_key("amex").is_some());
127        assert!(TLD.get_key("zw").is_some());
128    }
129
130    #[test]
131    fn test_exist() {
132        assert!(exist("fr"));
133        assert!(exist("de"));
134        assert!(exist("zw"));
135        assert!(!exist("a9292zw"));
136        assert!(!exist("mcd"));
137        assert!(!exist(""));
138    }
139
140    #[test]
141    fn test_exist_case_insensitive() {
142        assert!(exist_case_insensitive("FR"));
143        assert!(exist_case_insensitive("De"));
144        assert!(exist_case_insensitive("zW"));
145        assert!(exist_case_insensitive("COM"));
146        assert!(!exist_case_insensitive("A9292ZW"));
147        assert!(!exist_case_insensitive("MCD"));
148        assert!(!exist_case_insensitive(""));
149        assert!(exist_case_insensitive("xn--fiqs8s"));
150        assert!(exist_case_insensitive("XN--FIQS8S"));
151    }
152}