use crate::{mapping, normalization, punycode, unicode, validation};
#[derive(Debug, Clone, PartialEq)]
pub enum IdnaError {
InvalidInput,
LabelTooLong,
EmptyLabel,
InvalidCharacter,
PunycodeError,
ValidationError,
}
pub fn to_ascii(domain: &str) -> Result<String, IdnaError> {
if domain.is_empty() {
return Err(IdnaError::EmptyLabel);
}
let labels: Vec<&str> = domain.split('.').collect();
let mut result_labels = Vec::with_capacity(labels.len());
for label in labels {
if label.is_empty() {
return Err(IdnaError::EmptyLabel);
}
let ascii_label = process_label_to_ascii(label)?;
result_labels.push(ascii_label);
}
Ok(result_labels.join("."))
}
pub fn to_unicode(domain: &str) -> Result<String, IdnaError> {
if domain.is_empty() {
return Err(IdnaError::EmptyLabel);
}
let labels: Vec<&str> = domain.split('.').collect();
let mut result_labels = Vec::with_capacity(labels.len());
for label in labels {
if label.is_empty() {
return Err(IdnaError::EmptyLabel);
}
let unicode_label = process_label_to_unicode(label)?;
result_labels.push(unicode_label);
}
Ok(result_labels.join("."))
}
fn process_label_to_ascii(label: &str) -> Result<String, IdnaError> {
if label.len() > 63 {
return Err(IdnaError::LabelTooLong);
}
if validation::is_ascii(label) {
let mapped = mapping::ascii_map(label);
if !validation::is_label_valid(&mapped) {
return Err(IdnaError::ValidationError);
}
return Ok(mapped);
}
let mapped = mapping::map(label);
let normalized = normalization::normalize(&mapped);
if validation::contains_forbidden_domain_code_point(&normalized) {
return Err(IdnaError::InvalidCharacter);
}
let utf32_chars = unicode::utf8_to_utf32(normalized.as_bytes());
if utf32_chars.is_empty() {
return Err(IdnaError::InvalidInput);
}
let punycode = punycode::utf32_to_punycode(&utf32_chars).ok_or(IdnaError::PunycodeError)?;
let result = format!("xn--{}", punycode);
if result.len() > 63 {
return Err(IdnaError::LabelTooLong);
}
Ok(result)
}
fn process_label_to_unicode(label: &str) -> Result<String, IdnaError> {
if !label.starts_with("xn--") {
if !validation::is_label_valid(label) {
return Err(IdnaError::ValidationError);
}
return Ok(label.to_string());
}
let punycode_part = &label[4..];
let utf32_chars = punycode::punycode_to_utf32(punycode_part).ok_or(IdnaError::PunycodeError)?;
let utf8_bytes = unicode::utf32_to_utf8(&utf32_chars);
if utf8_bytes.is_empty() {
return Err(IdnaError::InvalidInput);
}
let decoded = String::from_utf8(utf8_bytes).map_err(|_| IdnaError::InvalidInput)?;
let mapped = mapping::map(&decoded);
let normalized = normalization::normalize(&mapped);
if validation::contains_forbidden_domain_code_point(&normalized) {
return Err(IdnaError::InvalidCharacter);
}
let re_encoded_utf32 = unicode::utf8_to_utf32(normalized.as_bytes());
let re_encoded_punycode =
punycode::utf32_to_punycode(&re_encoded_utf32).ok_or(IdnaError::PunycodeError)?;
if re_encoded_punycode != punycode_part {
return Err(IdnaError::ValidationError);
}
Ok(normalized)
}
pub use validation::{contains_forbidden_domain_code_point, is_ascii};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_ascii_simple() {
let result = to_ascii("example.com");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "example.com");
}
#[test]
fn test_to_ascii_unicode() {
let result = to_ascii("café.example");
assert!(result.is_ok());
assert!(result.unwrap().starts_with("xn--"));
}
#[test]
fn test_to_unicode() {
let result = to_unicode("xn--4ca.example");
if result.is_ok() {
let unicode_domain = result.unwrap();
assert!(unicode_domain.contains("ä"));
}
}
#[test]
fn test_empty_domain() {
assert!(to_ascii("").is_err());
assert!(to_unicode("").is_err());
}
#[test]
fn test_label_too_long() {
let long_label = "a".repeat(64);
let result = to_ascii(&long_label);
assert!(result.is_err());
}
}