use alloc::borrow::Cow;
use alloc::string::String;
use alloc::vec::Vec;
use unicode_normalization::UnicodeNormalization;
use unicode_normalization::char::canonical_combining_class;
use super::percent::AppendBuf;
use super::unicode_ranges::{
is_arabic_ext_b_compatible_cp, is_arabic_ext_b_era_mark_cp, is_arabic_ext_b_era_ok_cp,
is_arabic_ext_b_letter_cp, is_bidi_ignored_mark, is_bidi_number_cp, is_disallowed_cp,
is_joining_letter_cp, is_legacy_arabic_cp, is_ltr_letter_block, is_rtl_alphabetic_block,
is_rtl_non_letter, is_uts46_ignored_cp, maps_nfkc_via_space_cp, uts46_needs_map_cp,
};
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);
}
let mut out = String::with_capacity(domain.len().saturating_add(16));
to_ascii_into(domain, &mut out)?;
if out.is_empty() || contains_forbidden_domain_code_point(&out) {
return Err(());
}
Ok(Cow::Owned(out))
}
pub(crate) fn to_ascii_into(domain: &str, out: &mut impl AppendBuf) -> Result<(), ()> {
debug_assert!(!domain.is_empty());
debug_assert!(!domain.is_ascii());
match scan_non_ascii(domain)? {
Scan::SimpleIdentity => encode_domain_labels_simple(domain, out)?,
Scan::Identity => encode_domain_labels(domain, out)?,
Scan::NeedsMap => {
let mapped = uts46_map_checked(domain)?;
if mapped.is_empty() {
return Err(());
}
encode_domain_labels(&mapped, out)?;
}
}
Ok(())
}
pub(crate) fn to_ascii_append_validated(
domain: &str,
out: &mut super::serialization::SerializationBuf<'_>,
) -> Result<(), ()> {
if domain.is_empty() {
return Err(());
}
let start = out.len();
if domain.is_ascii() {
if domain.bytes().all(|b| !b.is_ascii_uppercase()) {
if contains_forbidden_domain_code_point(domain) {
return Err(());
}
out.push_str(domain);
} else {
let lower = domain.to_ascii_lowercase();
if contains_forbidden_domain_code_point(&lower) {
return Err(());
}
out.push_str(&lower);
}
} else if to_ascii_into(domain, out).is_err() {
out.truncate(start);
return Err(());
} else {
let written = &out.as_str()[start..];
if written.is_empty() || contains_forbidden_domain_code_point(written) {
out.truncate(start);
return Err(());
}
}
Ok(())
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum Scan {
SimpleIdentity,
Identity,
NeedsMap,
}
fn scan_non_ascii(domain: &str) -> Result<Scan, ()> {
let mut needs_map = false;
let mut all_simple = true;
for c in domain.chars() {
if is_disallowed_idna(c) {
return Err(());
}
if c.is_ascii() {
if c.is_ascii_uppercase() {
needs_map = true;
all_simple = false;
}
continue;
}
if is_simple_identity_cp(c) {
continue;
}
all_simple = false;
if !needs_map && char_needs_uts46_map(c) {
needs_map = true;
}
}
Ok(if needs_map {
Scan::NeedsMap
} else if all_simple {
Scan::SimpleIdentity
} else {
Scan::Identity
})
}
#[inline]
fn is_simple_identity_cp(c: char) -> bool {
let u = c as u32;
if (0x00E0..=0x00F6).contains(&u) || (0x00F8..=0x00FF).contains(&u) || u == 0x00DF {
return true;
}
if (0x3400..=0x4DBF).contains(&u) || (0x4E00..=0x9FFF).contains(&u) {
return true;
}
if (0x3041..=0x3096).contains(&u)
|| (0x309B..=0x309F).contains(&u)
|| (0x30A0..=0x30FF).contains(&u)
{
return true;
}
(0xAC00..=0xD7A3).contains(&u)
}
#[inline]
fn char_needs_uts46_map(c: char) -> bool {
if uts46_needs_map_cp(c) || maps_nfkc_via_space(c) || letterlike_nfkc_ascii(c).is_some() {
return true;
}
if c.is_ascii() {
return c.is_ascii_uppercase();
}
if is_simple_identity_cp(c) {
return false;
}
let u = c as u32;
if (0x00C0..=0x00D6).contains(&u) || (0x00D8..=0x00DE).contains(&u) {
return true;
}
let mut has_ypogegrammeni = false;
unicode_normalization::char::decompose_canonical(c, |d| {
if d == '\u{0345}' {
has_ypogegrammeni = true;
}
});
if has_ypogegrammeni {
return true;
}
let mut it = c.nfkc();
match (it.next(), it.next()) {
(Some(ch), None) if ch == c => needs_unicode_case_fold(c),
_ => true, }
}
#[inline]
fn needs_unicode_case_fold(c: char) -> bool {
let u = c as u32;
if (0x00E0..=0x00F6).contains(&u) || (0x00F8..=0x00FF).contains(&u) || u == 0x00DF {
return false;
}
if (0x00C0..=0x00D6).contains(&u) || (0x00D8..=0x00DE).contains(&u) {
return true;
}
let mut iter = c.to_lowercase();
match (iter.next(), iter.next()) {
(Some(l), None) => l != c,
_ => true,
}
}
#[inline]
fn is_disallowed_idna(c: char) -> bool {
if is_disallowed_cp(c) {
return true;
}
let u = c as u32;
if matches!(u >> 16, 4..=13) {
return true;
}
(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 label_fails_check_bidi(label: &str) -> bool {
let mut has_rtl = false;
let mut has_ltr = false;
for c in label.chars() {
if is_bidi_transparent(c) {
continue;
}
if is_rtl_script(c) {
has_rtl = true;
} else if is_ltr_letter(c) {
has_ltr = true;
}
if has_rtl && has_ltr {
return true;
}
}
if has_rtl && !has_ltr {
let mut chars = label.chars();
let first = chars.next();
let last_strong = label.chars().rev().find(|c| !is_bidi_transparent(*c));
let last_ok = last_strong.is_some_and(|c| is_rtl_script(c) || is_bidi_number(c));
if !first.is_some_and(is_rtl_script) || !last_ok {
return true;
}
}
false
}
#[inline]
fn is_bidi_transparent(c: char) -> bool {
is_bidi_ignored_mark(c) || matches!(c, '\u{200C}' | '\u{200D}')
}
#[inline]
fn is_bidi_number(c: char) -> bool {
c.is_ascii_digit() || is_bidi_number_cp(c)
}
#[inline]
fn is_rtl_script(c: char) -> bool {
if is_bidi_ignored_mark(c) {
return false;
}
if is_rtl_non_letter(c) {
return true;
}
c.is_alphabetic() && is_rtl_alphabetic_block(c)
}
fn label_fails_arabic_ext_b_mix(label: &str) -> bool {
let mut has_ext_b = false;
let mut has_incompatible = false;
for c in label.chars() {
if is_bidi_transparent(c) {
continue;
}
if is_arabic_ext_b_letter(c) {
has_ext_b = true;
continue;
}
if c == '\u{0888}' {
has_ext_b = true;
continue;
}
if is_arabic_ext_b_compatible(c) || is_bidi_number(c) || c.is_ascii() {
continue;
}
if is_rtl_script(c) || is_legacy_arabic_letter(c) || is_ltr_letter(c) {
has_incompatible = true;
}
if has_ext_b && has_incompatible {
return true;
}
}
has_ext_b && has_incompatible
}
#[inline]
fn is_arabic_ext_c_letter(c: char) -> bool {
matches!(c, '\u{10EC2}'..='\u{10EC7}')
}
fn label_fails_arabic_ext_c_mix(label: &str) -> bool {
let mut has_ext_c = false;
let mut has_incompatible = false;
for c in label.chars() {
if is_bidi_transparent(c) {
continue;
}
if is_arabic_ext_c_letter(c) {
has_ext_c = true;
continue;
}
if is_arabic_ext_b_letter(c)
|| c == '\u{0888}'
|| is_arabic_ext_b_compatible(c)
|| is_bidi_number(c)
|| c.is_ascii()
|| is_ltr_letter(c)
{
continue;
}
if is_rtl_script(c) || is_legacy_arabic_letter(c) {
has_incompatible = true;
}
if has_ext_c && has_incompatible {
return true;
}
}
has_ext_c && has_incompatible
}
#[inline]
fn is_arabic_ext_b_letter(c: char) -> bool {
is_arabic_ext_b_letter_cp(c)
}
#[inline]
fn is_arabic_ext_b_compatible(c: char) -> bool {
is_arabic_ext_b_compatible_cp(c)
}
#[inline]
fn is_arabic_ext_b_era_mark(c: char) -> bool {
is_arabic_ext_b_era_mark_cp(c)
}
fn label_fails_ext_b_era_mark_mix(label: &str) -> bool {
if !label.chars().any(is_arabic_ext_b_era_mark) {
return false;
}
for c in label.chars() {
if is_bidi_transparent(c) || is_arabic_ext_b_era_mark(c) {
continue;
}
if is_arabic_ext_b_letter(c)
|| c == '\u{0888}'
|| is_arabic_ext_b_era_ok_cp(c)
|| is_bidi_number(c)
|| c.is_ascii()
|| is_ltr_letter(c)
{
continue;
}
if is_rtl_script(c) || is_legacy_arabic_letter(c) {
return true;
}
}
false
}
#[inline]
fn is_legacy_arabic_letter(c: char) -> bool {
is_legacy_arabic_cp(c)
}
fn label_fails_check_joiners(label: &str) -> bool {
if !label_may_contain_zwj_or_zwnj(label) {
return false;
}
let chars: Vec<char> = label.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c == '\u{200D}' {
match chars.get(i.wrapping_sub(1)).filter(|_| i > 0) {
Some(&b) if canonical_combining_class(b) == 9 => {}
_ => return true,
}
continue;
}
if c != '\u{200C}' {
continue;
}
let before = chars[..i].iter().rev().find(|c| !is_bidi_transparent(**c));
let after = chars[i + 1..].iter().find(|c| !is_bidi_transparent(**c));
match (before, after) {
(Some(&b), Some(&a)) if is_joining_letter(b) && is_joining_letter(a) => {}
_ => return true,
}
}
false
}
#[inline]
fn label_may_contain_zwj_or_zwnj(label: &str) -> bool {
let bytes = label.as_bytes();
let mut i = 0;
while i + 2 < bytes.len() {
if bytes[i] == 0xE2 && bytes[i + 1] == 0x80 && matches!(bytes[i + 2], 0x8C | 0x8D) {
return true;
}
i += 1;
}
false
}
#[inline]
fn is_joining_letter(c: char) -> bool {
c.is_alphabetic() && is_joining_letter_cp(c)
}
#[inline]
fn is_ltr_letter(c: char) -> bool {
c.is_alphabetic() && (c.is_ascii_alphabetic() || is_ltr_letter_block(c))
}
fn encode_domain_labels_simple(mapped: &str, out: &mut impl AppendBuf) -> Result<(), ()> {
let mut first = true;
for label in mapped.split('.') {
if !first {
out.push('.');
}
first = false;
if label.is_empty() {
continue;
}
if label.is_ascii() {
out.push_str(label);
} else {
out.push_str("xn--");
encode_punycode(label, out)?;
}
}
Ok(())
}
fn encode_domain_labels(mapped: &str, out: &mut impl AppendBuf) -> Result<(), ()> {
let mut first = true;
for label in mapped.split('.') {
if !first {
out.push('.');
}
first = false;
if label.is_empty() {
continue;
}
let nfc_owned;
let label = if unicode_normalization::is_nfc(label) {
label
} else {
nfc_owned = label.nfc().collect::<String>();
nfc_owned.as_str()
};
if label.chars().next().is_some_and(is_bidi_ignored_mark) {
return Err(());
}
if label_fails_check_bidi(label) {
return Err(());
}
if label_fails_arabic_ext_b_mix(label) {
return Err(());
}
if label_fails_arabic_ext_c_mix(label) {
return Err(());
}
if label_fails_ext_b_era_mark_mix(label) {
return Err(());
}
if label_fails_check_joiners(label) {
return Err(());
}
if label.is_ascii() {
out.push_str(label);
} else {
out.push_str("xn--");
encode_punycode(label, out)?;
}
}
Ok(())
}
fn uts46_map_checked(input: &str) -> Result<String, ()> {
let mut out = String::with_capacity(input.len());
for c in input.chars() {
map_char(c, &mut out);
}
if out.is_empty()
|| out.chars().any(is_disallowed_idna)
|| contains_forbidden_domain_code_point(&out)
{
return Err(());
}
Ok(out)
}
fn map_char(c: char, out: &mut String) {
if is_uts46_ignored_cp(c) {
return;
}
match c {
'\u{2000}'..='\u{200A}' => out.push(' '),
'\u{2011}' => out.push('\u{2010}'),
'\u{3002}' | '\u{FF61}' => out.push('.'),
'\u{1C80}' => out.push('\u{0432}'), '\u{1C81}' => out.push('\u{0434}'), '\u{1C82}' => out.push('\u{043E}'), '\u{1C83}' => out.push('\u{0441}'), '\u{1C84}' | '\u{1C85}' => out.push('\u{0442}'), '\u{1C86}' => out.push('\u{044A}'), '\u{1C87}' => out.push('\u{0463}'), '\u{1C88}' => out.push('\u{A64B}'), '\u{1C89}' => out.push('\u{1C8A}'),
'\u{13F8}' => out.push('\u{13F0}'),
'\u{13F9}' => out.push('\u{13F1}'),
'\u{13FA}' => out.push('\u{13F2}'),
'\u{13FB}' => out.push('\u{13F3}'),
'\u{13FC}' => out.push('\u{13F4}'),
'\u{13FD}' => out.push('\u{13F5}'),
c @ '\u{AB70}'..='\u{ABBF}' => {
out.push(char::from_u32(0x13A0 + (c as u32 - 0xAB70)).unwrap_or(c));
}
'\u{203E}' => {
out.push(' ');
out.push('\u{0305}');
}
c if maps_nfkc_via_space(c) => {
for ch in c.nfkc() {
out.push(ch);
}
}
'\u{00BC}'..='\u{00BE}' | '\u{2150}'..='\u{2189}' => {
for ch in c.nfkc() {
for folded in ch.to_lowercase() {
if folded.is_ascii() {
out.push(folded.to_ascii_lowercase());
} else {
out.push(folded);
}
}
}
}
'\u{00B2}' | '\u{00B3}' | '\u{00B9}' | '\u{2070}'..='\u{209F}' => {
for ch in c.nfkc() {
out.push(ch);
}
}
'\u{0345}' => out.push('\u{03B9}'),
'\u{03F2}' => out.push('\u{03C3}'),
'\u{1D6D3}' | '\u{1D70D}' | '\u{1D747}' | '\u{1D781}' | '\u{1D7BB}' => out.push('\u{03C3}'),
'\u{00AA}'
| '\u{00BA}'
| '\u{01C4}'..='\u{01CC}'
| '\u{03D0}'..='\u{03D7}'
| '\u{03F0}'..='\u{03F1}'
| '\u{03F4}'..='\u{03F5}' => {
for ch in c.nfkc() {
for folded in ch.to_lowercase() {
if folded.is_ascii() {
out.push(folded.to_ascii_lowercase());
} else {
out.push(folded);
}
}
}
}
'\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 letterlike_nfkc_ascii(c).is_some() => {
let mapped = letterlike_nfkc_ascii(c).unwrap();
for ch in mapped.chars() {
out.push(ch.to_ascii_lowercase());
}
}
c if c.is_ascii() => out.push(c.to_ascii_lowercase()),
c if {
let u = c as u32;
(0x00E0..=0x00F6).contains(&u) || (0x00F8..=0x00FF).contains(&u) || u == 0x00DF
} =>
{
out.push(c);
}
c if {
let u = c as u32;
(0x00C0..=0x00D6).contains(&u) || (0x00D8..=0x00DE).contains(&u)
} =>
{
out.push(char::from_u32((c as u32) + 0x20).unwrap_or(c));
}
c => {
for ch in c.nfkc() {
if matches!(ch, '\u{13A0}'..='\u{13F5}') {
out.push(ch);
continue;
}
if ch == '\u{3002}' {
out.push('.');
continue;
}
let nfd: String = ch.nfd().collect();
if nfd.contains('\u{0345}') {
for d in nfd.chars() {
if d == '\u{0345}' {
out.push('\u{03B9}');
} else {
for folded in d.to_lowercase() {
if folded.is_ascii() {
out.push(folded.to_ascii_lowercase());
} else {
out.push(folded);
}
}
}
}
continue;
}
for folded in ch.to_lowercase() {
if folded.is_ascii() {
out.push(folded.to_ascii_lowercase());
} else {
out.push(folded);
}
}
}
}
}
}
#[inline]
fn maps_nfkc_via_space(c: char) -> bool {
maps_nfkc_via_space_cp(c)
}
#[inline]
fn letterlike_nfkc_ascii(c: char) -> Option<&'static str> {
Some(match c {
'\u{2100}' => "a/c",
'\u{2101}' => "a/s",
'\u{2102}' => "C",
'\u{2105}' => "c/o",
'\u{2106}' => "c/u",
'\u{210A}' => "g",
'\u{210B}' => "H",
'\u{210C}' => "H",
'\u{210D}' => "H",
'\u{210E}' => "h",
'\u{2110}' => "I",
'\u{2111}' => "I",
'\u{2112}' => "L",
'\u{2113}' => "l",
'\u{2115}' => "N",
'\u{2116}' => "No",
'\u{2119}' => "P",
'\u{211A}' => "Q",
'\u{211B}' => "R",
'\u{211C}' => "R",
'\u{211D}' => "R",
'\u{2120}' => "SM",
'\u{2121}' => "TEL",
'\u{2122}' => "TM",
'\u{2124}' => "Z",
'\u{2128}' => "Z",
'\u{212A}' => "K",
'\u{212C}' => "B",
'\u{212D}' => "C",
'\u{212F}' => "e",
'\u{2130}' => "E",
'\u{2131}' => "F",
'\u{2133}' => "M",
'\u{2134}' => "o",
'\u{2139}' => "i",
'\u{213B}' => "FAX",
'\u{2145}' => "D",
'\u{2146}' => "d",
'\u{2147}' => "e",
'\u{2148}' => "i",
'\u{2149}' => "j",
_ => return None,
})
}
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;
const STACK_LABEL_CHARS: usize = 64;
const MAX_ACE_OCTETS: usize = 4096;
const MAX_LABEL_CODE_POINTS: usize = 2048;
fn encode_punycode(input: &str, out: &mut impl AppendBuf) -> Result<(), ()> {
#![allow(clippy::many_single_char_names)] let mut stack_ace = [0u8; 128];
let mut stack_len = 0usize;
let mut heap_ace: Option<Vec<u8>> = None;
let push_ascii = |stack: &mut [u8; 128],
stack_len: &mut usize,
heap: &mut Option<Vec<u8>>,
b: u8|
-> Result<(), ()> {
if let Some(v) = heap {
if v.len() >= MAX_ACE_OCTETS {
return Err(());
}
v.push(b);
return Ok(());
}
if *stack_len < stack.len() {
stack[*stack_len] = b;
*stack_len += 1;
return Ok(());
}
let mut v = Vec::with_capacity(256);
v.extend_from_slice(&stack[..*stack_len]);
v.push(b);
*heap = Some(v);
Ok(())
};
let mut n = INITIAL_N;
let mut delta: u32 = 0;
let mut bias = INITIAL_BIAS;
let mut stack = [0u32; STACK_LABEL_CHARS];
let mut heap: Vec<u32> = Vec::new();
let mut len = 0usize;
for c in input.chars() {
let cp = c as u32;
if len < STACK_LABEL_CHARS && heap.is_empty() {
stack[len] = cp;
len += 1;
} else {
if heap.is_empty() {
heap.reserve(len + 8);
heap.extend_from_slice(&stack[..len]);
}
heap.push(cp);
len += 1;
}
}
if len > MAX_LABEL_CODE_POINTS {
return Err(());
}
let chars: &[u32] = if heap.is_empty() {
&stack[..len]
} else {
&heap
};
let mut basic_count = 0u32;
for &cp in chars {
if cp < 0x80 {
push_ascii(
&mut stack_ace,
&mut stack_len,
&mut heap_ace,
(cp as u8).to_ascii_lowercase(),
)?;
basic_count += 1;
}
}
let mut h = basic_count;
let b = basic_count;
if b > 0 {
push_ascii(&mut stack_ace, &mut stack_len, &mut heap_ace, b'-')?;
}
while (h as usize) < chars.len() {
let mut m = u32::MAX;
for &cp in chars {
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 &cp in chars {
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));
push_ascii(
&mut stack_ace,
&mut stack_len,
&mut heap_ace,
digit_to_byte(code)?,
)?;
q = (q - t) / (BASE - t);
k = k.checked_add(BASE).ok_or(())?;
}
push_ascii(
&mut stack_ace,
&mut stack_len,
&mut heap_ace,
digit_to_byte(q)?,
)?;
bias = adapt(delta, h + 1, h == b);
delta = 0;
h += 1;
}
}
delta = delta.checked_add(1).ok_or(())?;
n = n.checked_add(1).ok_or(())?;
}
let ace_str = match &heap_ace {
Some(v) => core::str::from_utf8(v).map_err(|_| ())?,
None => core::str::from_utf8(&stack_ace[..stack_len]).map_err(|_| ())?,
};
out.push_str(ace_str);
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_byte(d: u32) -> Result<u8, ()> {
match d {
0..=25 => Ok(b'a' + d as u8),
26..=35 => Ok(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 long_ace_label_beyond_former_stack_cap() {
let myanmar = '\u{100b}';
let label = format!("x{myanmar}n--{}wssf", "L".repeat(120));
let ace = to_ascii(&label).expect("long ACE must succeed under beStrict=false");
assert!(ace.starts_with("xn--"));
assert!(
ace.len() > 128,
"ACE len {} should exceed old 128 cap",
ace.len()
);
}
#[test]
fn label_over_former_256_code_point_cap() {
let label = format!("é{}", "a".repeat(260));
let ace = to_ascii(&label).expect(">256 code points allowed under beStrict=false");
assert!(ace.starts_with("xn--"));
}
#[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");
assert_eq!(to_ascii("\u{210B}").unwrap(), "h");
assert!(to_ascii("ےў").is_err());
assert!(to_ascii("-\u{068B}").is_err());
assert!(to_ascii("\u{d71cb}").is_err());
assert!(to_ascii("\u{203E}").is_err());
assert_eq!(to_ascii("sw*").unwrap(), "sw*");
}
#[test]
fn identity_turkish_bench_label() {
assert_eq!(to_ascii("türkçe.com").unwrap(), "xn--trke-2oa7j.com");
assert_eq!(scan_non_ascii("türkçe.com"), Ok(Scan::SimpleIdentity));
assert_eq!(scan_non_ascii("你好你好.com"), Ok(Scan::SimpleIdentity));
assert_ne!(scan_non_ascii("a\u{0301}.com"), Ok(Scan::SimpleIdentity));
}
#[test]
fn to_ascii_into_matches_to_ascii() {
let domain = "türkçe.com";
let mut buf = String::new();
to_ascii_into(domain, &mut buf).unwrap();
assert_eq!(buf, to_ascii(domain).unwrap());
}
}
#[cfg(test)]
mod triage_nsm {
use super::*;
#[test]
fn arabic_nsm_with_latin_ok() {
let domain = "ex\u{613}";
assert!(!domain.is_ascii());
let scan = scan_non_ascii(domain);
assert!(scan.is_ok(), "scan={scan:?}");
println!("scan={:?}", scan.unwrap());
assert!(!label_fails_check_bidi(domain), "bidi failed");
let mut buf = String::new();
let enc = encode_domain_labels(domain, &mut buf);
assert!(enc.is_ok(), "encode={enc:?} buf={buf}");
println!("encoded={buf}");
let r = to_ascii(domain);
assert!(r.is_ok(), "to_ascii={r:?}");
}
}
#[cfg(test)]
mod tick_fixes {
use super::*;
#[test]
fn nfc_cedilla() {
assert_eq!(to_ascii("--tabout\u{327}").unwrap(), "xn----tabou-drb");
}
#[test]
fn leading_mark_rejected() {
assert!(to_ascii("\u{34b}g").is_err());
assert!(to_ascii("\u{5c7}e").is_err());
assert!(to_ascii("\u{5cc}sw*").is_err());
}
#[test]
fn nfkc_fractions_and_space_compat() {
assert_eq!(to_ascii("sst\u{00bc}r").unwrap(), "xn--sst14r-sq0c");
assert_eq!(to_ascii("h\u{215b}1").unwrap(), "xn--h181-1g7a");
assert!(to_ascii("h\u{00a8}J").is_err());
assert!(to_ascii("z\u{0385}").is_err());
assert!(to_ascii("0w\u{02d9}\u{043f}.").is_err());
assert!(to_ascii("8888888888888\u{202b}out8B88HTTP").is_err());
assert!(to_ascii("+t\u{00fc}M\u{008e}=1\u{00e7}.com").is_err());
assert_eq!(to_ascii("\u{01ca}s").unwrap(), "njs");
assert_eq!(to_ascii("h\u{2011}").unwrap(), "xn--h-0gn");
assert_eq!(to_ascii("\u{03d2}").unwrap(), "xn--6xa");
assert_eq!(to_ascii("2e2\u{00aa}dta0").unwrap(), "2e2adta0");
assert!(to_ascii("c\u{e691}").is_err());
assert!(to_ascii("\u{200d}b").is_err());
assert_eq!(
to_ascii("255.255.255.2\u{2075}85").unwrap(),
"255.255.255.2585"
);
assert_eq!(to_ascii("\u{587}7\u{187}7f").unwrap(), "xn--77f-h4a750c8e");
assert_eq!(to_ascii("7\u{2230},").unwrap(), "xn--7,-cevaa");
assert_eq!(to_ascii("\u{314f}ile").unwrap(), "xn--ile-3do");
assert_eq!(to_ascii("\u{6de}e").unwrap(), "xn--e-fxc");
assert!(to_ascii("\u{6de}").is_ok(), "{:?}", to_ascii("\u{6de}"));
assert_eq!(to_ascii("\u{7a3}\u{7aa}").unwrap(), "xn--hrbo");
assert_eq!(to_ascii("\u{7ff}\u{5e8}").unwrap(), "xn--teb38h");
assert_eq!(to_ascii("\u{5c0}").unwrap(), "xn--odb");
assert!(to_ascii("0x=7f00\u{248b}ac").is_err());
assert_eq!(to_ascii("s\u{3f2}x").unwrap(), "xn--sx-vbc");
assert_eq!(to_ascii("c\u{3c2}").unwrap(), "xn--c-ymb"); assert_eq!(to_ascii("D\u{7fd}").unwrap(), "xn--d-ued");
assert_eq!(to_ascii("\u{7ff}\u{7c8}").unwrap(), "xn--jsb3i");
assert_eq!(to_ascii("a\u{1160}b").unwrap(), "ab");
assert_eq!(to_ascii("5x\u{345}fl").unwrap(), "xn--5xfl-6md");
}
#[test]
fn unassigned_greek() {
assert!(to_ascii("s\u{38d}w").is_err());
}
}
#[cfg(test)]
mod tick18 {
use super::*;
#[test]
fn cases() {
assert_eq!(
to_ascii("\u{5c6}\u{7fd}\u{5c7}\u{6fd}").unwrap(),
"xn--udbc44gi3a"
);
assert_eq!(
to_ascii("87\u{116e}\u{13a5}\u{116b}B").unwrap(),
"xn--87b-bfoo615a"
);
}
}
#[cfg(test)]
mod tick19 {
use super::*;
#[test]
fn check_bidi_skips_cyrillic_marks() {
assert_eq!(to_ascii("\u{5c0}\u{484}\u{5c5}").unwrap(), "xn--n3a54dpa");
}
#[test]
fn greek_ypogegrammeni_expands() {
assert_eq!(
to_ascii("\u{1940}DXXE\u{1c80}").unwrap(),
"xn--dxxe-j4d9443a"
);
assert_eq!(to_ascii("\u{13fa}").unwrap(), "xn--ice");
assert_eq!(to_ascii("\u{1c89}").unwrap(), "xn--d4f");
assert_eq!(to_ascii("\u{1fc4}\u{1c89}").unwrap(), "xn--jxaw371q");
assert_eq!(to_ascii("\u{fd9e}\u{ff9e}").unwrap(), "xn--ngbm8fw67r");
assert_eq!(to_ascii("\u{ffa0}.").unwrap(), ".");
assert!(to_ascii("\u{ffa0}").is_err()); assert_eq!(
to_ascii("\u{700}\u{1dc0}\u{1dc0}\u{1dc0}").unwrap(),
"xn--tmb267iaa"
);
assert_eq!(to_ascii("\u{069c}\u{1032}").unwrap(), "xn--yjb220c");
assert_eq!(
to_ascii("\u{079c}\u{065c}\u{0658}\u{0a4b}").unwrap(),
"xn--0hbh37gu8e"
);
assert_eq!(
to_ascii("\u{079c}\u{065e}\u{065c}\u{0658}\u{0902}").unwrap(),
"xn--0hbhg48jizc"
);
assert_eq!(
to_ascii("vvvvv\u{fb1e}\u{fb1e}").unwrap(),
"xn--vvvvv-xi82aa"
);
assert_eq!(to_ascii("f\u{fb1e}\u{ff9e}").unwrap(), "xn--f-wdu9988e");
assert_eq!(
to_ascii("\u{079c}\u{065c}\u{1886}").unwrap(),
"xn--4hb35dg58a"
);
assert_eq!(to_ascii("\u{080f}2\u{09c1}").unwrap(), "xn--2-ufd71k");
assert_eq!(to_ascii("\u{079c}\u{0861}").unwrap(), "xn--9qb80b");
assert_eq!(
to_ascii("\u{0642}\u{065c}\u{200c}\u{0642}\u{065c}").unwrap(),
"xn--ehba1fb2993a"
);
assert!(to_ascii("a\u{200c}b").is_err());
assert_eq!(to_ascii("\u{0629}\u{0c81}").unwrap(), "xn--ogb263a");
assert_eq!(to_ascii("\u{0869}\u{0c81}").unwrap(), "xn--4wb01s");
assert_eq!(to_ascii("\u{088f}").unwrap(), "xn--7xb");
assert!(to_ascii("\u{088f}\u{084f}").is_err()); assert!(to_ascii("\u{0890}").is_err());
assert!(to_ascii("\u{1c8b}").is_err());
assert!(to_ascii("\u{079c}\u{08d2}").is_err()); assert!(to_ascii("AGAAA.\u{33c2}\u{3002}DFx").is_err());
assert_eq!(to_ascii("\u{1940}DE\u{1fc4}").unwrap(), "xn--de-38b8b108q");
}
}
#[cfg(test)]
mod tick20 {
use super::*;
#[test]
fn division_not_ltr() {
assert_eq!(to_ascii("\u{624}\u{f7}0001").unwrap(), "xn--0001-9qa360f");
assert!(to_ascii("2a\u{2026}7g").is_err());
}
}
#[cfg(test)]
mod tick21 {
use super::*;
#[test]
fn check_bidi_tibetan_and_ext_marks() {
assert_eq!(to_ascii("\u{079c}\u{0f8f}").unwrap(), "xn--9qb481b");
assert_eq!(to_ascii("\u{0700}\u{1aba}").unwrap(), "xn--tmb412h");
assert!(to_ascii("\u{0798}\u{1acf}").is_err());
assert_eq!(to_ascii("\u{1acf}").unwrap(), "xn--prf");
assert!(to_ascii("\u{0798}\u{1ace}").is_err()); }
}
#[cfg(test)]
mod tick22 {
use super::*;
#[test]
fn vedic_thai_rejang_and_syriac_zwnj() {
assert_eq!(to_ascii("\u{0704}\u{1ce7}").unwrap(), "xn--xmb023i");
assert_eq!(to_ascii("\u{069c}\u{0e49}").unwrap(), "xn--yjb440b");
assert_eq!(
to_ascii("\u{0642}\u{065c}\u{200c}\u{0722}").unwrap(),
"xn--ehb6cs6ah11f"
);
assert_eq!(
to_ascii("\u{079c}\u{065e}\u{a94f}").unwrap(),
"xn--6hb94dg266a"
);
assert!(to_ascii("\u{079c}\u{1ce1}").is_err()); assert!(to_ascii("\u{05d0}\u{200c}\u{0642}").is_err()); }
}
#[cfg(test)]
mod tick23 {
use super::*;
#[test]
fn vedic_nihshvasa_and_cherokee_supplement() {
assert_eq!(to_ascii("\u{1cd3}").unwrap(), "xn--g6f");
assert!(to_ascii("\u{1cd0}").is_err()); assert_eq!(to_ascii("\u{ab72}").unwrap(), "xn--78d");
assert_eq!(to_ascii("\u{13a2}").unwrap(), "xn--78d");
assert_eq!(to_ascii("\u{ab70}").unwrap(), "xn--58d");
assert_eq!(
to_ascii("\u{069c}\u{0e49}\u{0ec9}").unwrap(),
"xn--yjb440b8k"
); assert_eq!(to_ascii("\u{069c}\u{0d81}").unwrap(), "xn--yjb446a");
assert_eq!(to_ascii("\u{069c}\u{0dd4}").unwrap(), "xn--yjb018a");
assert_eq!(to_ascii("\u{0704}\u{1a77}").unwrap(), "xn--xmb270h"); assert_eq!(
to_ascii("\u{0704}\u{0704}\u{1ce7}\u{2de7}").unwrap(),
"xn--xmba329nnrp"
);
assert_eq!(to_ascii("\u{0704}\u{1b37}").unwrap(), "xn--xmb654h");
assert_eq!(to_ascii("\u{0700}\u{1b3a}").unwrap(), "xn--tmb074h");
assert_eq!(to_ascii("\u{0851}=\u{083c}").unwrap(), "xn--=-gid5d");
assert_eq!(
to_ascii("\u{062c}\u{1c33}\u{061a}\u{1b73}").unwrap(),
"xn--8fb9a697i5va"
); assert!(to_ascii("\u{088f}\u{fb9e}").is_err()); assert_eq!(to_ascii("\u{069c}\u{1bef}").unwrap(), "xn--yjb230i"); assert_eq!(to_ascii("\u{069c}\u{1bed}").unwrap(), "xn--yjb820i");
assert_eq!(to_ascii("\u{0834}\u{1b80}").unwrap(), "xn--mvb499g"); assert_eq!(
to_ascii("\u{08b5}\u{aac1}\u{0ac1}").unwrap(),
"xn--bzb26h302y"
); assert!(to_ascii("\u{0868}\u{08d1}\u{0a51}").is_err()); assert!(to_ascii("\u{17b4}").is_err()); assert_eq!(to_ascii("\u{0624}\u{17b4}").unwrap(), "xn--jgb");
assert_eq!(to_ascii("\u{0851}\u{a9bc}").unwrap(), "xn--gwb0673f"); assert_eq!(
to_ascii("\u{0704}\u{1773}\u{1cdd}").unwrap(),
"xn--xmb825f25d"
); assert_eq!(to_ascii("\u{0851}\u{aaf6}").unwrap(), "xn--gwb8834f");
assert_eq!(
to_ascii("\u{0624}\u{abed}\u{1bed}").unwrap(),
"xn--jgb862i025h"
);
assert_eq!(to_ascii("\u{0851}\u{aa36}").unwrap(), "xn--gwb4004f"); assert_eq!(to_ascii("\u{0851}\u{a8f0}").unwrap(), "xn--gwb2533f"); assert_eq!(
to_ascii("\u{10c9d}\u{05c7}\u{05dd}\u{10cb2}").unwrap(),
"xn--vdb8b4637jqca"
);
assert_eq!(to_ascii("\u{10b1d}\u{10a72}").unwrap(), "xn--hu9cll");
assert_eq!(to_ascii("\u{0704}\u{1713}").unwrap(), "xn--xmb633f");
assert_eq!(to_ascii("\u{10c32}\u{11c32}").unwrap(), "xn--969c66y"); assert_eq!(to_ascii("\u{0851}\u{a80b}").unwrap(), "xn--gwb4982f"); assert_eq!(to_ascii("\u{10e9d}\u{10e72}").unwrap(), "xn--qo0dzc"); assert_eq!(to_ascii("\u{10810}&0\u{11c32}").unwrap(), "xn--&0-li9nk85d");
assert_eq!(to_ascii("\u{109f2}\u{109b2}").unwrap(), "xn--zo9che");
assert_eq!(to_ascii("\u{10a1d}\u{1089d}").unwrap(), "xn--2g9crz");
assert!(to_ascii("\u{10a85}\u{10a85}\u{10945}").is_err());
assert_eq!(to_ascii("\u{10b0b}\u{10f1d}").unwrap(), "xn--uy9co3e"); assert_eq!(
to_ascii("\u{0624}\u{1bed}\u{a92d}").unwrap(),
"xn--jgb862ip43h"
); assert_eq!(to_ascii("\u{10b1d}\u{10a9d}").unwrap(), "xn--pv9cpi"); assert_eq!(
to_ascii("\u{10a9d}\u{10e9d}.\u{10e9d}\u{10e9d}.\u{10cb2}").unwrap(),
"xn--pv9ci2e.xn--yp0da.xn--rd0d"
);
assert_eq!(to_ascii("\u{10a85}\u{11a05}").unwrap(), "xn--0u9cq8x"); assert_eq!(
to_ascii("\u{10810}0\u{1143a}\u{11c3a}").unwrap(),
"xn--0-tf4im51ac5h"
); assert_eq!(to_ascii("\u{10a72}\u{10af2}").unwrap(), "xn--hu9cqi"); assert!(to_ascii("\u{10a85}\u{10ec5}").is_err()); assert_eq!(to_ascii("\u{10877}\u{10872}").unwrap(), "xn--uf9cja"); assert_eq!(to_ascii("\u{10b72}\u{10b79}").unwrap(), "xn--s19coa"); assert_eq!(to_ascii("\u{108af}\u{108f2}").unwrap(), "xn--lh9coe"); assert_eq!(to_ascii("\u{10c32}\u{116b2}").unwrap(), "xn--969cg3p"); assert_eq!(
to_ascii("\u{0704}\u{135d}\u{1cdd}").unwrap(),
"xn--xmb634dpmh"
); assert_eq!(to_ascii("\u{0624}\u{a9e5}").unwrap(), "xn--jgb6594f"); assert_eq!(
to_ascii("\u{0624}\u{a9e5}\u{abe5}").unwrap(),
"xn--jgb6594f5ib"
);
assert!(to_ascii("\u{10a85}\u{10d85}").is_err()); assert_eq!(to_ascii("\u{10810}0\u{11832}").unwrap(), "xn--0-tf4iw31b"); assert_eq!(to_ascii("\u{10add}\u{102e6}").unwrap(), "xn--987cr4l"); assert_eq!(to_ascii("\u{109bf}\u{1093f}").unwrap(), "xn--pl9cpi"); assert_eq!(to_ascii("\u{10810}\u{11d3a}").unwrap(), "xn--1c9cg57a"); assert_eq!(to_ascii("\u{10a85}\u{10fc5}").unwrap(), "xn--0u9cs3g"); assert_eq!(
to_ascii("\u{10810}\u{1183a}\u{1143a}\u{1103a}").unwrap(),
"xn--1c9ck3l0md96e"
); assert_eq!(to_ascii("\u{10878}\u{10cfc}").unwrap(), "xn--0f9c60f"); assert_eq!(
to_ascii("\u{10c9d}\u{10c9d}\u{11cb2}.\u{10cb2}").unwrap(),
"xn--5c0da372j.xn--rd0d"
); assert_eq!(
to_ascii("\u{0851}\u{aab8}\u{a678}").unwrap(),
"xn--gwb8802fixc"
); assert_eq!(
to_ascii("\u{10810}\u{11831}\u{1163a}\u{1143a}").unwrap(),
"xn--1c9cs1s4pbq1c"
); assert_eq!(to_ascii("\u{10910}\u{1091a}").unwrap(), "xn--dk9cua"); assert_eq!(
to_ascii("\u{0642}\u{20dc}\u{065c}\u{065c}\u{065c}\u{065c}").unwrap(),
"xn--ehb6caaa8181b"
); assert_eq!(to_ascii("\u{10a90}\u{1da9d}").unwrap(), "xn--cv9cw466a"); assert_eq!(
to_ascii("\u{10e9d}\u{10e9d}\u{11236}").unwrap(),
"xn--yp0da77y"
); assert_eq!(
to_ascii("\u{10810}\u{11d32}\u{10810}\u{10d32}").unwrap(),
"xn--1c9ca650bc5t"
); assert_eq!(
to_ascii("\u{0851}\u{a672}\u{aa36}").unwrap(),
"xn--gwb6702fwmc"
); assert_eq!(
to_ascii("\u{10810}\u{1183a}\u{1143a}\u{114ba}").unwrap(),
"xn--1c9cs1s8kar9i"
); assert!(to_ascii("\u{10810}\u{11f3a}").is_err()); assert_eq!(to_ascii("\u{1e810}\u{1e010}").unwrap(), "xn--xh4h51k"); assert_eq!(to_ascii("\u{10810}\u{115b3}").unwrap(), "xn--1c9cw6u"); assert_eq!(to_ascii("\u{fe73}\u{fe2f}").unwrap(), "xn--s96cte"); assert_eq!(to_ascii("\u{10e85}\u{11943}").unwrap(), "xn--9o0dk7p"); assert!(to_ascii("\u{10810}\u{13440}").is_err()); assert_eq!(to_ascii("\u{10f52}\u{10f50}").unwrap(), "xn--2u0dd"); assert_eq!(to_ascii("\u{0798}\u{1a18}").unwrap(), "xn--5qb685g"); assert_eq!(
to_ascii("\u{0704}\u{1922}\u{1ba2}\u{1ba2}\u{1ba2}").unwrap(),
"xn--xmb093g4ybaa"
); assert_eq!(to_ascii("\u{1e950}\u{1e810}").unwrap(), "xn--g55h5t"); assert_eq!(to_ascii("\u{10a52}\u{11a52}").unwrap(), "xn--kt9c66y"); assert_eq!(to_ascii("\u{10810}\u{11128}").unwrap(), "xn--1c9cg9m"); assert_eq!(
to_ascii("\u{10810}\u{11d32}\u{10810}\u{11372}").unwrap(),
"xn--1c9ca658fhyj"
); assert_eq!(to_ascii("\u{10e9d}\u{110b6}").unwrap(), "xn--yp0dy9a"); assert!(to_ascii("\u{fe12}").is_err()); assert!(to_ascii("\u{fe52}").is_err()); assert_eq!(to_ascii("\u{10817}\u{111b8}").unwrap(), "xn--8c9ck8n"); assert_eq!(
to_ascii("\u{10e9d}\u{114b6}\u{10e9d}\u{114b6}\u{10ead}\u{114b6}").unwrap(),
"xn--yp0da2c889abab"
); assert_eq!(to_ascii("-\u{10d25}").unwrap(), "xn----6j6i"); assert_eq!(to_ascii("\u{10810}\u{11173}").unwrap(), "xn--1c9cg4n"); assert!(to_ascii("\u{10a85}\u{10f85}").is_err()); assert_eq!(
to_ascii("..\u{6fd}\u{d4d}\u{d4d}\u{16b35}").unwrap(),
"..xn--qmb643aa81496a"
); assert_eq!(to_ascii("\u{10cb2}\u{119d6}").unwrap(), "xn--rd0d43t"); assert_eq!(to_ascii("\u{1e950}\u{1e946}").unwrap(), "xn--bf6hta"); assert_eq!(
to_ascii("\u{1e810}\u{1e020}\u{1ec90}\u{1e020}").unwrap(),
"xn--ei4ha802dyxe"
); assert!(to_ascii("\u{10e9d}\u{11b63}").is_err()); assert_eq!(
to_ascii("\u{8b5}\u{9e2}\u{9fe}\u{9e2}\u{9e2}").unwrap(),
"xn--bzb61daa2l"
); assert_eq!(to_ascii("\u{7d1}\u{aa7c}").unwrap(), "xn--ssb0044f"); assert_eq!(
to_ascii("\u{10e9d}\u{1bc9d}\u{11c92}\u{11c92}\u{11c92}").unwrap(),
"xn--yp0dg2vaa6695r"
); assert_eq!(to_ascii("\u{1e810}\u{1e8c7}").unwrap(), "xn--g55hol"); assert_eq!(
to_ascii("\u{851}\u{a8e3}\u{a8e3}\u{a8c4}\u{a8e3}").unwrap(),
"xn--gwb4623fxcaaa"
); assert_eq!(to_ascii("\u{1ec90}\u{1e8d0}").unwrap(), "xn--xb6h53d"); assert_eq!(
to_ascii("\u{1e810}\u{1e010}\u{1e8d0}\u{1e010}\u{1e010}").unwrap(),
"xn--xh4haa303fp1a"
);
assert!(to_ascii("\u{1ed1e}&\u{088c}").is_err()); assert_eq!(to_ascii("\u{856}\u{a6f1}").unwrap(), "xn--lwb0232f"); assert_eq!(
to_ascii("\u{856}\u{a6f1}\u{aa31}\u{aa31}\u{aa31}").unwrap(),
"xn--lwb0232flbcaa"
);
assert!(to_ascii("\u{1bca0}").is_err()); assert_eq!(to_ascii("\u{1bca2}tttt").unwrap(), "tttt"); assert_eq!(to_ascii("'\u{1bca2}").unwrap(), "'");
assert_eq!(
to_ascii("\u{10a94}\u{1d17d}\u{1da3d}").unwrap(),
"xn--gv9cw805a9qf"
); assert_eq!(
to_ascii("\u{10cea}\u{1da84}\u{1d244}\u{1da84}\u{1da84}").unwrap(),
"xn--jd0d8184a9ffbaa"
); assert_eq!(to_ascii("\u{7dc}\u{302a}").unwrap(), "xn--3sb057r"); assert_eq!(to_ascii("-\u{1d17a}").unwrap(), "-"); assert_eq!(
to_ascii("nx\u{11a99}\u{200d}atac").unwrap(),
"xn--nxatac-qf0ct836r"
); assert!(to_ascii("a\u{200d}b").is_err()); assert!(to_ascii("\u{1ee10}1\u{088f}").is_err()); assert_eq!(to_ascii("\u{1ec90}\u{1e131}").unwrap(), "xn--6p4h35p"); assert_eq!(to_ascii("\u{10af2}\u{16af2}").unwrap(), "xn--4x9cm25n"); assert_eq!(to_ascii("\u{fdf8}\u{fbba}").unwrap(), "xn--ygb8adj78252a"); assert_eq!(to_ascii("\u{10810}\u{11ef4}").unwrap(), "xn--1c9cu40b"); assert_eq!(
to_ascii("\u{7c1}\u{7c1}\u{1172b}\u{116ab}\u{116ab}\u{116ab}").unwrap(),
"xn--csba6719raa97e"
); assert!(to_ascii("\u{10f32}\u{1cf32}").is_err()); assert_eq!(to_ascii("\u{10828}\u{10378}").unwrap(), "xn--ge8cv6f"); assert_eq!(to_ascii("\u{108fd}\u{101fd}").unwrap(), "xn--m27cq7j"); assert!(to_ascii("\u{1e810}\u{1e6e6}").is_err()); assert_eq!(to_ascii("\u{0727}\u{2cef}").unwrap(), "xn--xnb854q");
assert_eq!(
to_ascii("\u{0798}\u{102e0}\u{112e4}").unwrap(),
"xn--5qb0779jpbk"
);
assert_eq!(to_ascii("\u{10add}\u{112e6}").unwrap(), "xn--jx9c80l");
assert_eq!(
to_ascii("\u{10a10}\u{112df}\u{11d3f}").unwrap(),
"xn--or9ck4mx1i"
);
assert_eq!(
to_ascii("n\u{11a99}\u{0327}\u{200d}n").unwrap(),
"xn--n-lla157uje6t"
);
assert!(to_ascii("n\u{11a99}\u{0308}\u{200d}n").is_err()); assert_eq!(to_ascii("\u{1d781}").unwrap(), "xn--4xa");
assert_eq!(to_ascii("\u{1d6d3}").unwrap(), "xn--4xa");
assert_eq!(to_ascii("\u{03c2}").unwrap(), "xn--3xa"); assert_eq!(to_ascii("\u{088b}\u{08c8}").unwrap(), "xn--3xb6j");
assert_eq!(to_ascii("\u{088f}\u{08cf}").unwrap(), "xn--7xb2k");
assert_eq!(to_ascii("\u{0398}\u{089f}").unwrap(), "xn--txa42w");
assert_eq!(to_ascii("\u{088f}\u{0888}").unwrap(), "xn--0xbn"); assert_eq!(to_ascii("\u{0888}\u{089f}").unwrap(), "xn--0xb0c"); assert_eq!(to_ascii("\u{0888}\u{0898}").unwrap(), "xn--0xb6a");
assert!(to_ascii("\u{1e810}\u{1e08f}\u{1e010}").is_err());
}
}
#[cfg(test)]
mod security_fast_path {
use super::*;
#[test]
fn leading_kana_voicing_mark_rejected() {
let domain = "\u{3099}a.com";
assert_ne!(scan_non_ascii(domain), Ok(Scan::SimpleIdentity));
assert!(to_ascii(domain).is_err(), "leading Mn must be rejected");
assert!(to_ascii("\u{309A}").is_err());
}
#[test]
fn kana_nfd_and_nfc_share_ace() {
let nfd = "\u{304b}\u{3099}"; let nfc = "\u{304c}"; assert_ne!(scan_non_ascii(nfd), Ok(Scan::SimpleIdentity));
assert_eq!(to_ascii(nfd).unwrap(), to_ascii(nfc).unwrap());
}
#[test]
fn turkish_and_cjk_still_simple_identity() {
assert_eq!(scan_non_ascii("türkçe.com"), Ok(Scan::SimpleIdentity));
assert_eq!(scan_non_ascii("你好你好.com"), Ok(Scan::SimpleIdentity));
assert_eq!(to_ascii("türkçe.com").unwrap(), "xn--trke-2oa7j.com");
}
#[test]
fn noncharacter_still_rejected() {
assert!(to_ascii("\u{ffff}").is_err());
}
}
#[cfg(test)]
mod audit_unassigned_simple_identity {
use super::*;
#[test]
fn unassigned_in_former_simple_ranges_rejected() {
let cps: &[u32] = &[
0x3040, 0x3097, 0x3098, 0xD7A4, 0xD7AF, ];
for &cp in cps {
let ch = char::from_u32(cp).unwrap();
let domain = format!("{ch}.com");
assert!(
to_ascii(&domain).is_err(),
"U+{cp:04X} must be rejected (servo/UTS46 unassigned)"
);
assert_ne!(
scan_non_ascii(&domain),
Ok(Scan::SimpleIdentity),
"U+{cp:04X} must not be SimpleIdentity"
);
}
}
}