1use crate::{mapping, normalization, punycode, unicode, validation};
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum IdnaError {
5 InvalidInput,
6 LabelTooLong,
7 EmptyLabel,
8 InvalidCharacter,
9 PunycodeError,
10 ValidationError,
11}
12
13pub fn to_ascii(domain: &str) -> Result<String, IdnaError> {
14 if domain.is_empty() {
15 return Err(IdnaError::EmptyLabel);
16 }
17
18 let labels: Vec<&str> = domain.split('.').collect();
19 let mut result_labels = Vec::with_capacity(labels.len());
20
21 for label in labels {
22 if label.is_empty() {
23 return Err(IdnaError::EmptyLabel);
24 }
25
26 let ascii_label = process_label_to_ascii(label)?;
27 result_labels.push(ascii_label);
28 }
29
30 Ok(result_labels.join("."))
31}
32
33pub fn to_unicode(domain: &str) -> Result<String, IdnaError> {
34 if domain.is_empty() {
35 return Err(IdnaError::EmptyLabel);
36 }
37
38 let labels: Vec<&str> = domain.split('.').collect();
39 let mut result_labels = Vec::with_capacity(labels.len());
40
41 for label in labels {
42 if label.is_empty() {
43 return Err(IdnaError::EmptyLabel);
44 }
45
46 let unicode_label = process_label_to_unicode(label)?;
47 result_labels.push(unicode_label);
48 }
49
50 Ok(result_labels.join("."))
51}
52
53fn process_label_to_ascii(label: &str) -> Result<String, IdnaError> {
54 if label.len() > 63 {
55 return Err(IdnaError::LabelTooLong);
56 }
57
58 if validation::is_ascii(label) {
59 let mapped = mapping::ascii_map(label);
60 if !validation::is_label_valid(&mapped) {
61 return Err(IdnaError::ValidationError);
62 }
63 return Ok(mapped);
64 }
65
66 let mapped = mapping::map(label);
67 let normalized = normalization::normalize(&mapped);
68
69 if validation::contains_forbidden_domain_code_point(&normalized) {
70 return Err(IdnaError::InvalidCharacter);
71 }
72
73 let utf32_chars = unicode::utf8_to_utf32(normalized.as_bytes());
74 if utf32_chars.is_empty() {
75 return Err(IdnaError::InvalidInput);
76 }
77
78 let punycode = punycode::utf32_to_punycode(&utf32_chars).ok_or(IdnaError::PunycodeError)?;
79
80 let result = format!("xn--{}", punycode);
81
82 if result.len() > 63 {
83 return Err(IdnaError::LabelTooLong);
84 }
85
86 Ok(result)
87}
88
89fn process_label_to_unicode(label: &str) -> Result<String, IdnaError> {
90 if !label.starts_with("xn--") {
91 if !validation::is_label_valid(label) {
92 return Err(IdnaError::ValidationError);
93 }
94 return Ok(label.to_string());
95 }
96
97 let punycode_part = &label[4..];
98
99 let utf32_chars = punycode::punycode_to_utf32(punycode_part).ok_or(IdnaError::PunycodeError)?;
100
101 let utf8_bytes = unicode::utf32_to_utf8(&utf32_chars);
102 if utf8_bytes.is_empty() {
103 return Err(IdnaError::InvalidInput);
104 }
105
106 let decoded = String::from_utf8(utf8_bytes).map_err(|_| IdnaError::InvalidInput)?;
107
108 let mapped = mapping::map(&decoded);
109 let normalized = normalization::normalize(&mapped);
110
111 if validation::contains_forbidden_domain_code_point(&normalized) {
112 return Err(IdnaError::InvalidCharacter);
113 }
114
115 let re_encoded_utf32 = unicode::utf8_to_utf32(normalized.as_bytes());
116 let re_encoded_punycode =
117 punycode::utf32_to_punycode(&re_encoded_utf32).ok_or(IdnaError::PunycodeError)?;
118
119 if re_encoded_punycode != punycode_part {
120 return Err(IdnaError::ValidationError);
121 }
122
123 Ok(normalized)
124}
125
126pub use validation::{contains_forbidden_domain_code_point, is_ascii};
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_to_ascii_simple() {
134 let result = to_ascii("example.com");
135 assert!(result.is_ok());
136 assert_eq!(result.unwrap(), "example.com");
137 }
138
139 #[test]
140 fn test_to_ascii_unicode() {
141 let result = to_ascii("café.example");
142 assert!(result.is_ok());
143 assert!(result.unwrap().starts_with("xn--"));
144 }
145
146 #[test]
147 fn test_to_unicode() {
148 let result = to_unicode("xn--4ca.example");
149 if result.is_ok() {
152 let unicode_domain = result.unwrap();
153 assert!(unicode_domain.contains("ä"));
154 }
155 }
158
159 #[test]
160 fn test_empty_domain() {
161 assert!(to_ascii("").is_err());
162 assert!(to_unicode("").is_err());
163 }
164
165 #[test]
166 fn test_label_too_long() {
167 let long_label = "a".repeat(64);
168 let result = to_ascii(&long_label);
169 assert!(result.is_err());
170 }
171}