use std::borrow::Cow;
use deno_core::op2;
const PUNY_PREFIX: &str = "xn--";
#[derive(Debug, thiserror::Error, deno_error::JsError)]
pub enum IdnaError {
#[class(range)]
#[error("Invalid input")]
InvalidInput,
#[class(generic)]
#[error("Input would take more than 63 characters to encode")]
InputTooLong,
#[class(range)]
#[error("Illegal input >= 0x80 (not a basic code point)")]
IllegalInput,
}
deno_error::js_error_wrapper!(idna::Errors, JsIdnaErrors, "Error");
fn map_domain(
domain: &str,
f: impl Fn(&str) -> Result<Cow<'_, str>, IdnaError>,
) -> Result<String, IdnaError> {
let mut result = String::with_capacity(domain.len());
let mut domain = domain;
let mut parts = domain.split('@');
if let (Some(local), Some(remaining)) = (parts.next(), parts.next()) {
result.push_str(local);
result.push('@');
domain = remaining;
}
for (i, label) in domain.split('.').enumerate() {
if i > 0 {
result.push('.');
}
result.push_str(&f(label)?);
}
Ok(result)
}
fn to_ascii(input: &str) -> Result<String, IdnaError> {
if input.is_ascii() {
return Ok(input.into());
}
let mut result = String::with_capacity(input.len());
let rest = map_domain(input, |label| {
if label.is_ascii() {
Ok(label.into())
} else {
idna::punycode::encode_str(label)
.map(|encoded| [PUNY_PREFIX, &encoded].join("").into()) .ok_or(IdnaError::InputTooLong) }
})?;
result.push_str(&rest);
Ok(result)
}
fn to_unicode(input: &str) -> Result<String, IdnaError> {
map_domain(input, |s| {
if let Some(puny) = s.strip_prefix(PUNY_PREFIX) {
Ok(
idna::punycode::decode_to_string(&puny.to_lowercase())
.ok_or(IdnaError::InvalidInput)?
.into(),
)
} else {
Ok(s.into())
}
})
}
#[op2]
#[string]
pub fn op_node_idna_punycode_to_ascii(
#[string] domain: String,
) -> Result<String, IdnaError> {
to_ascii(&domain)
}
#[op2]
#[string]
pub fn op_node_idna_punycode_to_unicode(
#[string] domain: String,
) -> Result<String, IdnaError> {
to_unicode(&domain)
}
#[op2]
#[string]
pub fn op_node_idna_domain_to_ascii(#[string] domain: String) -> String {
idna::domain_to_ascii(&domain).unwrap_or_default()
}
#[op2]
#[string]
pub fn op_node_idna_domain_to_unicode(#[string] domain: String) -> String {
idna::domain_to_unicode(&domain).0
}
#[op2]
#[string]
pub fn op_node_idna_punycode_decode(
#[string] domain: String,
) -> Result<String, IdnaError> {
if domain.is_empty() {
return Ok(domain);
}
let last_dash = domain.len()
- 1
- domain
.bytes()
.rev()
.position(|b| b == b'-')
.unwrap_or(domain.len() - 1);
if !domain[..last_dash].is_ascii() {
return Err(IdnaError::IllegalInput);
}
idna::punycode::decode_to_string(&domain).ok_or(IdnaError::InvalidInput)
}
#[op2]
#[string]
pub fn op_node_idna_punycode_encode(#[string] domain: String) -> String {
idna::punycode::encode_str(&domain).unwrap_or_default()
}