Skip to main content

ada_idna/
validation.rs

1pub fn valid_name_code_point(cp: u32) -> bool {
2    matches!(
3        cp,
4        0x002D | 0x0030..=0x0039 | 0x0061..=0x007A
5        | 0x00B7
6        | 0x00C0..=0x00D6 | 0x00D8..=0x00F6 | 0x00F8..=0x037D
7        | 0x037F..=0x1FFF
8        | 0x200C | 0x200D
9        | 0x203F | 0x2040
10        | 0x2070..=0x218F
11        | 0x2C00..=0x2FEF
12        | 0x3001..=0xD7FF
13        | 0xF900..=0xFDCF
14        | 0xFDF0..=0xFFFD
15        | 0x10000..=0xEFFFF
16    )
17}
18
19pub fn valid_name_code_point_first_position(cp: u32) -> bool {
20    // First position cannot be a digit or hyphen
21    match cp {
22        0x0030..=0x0039 => false, // digits
23        0x002D => false,          // hyphen
24        _ => is_letter(cp),
25    }
26}
27
28pub fn valid_name_code_point_other_position(cp: u32) -> bool {
29    // Other positions can be letters, digits, or hyphen (but not special chars)
30    match cp {
31        0x0030..=0x0039 => true, // digits
32        0x002D => true,          // hyphen
33        _ => is_letter(cp),
34    }
35}
36
37fn is_letter(cp: u32) -> bool {
38    matches!(
39        cp,
40        0x0041..=0x005A | 0x0061..=0x007A // A-Z, a-z
41        | 0x00C0..=0x00D6 | 0x00D8..=0x00F6 | 0x00F8..=0x00FF // Latin-1 Supplement letters
42        | 0x0100..=0x017F // Latin Extended-A
43        | 0x0180..=0x024F // Latin Extended-B
44        | 0x0370..=0x03FF // Greek and Coptic
45        | 0x0400..=0x04FF // Cyrillic
46        | 0x0590..=0x05FF // Hebrew
47        | 0x0600..=0x06FF // Arabic
48        | 0x4E00..=0x9FFF // CJK Unified Ideographs
49        | 0x3040..=0x309F // Hiragana
50        | 0x30A0..=0x30FF // Katakana
51        | 0xAC00..=0xD7AF // Hangul Syllables
52        | 0x0E00..=0x0E7F // Thai
53    )
54}
55
56pub fn contains_forbidden_domain_code_point(input: &str) -> bool {
57    for c in input.chars() {
58        let cp = c as u32;
59        match cp {
60            0x0000..=0x001F | 0x007F..=0x009F => return true,
61            0x0020 | 0x0022 | 0x0023 | 0x0025 | 0x002F => return true,
62            0x003A | 0x003C | 0x003E | 0x003F | 0x0040 => return true,
63            0x005B | 0x005C | 0x005D | 0x005E | 0x007C => return true,
64            _ => continue,
65        }
66    }
67    false
68}
69
70pub fn is_ascii(input: &str) -> bool {
71    input.is_ascii()
72}
73
74pub fn is_label_valid(label: &str) -> bool {
75    if label.is_empty() || label.len() > 63 {
76        return false;
77    }
78
79    if label.starts_with('-') || label.ends_with('-') {
80        return false;
81    }
82
83    if let Some(stripped) = label.strip_prefix("xn--") {
84        return crate::punycode::verify_punycode(stripped);
85    }
86
87    for c in label.chars() {
88        if !valid_name_code_point(c as u32) {
89            return false;
90        }
91    }
92
93    true
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_valid_name_code_point() {
102        assert!(valid_name_code_point(b'a' as u32));
103        assert!(valid_name_code_point(b'0' as u32));
104        assert!(valid_name_code_point(b'-' as u32));
105        assert!(!valid_name_code_point(b' ' as u32));
106    }
107
108    #[test]
109    fn test_is_ascii() {
110        assert!(is_ascii("hello"));
111        assert!(!is_ascii("café"));
112    }
113
114    #[test]
115    fn test_is_label_valid() {
116        assert!(is_label_valid("hello"));
117        assert!(is_label_valid("test-domain"));
118        assert!(!is_label_valid("-invalid"));
119        assert!(!is_label_valid("invalid-"));
120        assert!(!is_label_valid(""));
121    }
122
123    #[test]
124    fn test_contains_forbidden_domain_code_point() {
125        assert!(!contains_forbidden_domain_code_point("example.com"));
126        assert!(contains_forbidden_domain_code_point("exam ple.com"));
127        assert!(contains_forbidden_domain_code_point("example.com/path"));
128    }
129}