use alloc::string::String;
use alloc::string::ToString;
use alloc::vec::Vec;
use crate::error::{HttpUrlError, Result};
pub fn canonicalize_host(host: &str) -> Result<String> {
let host = host.trim();
if host.is_empty() {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
if host.starts_with('[') {
if !host.ends_with(']') {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
let inner = &host[1..host.len() - 1];
if inner.is_empty() {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
if !is_valid_ipv6(inner) {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
return Ok(host.to_string()); }
if is_ipv4_literal(host) {
if !is_valid_ipv4(host) {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
return Ok(host.to_string());
}
let lower = host.to_ascii_lowercase();
validate_hostname(&lower)?;
Ok(lower)
}
fn validate_hostname(host: &str) -> Result<()> {
if host.is_empty() {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
if host.len() > 253 {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
let labels: Vec<&str> = host.split('.').collect();
for label in &labels {
if label.is_empty() {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
if label.len() > 63 {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
for (i, &b) in label.as_bytes().iter().enumerate() {
if !b.is_ascii_alphanumeric() && b != b'-' {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
if b == b'-' && (i == 0 || i == label.len() - 1) {
return Err(HttpUrlError::InvalidHost(host.to_string()));
}
}
}
Ok(())
}
fn is_ipv4_literal(s: &str) -> bool {
s.bytes().all(|b| b.is_ascii_digit() || b == b'.')
}
fn is_valid_ipv4(s: &str) -> bool {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 4 {
return false;
}
parts.iter().all(|p| {
if p.is_empty() {
return false;
}
if p.len() > 1 && p.starts_with('0') {
return false;
}
p.bytes().all(|b| b.is_ascii_digit())
&& p.len() <= 3
&& p.parse::<u16>().is_ok_and(|v| v <= 255)
})
}
fn is_valid_ipv6(s: &str) -> bool {
if s.is_empty() || s.len() > 128 {
return false;
}
if s.starts_with(':') && !s.starts_with("::") {
return false;
}
if s.ends_with(':') && !s.ends_with("::") {
return false;
}
s.bytes()
.all(|b| b.is_ascii_hexdigit() || b == b':' || b == b'.')
}
pub fn is_domain(host: &str) -> bool {
host.bytes().any(|b| b.is_ascii_alphabetic() && b != b'.')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonicalize_host_lowercase() {
assert_eq!(canonicalize_host("Example.COM").unwrap(), "example.com");
}
#[test]
fn test_canonicalize_host_ipv4() {
assert_eq!(canonicalize_host("192.168.1.1").unwrap(), "192.168.1.1");
}
#[test]
fn test_canonicalize_host_ipv6() {
let host = canonicalize_host("[::1]").unwrap();
assert!(host.starts_with('['));
}
#[test]
fn test_canonicalize_host_invalid_ipv4() {
assert!(canonicalize_host("256.0.0.1").is_err());
}
#[test]
fn test_canonicalize_host_invalid_label() {
assert!(canonicalize_host("-host.com").is_err());
assert!(canonicalize_host("host-.com").is_err());
}
#[test]
fn test_canonicalize_host_empty() {
assert!(canonicalize_host("").is_err());
}
}