use std::borrow::Cow;
pub(crate) fn to_ascii(domain: &str) -> Result<Cow<'_, str>, ()> {
if domain.is_empty() {
return Err(());
}
if domain.is_ascii() {
let lower = lowercase_ascii(domain);
if contains_forbidden_domain_code_point(lower.as_ref()) {
return Err(());
}
return Ok(lower);
}
if domain.chars().any(is_disallowed_idna) {
return Err(());
}
let mapped = uts46_map(domain);
if mapped.is_empty() {
return Err(());
}
if mapped.chars().any(is_disallowed_idna) {
return Err(());
}
let mut out = String::with_capacity(mapped.len() + 16);
for (i, label) in mapped.split('.').enumerate() {
if i > 0 {
out.push('.');
}
if label.is_empty() {
continue;
}
if label.is_ascii() {
out.push_str(label);
} else {
out.push_str("xn--");
encode_punycode(label, &mut out)?;
}
}
if out.is_empty() || contains_forbidden_domain_code_point(&out) {
return Err(());
}
Ok(Cow::Owned(out))
}
fn is_disallowed_idna(c: char) -> bool {
match c {
'\u{00A0}' | '\u{3000}' | '\u{FFFD}' => true,
'\u{2028}' | '\u{2029}' => true,
'\u{FDD0}'..='\u{FDEF}' => true,
c => {
let u = c as u32;
(u & 0xFFFE) == 0xFFFE
}
}
}
fn lowercase_ascii(s: &str) -> Cow<'_, str> {
if s.bytes().all(|b| !b.is_ascii_uppercase()) {
Cow::Borrowed(s)
} else {
Cow::Owned(s.to_ascii_lowercase())
}
}
#[inline]
fn contains_forbidden_domain_code_point(s: &str) -> bool {
s.bytes().any(|b| {
matches!(
b,
0x00..=0x1F
| b' '
| b'#'
| b'/'
| b':'
| b'<'
| b'>'
| b'?'
| b'@'
| b'['
| b'\\'
| b']'
| b'^'
| b'|'
| b'%'
| 0x7F
)
})
}
fn uts46_map(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for c in input.chars() {
map_char(c, &mut out);
}
out
}
fn map_char(c: char, out: &mut String) {
match c {
'\u{00AD}' | '\u{200B}' | '\u{2060}' | '\u{FEFF}' => {}
'\u{3002}' => out.push('.'),
'\u{FF01}'..='\u{FF5E}' => {
let mapped = char::from_u32(u32::from(c) - 0xFEE0).unwrap_or(c);
out.push(mapped.to_ascii_lowercase());
}
c if (0x1D400..=0x1D419).contains(&(c as u32)) => {
out.push(char::from(b'a' + (c as u32 - 0x1D400) as u8));
}
c if (0x1D41A..=0x1D433).contains(&(c as u32)) => {
out.push(char::from(b'a' + (c as u32 - 0x1D41A) as u8));
}
c if c.is_ascii() => out.push(c.to_ascii_lowercase()),
c => {
for ch in c.to_lowercase() {
if ch.is_ascii() {
out.push(ch.to_ascii_lowercase());
} else {
out.push(ch);
}
}
}
}
}
const BASE: u32 = 36;
const TMIN: u32 = 1;
const TMAX: u32 = 26;
const SKEW: u32 = 38;
const DAMP: u32 = 700;
const INITIAL_BIAS: u32 = 72;
const INITIAL_N: u32 = 128;
fn encode_punycode(input: &str, out: &mut String) -> Result<(), ()> {
let mut n = INITIAL_N;
let mut delta: u32 = 0;
let mut bias = INITIAL_BIAS;
let chars: Vec<char> = input.chars().collect();
let mut basic_count = 0u32;
for &c in &chars {
if c.is_ascii() {
out.push(c.to_ascii_lowercase());
basic_count += 1;
}
}
let mut h = basic_count;
let b = basic_count;
if b > 0 {
out.push('-');
}
while (h as usize) < chars.len() {
let mut m = u32::MAX;
for &c in &chars {
let cp = c as u32;
if cp >= n && cp < m {
m = cp;
}
}
if m == u32::MAX {
return Err(());
}
let advance = m.checked_sub(n).ok_or(())?;
delta = delta
.checked_add(advance.checked_mul(h.checked_add(1).ok_or(())?).ok_or(())?)
.ok_or(())?;
n = m;
for &c in &chars {
let cp = c as u32;
if cp < n {
delta = delta.checked_add(1).ok_or(())?;
} else if cp == n {
let mut q = delta;
let mut k = BASE;
loop {
let t = if k <= bias {
TMIN
} else if k >= bias.saturating_add(TMAX) {
TMAX
} else {
k - bias
};
if q < t {
break;
}
let code = t + ((q - t) % (BASE - t));
out.push(digit_to_char(code)?);
q = (q - t) / (BASE - t);
k = k.checked_add(BASE).ok_or(())?;
}
out.push(digit_to_char(q)?);
bias = adapt(delta, h + 1, h == b);
delta = 0;
h += 1;
}
}
delta += 1;
n += 1;
}
Ok(())
}
fn adapt(mut delta: u32, num_points: u32, first_time: bool) -> u32 {
delta = if first_time { delta / DAMP } else { delta / 2 };
delta += delta / num_points;
let mut k = 0u32;
while delta > ((BASE - TMIN) * TMAX) / 2 {
delta /= BASE - TMIN;
k += BASE;
}
k + (((BASE - TMIN + 1) * delta) / (delta + SKEW))
}
fn digit_to_char(d: u32) -> Result<char, ()> {
match d {
0..=25 => Ok(char::from(b'a' + d as u8)),
26..=35 => Ok(char::from(b'0' + (d as u8 - 26))),
_ => Err(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_passthrough_invalid_punycode() {
assert_eq!(
to_ascii("a.b.c.xn--pokxncvks").unwrap(),
"a.b.c.xn--pokxncvks"
);
assert_eq!(to_ascii("XN--").unwrap(), "xn--");
}
#[test]
fn punycode_examples() {
assert_eq!(to_ascii("é").unwrap(), "xn--9ca");
assert_eq!(to_ascii("你好你好").unwrap(), "xn--6qqa088eba");
assert_eq!(to_ascii("faß.ExAmPlE").unwrap(), "xn--fa-hia.example");
assert_eq!(to_ascii("☃").unwrap(), "xn--n3h");
assert!(to_ascii("\u{fffd}").is_err());
assert!(to_ascii("GOO\u{a0}goo.com").is_err());
}
#[test]
fn mapping_examples() {
assert_eq!(to_ascii("Go.com").unwrap(), "go.com");
assert_eq!(to_ascii("www.foo。bar.com").unwrap(), "www.foo.bar.com");
assert_eq!(
to_ascii("GOO\u{200b}\u{2060}\u{feff}goo.com").unwrap(),
"googoo.com"
);
assert_eq!(to_ascii("a\u{ad}b").unwrap(), "ab");
assert_eq!(to_ascii("loC𝐀𝐋𝐇𝐨𝐬𝐭").unwrap(), "localhost");
}
}