domain_check_lib/
utils.rs

1//! Utility functions for domain processing and validation.
2//!
3//! This module contains helper functions for domain name validation,
4//! parsing, and other common operations used throughout the library.
5
6use crate::error::DomainCheckError;
7
8/// Validate a domain name format.
9///
10/// Checks if a domain name has valid syntax according to RFC specifications.
11/// This is a basic validation - more comprehensive checks happen during lookup.
12///
13/// # Arguments
14///
15/// * `domain` - The domain name to validate
16///
17/// # Returns
18///
19/// `Ok(())` if valid, `Err(DomainCheckError)` if invalid.
20pub fn validate_domain(domain: &str) -> Result<(), DomainCheckError> {
21    let domain = domain.trim();
22
23    if domain.is_empty() {
24        return Err(DomainCheckError::invalid_domain(
25            domain,
26            "Domain name cannot be empty",
27        ));
28    }
29
30    // TODO: Implement proper domain validation
31    // For now, just check basic format
32    if !domain.contains('.') && domain.len() < 2 {
33        return Err(DomainCheckError::invalid_domain(
34            domain,
35            "Domain name too short",
36        ));
37    }
38
39    Ok(())
40}
41
42/// Extract the base name and TLD from a domain.
43///
44/// Handles multi-level TLDs properly (e.g., "example.co.uk" -> ("example", "co.uk")).
45///
46/// # Arguments
47///
48/// * `domain` - The domain to parse
49///
50/// # Returns
51///
52/// A tuple of (base_name, tld) where TLD is None if no dot is found.
53#[allow(dead_code)]
54pub fn extract_domain_parts(domain: &str) -> (String, Option<String>) {
55    let parts: Vec<&str> = domain.split('.').collect();
56
57    if parts.len() >= 2 {
58        let base_name = parts[0].to_string();
59        let tld = parts[1..].join(".");
60        (base_name, Some(tld))
61    } else {
62        (domain.to_string(), None)
63    }
64}
65
66/// Expand domain inputs based on smart detection rules.
67///
68/// Implements the smart expansion logic:
69/// - Domains with dots are treated as FQDNs (no expansion)
70/// - Domains without dots get expanded with provided TLDs
71/// - Validates and filters out invalid domains
72///
73/// # Arguments
74///
75/// * `domains` - Input domain names
76/// * `tlds` - TLDs to use for expansion (defaults to ["com"] if None)
77///
78/// # Returns
79///
80/// Vector of fully qualified domain names ready for checking.
81pub fn expand_domain_inputs(domains: &[String], tlds: &Option<Vec<String>>) -> Vec<String> {
82    let mut results = Vec::new();
83
84    for domain in domains {
85        let trimmed = domain.trim();
86
87        // Skip empty or invalid domains
88        if trimmed.is_empty() {
89            continue;
90        }
91
92        if trimmed.contains('.') {
93            // Has dot = treat as FQDN (Fully Qualified Domain Name)
94            // Validate basic FQDN structure
95            if is_valid_fqdn(trimmed) {
96                results.push(trimmed.to_string());
97            }
98        } else {
99            // No dot = base name, expand with TLDs
100            // Validate base name (minimum 2 chars, basic format)
101            if is_valid_base_name(trimmed) {
102                match tlds {
103                    Some(tld_list) => {
104                        for tld in tld_list {
105                            let tld_clean = tld.trim();
106                            if !tld_clean.is_empty() {
107                                results.push(format!("{}.{}", trimmed, tld_clean));
108                            }
109                        }
110                    }
111                    None => {
112                        // Default to .com if no TLDs specified
113                        results.push(format!("{}.com", trimmed));
114                    }
115                }
116            }
117        }
118    }
119
120    results
121}
122
123/// Validate that a base domain name (without TLD) is acceptable.
124fn is_valid_base_name(domain: &str) -> bool {
125    // Minimum length check
126    if domain.len() < 2 {
127        return false;
128    }
129
130    // Basic character validation (alphanumeric and hyphens)
131    // Cannot start or end with hyphen
132    if domain.starts_with('-') || domain.ends_with('-') {
133        return false;
134    }
135
136    // Only allow alphanumeric and hyphens
137    domain.chars().all(|c| c.is_alphanumeric() || c == '-')
138}
139
140/// Validate that an FQDN has basic valid structure.
141fn is_valid_fqdn(domain: &str) -> bool {
142    // Basic checks
143    if domain.len() < 4 || domain.len() > 253 {
144        return false;
145    }
146
147    // Must contain at least one dot
148    if !domain.contains('.') {
149        return false;
150    }
151
152    // Cannot start or end with dot or hyphen
153    if domain.starts_with('.')
154        || domain.ends_with('.')
155        || domain.starts_with('-')
156        || domain.ends_with('-')
157    {
158        return false;
159    }
160
161    // Check each part
162    let parts: Vec<&str> = domain.split('.').collect();
163    if parts.len() < 2 {
164        return false;
165    }
166
167    // Each part must be valid
168    for part in parts {
169        if part.is_empty() || part.len() > 63 {
170            return false;
171        }
172
173        // Cannot start or end with hyphen
174        if part.starts_with('-') || part.ends_with('-') {
175            return false;
176        }
177
178        // Only alphanumeric and hyphens
179        if !part.chars().all(|c| c.is_alphanumeric() || c == '-') {
180            return false;
181        }
182    }
183
184    true
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_validate_domain() {
193        assert!(validate_domain("example.com").is_ok());
194        assert!(validate_domain("test").is_ok());
195        assert!(validate_domain("").is_err());
196        assert!(validate_domain("a").is_err());
197    }
198
199    #[test]
200    fn test_extract_domain_parts() {
201        assert_eq!(
202            extract_domain_parts("example.com"),
203            ("example".to_string(), Some("com".to_string()))
204        );
205        assert_eq!(
206            extract_domain_parts("test.co.uk"),
207            ("test".to_string(), Some("co.uk".to_string()))
208        );
209        assert_eq!(
210            extract_domain_parts("example"),
211            ("example".to_string(), None)
212        );
213    }
214
215    #[test]
216    fn test_expand_domain_inputs() {
217        let domains = vec!["example".to_string(), "test.com".to_string()];
218        let tlds = Some(vec!["com".to_string(), "org".to_string()]);
219
220        let result = expand_domain_inputs(&domains, &tlds);
221        assert_eq!(
222            result,
223            vec![
224                "example.com",
225                "example.org",
226                "test.com" // FQDN, no expansion
227            ]
228        );
229    }
230
231    #[test]
232    fn test_expand_domain_inputs_with_invalid() {
233        let domains = vec![
234            "".to_string(),
235            "a".to_string(),
236            "valid".to_string(),
237            "test.com".to_string(),
238        ];
239        let tlds = Some(vec!["com".to_string(), "org".to_string()]);
240
241        let result = expand_domain_inputs(&domains, &tlds);
242        // Should skip empty and single-char domains
243        assert_eq!(result, vec!["valid.com", "valid.org", "test.com"]);
244    }
245
246    #[test]
247    fn test_is_valid_base_name() {
248        assert!(is_valid_base_name("example"));
249        assert!(is_valid_base_name("test-domain"));
250        assert!(is_valid_base_name("abc123"));
251
252        assert!(!is_valid_base_name(""));
253        assert!(!is_valid_base_name("a"));
254        assert!(!is_valid_base_name("-example"));
255        assert!(!is_valid_base_name("example-"));
256        assert!(!is_valid_base_name("test.com")); // Contains dot
257    }
258
259    #[test]
260    fn test_is_valid_fqdn() {
261        assert!(is_valid_fqdn("example.com"));
262        assert!(is_valid_fqdn("test.co.uk"));
263        assert!(is_valid_fqdn("sub.example.com"));
264
265        assert!(!is_valid_fqdn("example"));
266        assert!(!is_valid_fqdn(".com"));
267        assert!(!is_valid_fqdn("example."));
268        assert!(!is_valid_fqdn("-example.com"));
269        assert!(!is_valid_fqdn("example.com-"));
270        assert!(!is_valid_fqdn("ex."));
271    }
272}