#[cfg(feature = "idn")]
use crate::error::Error;
use crate::error::Result;
pub(crate) fn to_ascii(host: &str, enabled: bool) -> Result<String> {
#[cfg(feature = "idn")]
if enabled && !host.is_ascii() {
let ascii = intl::unicode::idna::to_ascii(host)
.map_err(|_| Error::InvalidUrl(format!("invalid IDN host: {host}")))?;
if ascii.bytes().any(|b| {
b < 0x20
|| matches!(
b,
0x7f | b' ' | b'/' | b'\\' | b'@' | b':' | b'?' | b'#' | b'%'
)
}) {
return Err(Error::InvalidUrl(format!(
"IDN host encodes to a forbidden authority delimiter: {host}"
)));
}
return Ok(ascii);
}
let _ = enabled;
Ok(host.to_string())
}
#[cfg(test)]
mod tests {
use super::to_ascii;
#[test]
fn ascii_hosts_pass_through_unchanged() {
for h in [
"example.com",
"127.0.0.1",
"[::1]",
"xn--mnchen-3ya.de", "localhost",
"Example.COM", ] {
assert_eq!(
to_ascii(h, true).unwrap(),
h,
"ASCII host must be untouched: {h}"
);
}
}
#[cfg(feature = "idn")]
#[test]
fn unicode_host_is_punycoded_when_enabled() {
assert_eq!(to_ascii("münchen.de", true).unwrap(), "xn--mnchen-3ya.de");
assert_eq!(to_ascii("☃.net", true).unwrap(), "xn--n3h.net");
}
#[test]
fn disabled_leaves_unicode_raw() {
assert_eq!(to_ascii("münchen.de", false).unwrap(), "münchen.de");
}
#[cfg(feature = "idn")]
#[test]
fn rejects_idn_authority_delimiter_injection() {
for input in [
"@evil.com", "good.com/../evil.com", "good.com:8080", "evil#.com", "x?y.com", ] {
assert!(
to_ascii(input, true).is_err(),
"IDN delimiter injection must be rejected: {input:?}"
);
}
}
#[cfg(feature = "idn")]
#[test]
fn legitimate_hosts_still_succeed_after_guard() {
assert_eq!(to_ascii("münchen.de", true).unwrap(), "xn--mnchen-3ya.de");
assert_eq!(to_ascii("example.com", true).unwrap(), "example.com");
}
}