Skip to main content

ada_idna/
punycode.rs

1const BASE: i32 = 36;
2const TMIN: i32 = 1;
3const TMAX: i32 = 26;
4const SKEW: i32 = 38;
5const DAMP: i32 = 700;
6const INITIAL_BIAS: i32 = 72;
7const INITIAL_N: u32 = 128;
8
9fn char_to_digit_value(value: u8) -> i32 {
10    match value {
11        b'a'..=b'z' => (value - b'a') as i32,
12        b'0'..=b'9' => (value - b'0') as i32 + 26,
13        _ => -1,
14    }
15}
16
17fn digit_to_char(digit: i32) -> u8 {
18    if digit < 26 {
19        (digit + 97) as u8
20    } else {
21        (digit + 22) as u8
22    }
23}
24
25fn adapt(mut d: i32, n: i32, firsttime: bool) -> i32 {
26    d = if firsttime { d / DAMP } else { d / 2 };
27    d += d / n;
28    let mut k = 0;
29    while d > ((BASE - TMIN) * TMAX) / 2 {
30        d /= BASE - TMIN;
31        k += BASE;
32    }
33    k + (((BASE - TMIN + 1) * d) / (d + SKEW))
34}
35
36pub fn punycode_to_utf32(input: &str) -> Option<Vec<u32>> {
37    // See https://github.com/whatwg/url/issues/803
38    if input.starts_with("xn--") {
39        return None;
40    }
41
42    let mut written_out = 0i32;
43    let mut out = Vec::new();
44    let mut n = INITIAL_N;
45    let mut i = 0i32;
46    let mut bias = INITIAL_BIAS;
47
48    let mut input_bytes = input.as_bytes();
49
50    // grab ascii content
51    if let Some(end_of_ascii) = input_bytes.iter().rposition(|&b| b == b'-') {
52        for &c in &input_bytes[..end_of_ascii] {
53            if c >= 0x80 {
54                return None;
55            }
56            out.push(c as u32);
57            written_out += 1;
58        }
59        input_bytes = &input_bytes[end_of_ascii + 1..];
60    }
61
62    let mut pos = 0;
63    while pos < input_bytes.len() {
64        let oldi = i;
65        let mut w = 1i32;
66        let k = BASE;
67        loop {
68            if pos >= input_bytes.len() {
69                return None;
70            }
71            let code_point = input_bytes[pos];
72            pos += 1;
73            let digit = char_to_digit_value(code_point);
74            if digit < 0 {
75                return None;
76            }
77            if digit > (0x7fffffff - i) / w {
78                return None;
79            }
80            i += digit * w;
81            let t = if k <= bias {
82                TMIN
83            } else if k >= bias + TMAX {
84                TMAX
85            } else {
86                k - bias
87            };
88            if digit < t {
89                break;
90            }
91            if w > 0x7fffffff / (BASE - t) {
92                return None;
93            }
94            w *= BASE - t;
95        }
96        bias = adapt(i - oldi, written_out + 1, oldi == 0);
97        if i / (written_out + 1) > (0x7fffffff - n as i32) {
98            return None;
99        }
100        n += (i / (written_out + 1)) as u32;
101        i %= written_out + 1;
102        if n < 0x80 {
103            return None;
104        }
105        out.insert(i as usize, n);
106        written_out += 1;
107        i += 1;
108    }
109
110    Some(out)
111}
112
113pub fn verify_punycode(input: &str) -> bool {
114    if input.starts_with("xn--") {
115        return false;
116    }
117
118    let mut written_out = 0usize;
119    let mut n = INITIAL_N;
120    let mut i = 0i32;
121    let mut bias = INITIAL_BIAS;
122
123    let mut input_bytes = input.as_bytes();
124
125    // grab ascii content
126    if let Some(end_of_ascii) = input_bytes.iter().rposition(|&b| b == b'-') {
127        for &c in &input_bytes[..end_of_ascii] {
128            if c >= 0x80 {
129                return false;
130            }
131            written_out += 1;
132        }
133        input_bytes = &input_bytes[end_of_ascii + 1..];
134    }
135
136    let mut pos = 0;
137    while pos < input_bytes.len() {
138        let oldi = i;
139        let mut w = 1i32;
140        let k = BASE;
141        loop {
142            if pos >= input_bytes.len() {
143                return false;
144            }
145            let code_point = input_bytes[pos];
146            pos += 1;
147            let digit = char_to_digit_value(code_point);
148            if digit < 0 {
149                return false;
150            }
151            if digit > (0x7fffffff - i) / w {
152                return false;
153            }
154            i += digit * w;
155            let t = if k <= bias {
156                TMIN
157            } else if k >= bias + TMAX {
158                TMAX
159            } else {
160                k - bias
161            };
162            if digit < t {
163                break;
164            }
165            if w > 0x7fffffff / (BASE - t) {
166                return false;
167            }
168            w *= BASE - t;
169        }
170        bias = adapt(i - oldi, (written_out + 1) as i32, oldi == 0);
171        if i / (written_out + 1) as i32 > (0x7fffffff_u32 - n) as i32 {
172            return false;
173        }
174        n += (i / (written_out + 1) as i32) as u32;
175        i %= (written_out + 1) as i32;
176        if n < 0x80 {
177            return false;
178        }
179        written_out += 1;
180        i += 1;
181    }
182
183    true
184}
185
186pub fn utf32_to_punycode(input: &[u32]) -> Option<String> {
187    let mut out = Vec::new();
188    let mut n = INITIAL_N;
189    let mut d = 0i32;
190    let mut bias = INITIAL_BIAS;
191    let mut h = 0usize;
192
193    // first push the ascii content
194    for &c in input {
195        if c < 0x80 {
196            h += 1;
197            out.push(c as u8);
198        }
199        if c > 0x10ffff || (0xd800..0xe000).contains(&c) {
200            return None;
201        }
202    }
203    let b = h;
204    if b > 0 {
205        out.push(b'-');
206    }
207
208    while h < input.len() {
209        let mut m = 0x10FFFF;
210        for &code_point in input {
211            if code_point >= n && code_point < m {
212                m = code_point;
213            }
214        }
215
216        if (m - n) > ((0x7fffffff_u32 - d as u32) / (h + 1) as u32) {
217            return None;
218        }
219        d += ((m - n) * (h + 1) as u32) as i32;
220        n = m;
221
222        for &c in input {
223            if c < n {
224                if d == 0x7fffffff {
225                    return None;
226                }
227                d += 1;
228            }
229            if c == n {
230                let mut q = d;
231                let k = BASE;
232                loop {
233                    let t = if k <= bias {
234                        TMIN
235                    } else if k >= bias + TMAX {
236                        TMAX
237                    } else {
238                        k - bias
239                    };
240
241                    if q < t {
242                        break;
243                    }
244                    out.push(digit_to_char(t + ((q - t) % (BASE - t))));
245                    q = (q - t) / (BASE - t);
246                }
247                out.push(digit_to_char(q));
248                bias = adapt(d, (h + 1) as i32, h == b);
249                d = 0;
250                h += 1;
251            }
252        }
253        d += 1;
254        n += 1;
255    }
256
257    String::from_utf8(out).ok()
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_punycode_encoding() {
266        let input = vec![0x00E4];
267        let result = utf32_to_punycode(&input);
268        assert!(result.is_some());
269        let encoded = result.unwrap();
270        assert_eq!(encoded, "4ca");
271    }
272
273    #[test]
274    fn test_punycode_decoding() {
275        let input = vec![0x00E4];
276        let encoded = utf32_to_punycode(&input).unwrap();
277        let decoded = punycode_to_utf32(&encoded);
278        assert_eq!(decoded, Some(vec![0x00E4]));
279    }
280
281    #[test]
282    fn test_verify_punycode() {
283        assert!(verify_punycode("4ca"));
284        // Empty string is valid punycode (empty input), so test something else
285        assert!(verify_punycode(""));
286    }
287
288    #[test]
289    fn test_xn_prefix_rejection() {
290        // Should reject input starting with "xn--"
291        assert_eq!(punycode_to_utf32("xn--test"), None);
292        assert!(!verify_punycode("xn--test"));
293    }
294}