1use std::fmt;
14
15const BASE: i64 = 36;
16const TMIN: i64 = 1;
17const TMAX: i64 = 26;
18const INITIAL_BIAS: i64 = 72;
19const DAMP: i64 = 700;
20const SKEW: i64 = 38;
21
22const DIGITS: &[u8; 36] = b"abcdefghijklmnopqrstuvwxyz0123456789";
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum PunycodeError {
28 NotAscii,
30 Incomplete,
32 InvalidDigit,
34 InvalidCodePoint,
36}
37
38impl fmt::Display for PunycodeError {
39 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40 let msg = match self {
41 PunycodeError::NotAscii => "input is not ASCII",
42 PunycodeError::Incomplete => "incomplete punycode string",
43 PunycodeError::InvalidDigit => "invalid extended code point",
44 PunycodeError::InvalidCodePoint => "invalid decoded character",
45 };
46 f.write_str(msg)
47 }
48}
49
50impl std::error::Error for PunycodeError {}
51
52fn threshold(j: i64, bias: i64) -> i64 {
56 (BASE * (j + 1) - bias).clamp(TMIN, TMAX)
57}
58
59fn adapt(mut delta: i64, first: bool, numchars: i64) -> i64 {
61 delta /= if first { DAMP } else { 2 };
62 delta += delta / numchars;
63 let mut divisions = 0;
64 while delta > ((BASE - TMIN) * TMAX) / 2 {
65 delta /= BASE - TMIN;
66 divisions += BASE;
67 }
68 divisions + (BASE * delta) / (delta + SKEW)
69}
70
71fn segregate(text: &[char]) -> (Vec<u8>, Vec<char>) {
75 let mut base = Vec::new();
76 let mut extended = std::collections::BTreeSet::new();
77 for &c in text {
78 if (c as u32) < 128 {
79 base.push(c as u8);
80 } else {
81 extended.insert(c);
82 }
83 }
84 (base, extended.into_iter().collect())
85}
86
87fn selective_len(text: &[char], max: u32) -> i64 {
89 text.iter().filter(|&&c| (c as u32) < max).count() as i64
90}
91
92fn selective_find(text: &[char], target: char, mut index: i64, mut pos: i64) -> (i64, i64) {
95 let len = text.len() as i64;
96 loop {
97 pos += 1;
98 if pos == len {
99 return (-1, -1);
100 }
101 let c = text[pos as usize];
102 if c == target {
103 return (index + 1, pos);
104 } else if c < target {
105 index += 1;
106 }
107 }
108}
109
110fn insertion_unsort(text: &[char], extended: &[char]) -> Vec<i64> {
112 let mut oldchar: i64 = 0x80;
113 let mut oldindex: i64 = -1;
114 let mut result = Vec::new();
115
116 for &c in extended {
117 let mut index: i64 = -1;
118 let mut pos: i64 = -1;
119 let ch = c as i64;
120 let curlen = selective_len(text, c as u32);
121 let mut delta = (curlen + 1) * (ch - oldchar);
122 loop {
123 let (i, p) = selective_find(text, c, index, pos);
124 index = i;
125 pos = p;
126 if index == -1 {
127 break;
128 }
129 delta += index - oldindex;
130 result.push(delta - 1);
131 oldindex = index;
132 delta = 0;
133 }
134 oldchar = ch;
135 }
136 result
137}
138
139fn generate_generalized_integer(mut n: i64, bias: i64) -> Vec<u8> {
141 let mut result = Vec::new();
142 let mut j = 0;
143 loop {
144 let t = threshold(j, bias);
145 if n < t {
146 result.push(DIGITS[n as usize]);
147 return result;
148 }
149 result.push(DIGITS[(t + (n - t) % (BASE - t)) as usize]);
150 n = (n - t) / (BASE - t);
151 j += 1;
152 }
153}
154
155fn generate_integers(baselen: i64, deltas: &[i64]) -> Vec<u8> {
157 let mut result = Vec::new();
158 let mut bias = INITIAL_BIAS;
159 for (points, &delta) in deltas.iter().enumerate() {
160 result.extend(generate_generalized_integer(delta, bias));
161 bias = adapt(delta, points == 0, baselen + points as i64 + 1);
162 }
163 result
164}
165
166pub fn encode(text: &str) -> String {
168 let chars: Vec<char> = text.chars().collect();
169 let (mut base, extended) = segregate(&chars);
170 let deltas = insertion_unsort(&chars, &extended);
171 let encoded = generate_integers(base.len() as i64, &deltas);
172
173 if !base.is_empty() {
174 base.push(b'-');
175 }
176 base.extend(encoded);
177 String::from_utf8(base).expect("punycode output is ASCII")
178}
179
180fn decode_generalized_number(
184 extended: &[u8],
185 mut extpos: usize,
186 bias: i64,
187) -> Result<(usize, i64), PunycodeError> {
188 let mut result = 0;
189 let mut w = 1;
190 let mut j = 0;
191 loop {
192 let ch = *extended.get(extpos).ok_or(PunycodeError::Incomplete)?;
193 extpos += 1;
194 let digit = if ch.is_ascii_uppercase() {
195 (ch - b'A') as i64
196 } else if ch.is_ascii_digit() {
197 (ch - b'0') as i64 + 26
198 } else {
199 return Err(PunycodeError::InvalidDigit);
200 };
201 let t = threshold(j, bias);
202 result += digit * w;
203 if digit < t {
204 return Ok((extpos, result));
205 }
206 w *= BASE - t;
207 j += 1;
208 }
209}
210
211fn insertion_sort(mut base: Vec<char>, extended: &[u8]) -> Result<Vec<char>, PunycodeError> {
213 let mut char_code: i64 = 0x80;
214 let mut pos: i64 = -1;
215 let mut bias = INITIAL_BIAS;
216 let mut extpos = 0;
217
218 while extpos < extended.len() {
219 let (newpos, delta) = decode_generalized_number(extended, extpos, bias)?;
220 pos += delta + 1;
221 char_code += pos / (base.len() as i64 + 1);
222 if char_code > 0x10FFFF {
223 return Err(PunycodeError::InvalidCodePoint);
224 }
225 pos %= base.len() as i64 + 1;
226 let ch = char::from_u32(char_code as u32).ok_or(PunycodeError::InvalidCodePoint)?;
227 base.insert(pos as usize, ch);
228 bias = adapt(delta, extpos == 0, base.len() as i64);
229 extpos = newpos;
230 }
231 Ok(base)
232}
233
234pub fn decode(text: &str) -> Result<String, PunycodeError> {
236 if !text.is_ascii() {
237 return Err(PunycodeError::NotAscii);
238 }
239 let bytes = text.as_bytes();
240
241 let (base, extended) = match bytes.iter().rposition(|&b| b == b'-') {
244 Some(pos) => (
245 bytes[..pos].iter().map(|&b| b as char).collect(),
246 bytes[pos + 1..].to_ascii_uppercase(),
247 ),
248 None => (Vec::new(), bytes.to_ascii_uppercase()),
249 };
250
251 Ok(insertion_sort(base, &extended)?.into_iter().collect())
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn encodes_known_values() {
260 assert_eq!(encode("münchen"), "mnchen-3ya");
261 assert_eq!(encode("abc"), "abc-"); assert_eq!(encode(""), "");
263 assert_eq!(encode("ü"), "tda");
264 }
265
266 #[test]
267 fn decodes_known_values() {
268 assert_eq!(decode("mnchen-3ya").unwrap(), "münchen");
269 assert_eq!(decode("abc-").unwrap(), "abc");
270 assert_eq!(decode("").unwrap(), "");
271 assert_eq!(decode("tda").unwrap(), "ü");
272 }
273
274 #[test]
275 fn round_trips() {
276 for s in [
277 "münchen",
278 "café",
279 "naïve",
280 "日本語",
281 "Ελληνικά",
282 "abc",
283 "Hello-World",
284 "",
285 "ñ",
286 "a1b2c3",
287 "emoji-💡-here",
288 "mixed café 日本 test",
289 ] {
290 let encoded = encode(s);
291 assert_eq!(decode(&encoded).unwrap(), s, "round-trip failed for {s:?}");
292 }
293 }
294
295 #[test]
296 fn decode_rejects_bad_input() {
297 assert_eq!(decode("café"), Err(PunycodeError::NotAscii)); assert!(decode("-!").is_err()); }
300}