use super::error::{Error, Result};
pub fn is_private_address(authority: &str) -> bool {
let host = if authority.starts_with('[') {
authority.find(']').map_or(authority, |i| &authority[1..i])
} else if let Some(colon_idx) = authority.rfind(':') {
let potential_port = &authority[colon_idx + 1..];
if !potential_port.is_empty() && potential_port.chars().all(|c| c.is_ascii_digit()) {
&authority[..colon_idx]
} else {
authority
}
} else {
authority
};
let host_lower = host.to_lowercase();
if host_lower == "localhost" || host_lower.ends_with(".localhost") {
return true;
}
if let Some((a, rest)) = host.split_once('.')
&& let Ok(first_octet) = a.parse::<u8>()
{
if first_octet == 127 {
return true;
}
if first_octet == 10 {
return true;
}
if first_octet == 0 {
return true;
}
if first_octet == 192
&& let Some((b, _)) = rest.split_once('.')
&& b == "168"
{
return true;
}
if first_octet == 172
&& let Some((b, _)) = rest.split_once('.')
&& let Ok(second_octet) = b.parse::<u8>()
&& (16..=31).contains(&second_octet)
{
return true;
}
if first_octet == 169
&& let Some((b, _)) = rest.split_once('.')
&& b == "254"
{
return true;
}
}
let ipv6 = host_lower.trim_start_matches('[').trim_end_matches(']');
if ipv6 == "::1" || ipv6 == "0:0:0:0:0:0:0:1" {
return true;
}
if ipv6 == "::" || ipv6 == "0:0:0:0:0:0:0:0" {
return true;
}
if ipv6.starts_with("fe80:") || ipv6.starts_with("fe80::") {
return true;
}
if ipv6.starts_with("fc") || ipv6.starts_with("fd") {
return true;
}
false
}
pub(super) fn validate_authority(authority: &str) -> Result<()> {
if authority.starts_with('[') {
let close_bracket = authority
.find(']')
.ok_or_else(|| Error::InvalidUrl("IPv6 address missing closing `]`".to_string()))?;
let ipv6_part = &authority[1..close_bracket];
validate_ipv6(ipv6_part)?;
let after_bracket = &authority[close_bracket + 1..];
if !after_bracket.is_empty() {
if !after_bracket.starts_with(':') {
return Err(Error::InvalidUrl(
"invalid characters after IPv6 address".to_string(),
));
}
validate_port(&after_bracket[1..])?;
}
} else {
if let Some(colon_idx) = authority.rfind(':') {
let potential_port = &authority[colon_idx + 1..];
if !potential_port.is_empty() && potential_port.chars().all(|c| c.is_ascii_digit()) {
validate_port(potential_port)?;
}
}
}
Ok(())
}
fn validate_ipv6(addr: &str) -> Result<()> {
if addr.is_empty() {
return Err(Error::InvalidUrl("empty IPv6 address".to_string()));
}
let mut double_colon_count = 0;
let mut segments = 0;
for part in addr.split(':') {
if part.is_empty() {
double_colon_count += 1;
continue;
}
if part.len() > 4 || !part.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(Error::InvalidUrl(format!("invalid IPv6 segment `{part}`")));
}
segments += 1;
}
if double_colon_count > 3 {
return Err(Error::InvalidUrl(
"invalid IPv6 address: multiple `::` sequences".to_string(),
));
}
if segments > 8 {
return Err(Error::InvalidUrl(
"invalid IPv6 address: too many segments".to_string(),
));
}
Ok(())
}
fn validate_port(port: &str) -> Result<()> {
if port.is_empty() {
return Err(Error::InvalidUrl("empty port number".to_string()));
}
let port_num: u32 = port
.parse()
.map_err(|_| Error::InvalidUrl(format!("invalid port number `{port}`")))?;
if port_num == 0 || port_num > 65535 {
return Err(Error::InvalidUrl(format!(
"port `{port_num}` out of range (1-65535)"
)));
}
Ok(())
}
pub(super) fn validate_percent_encoding(s: &str) -> Result<()> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
if i + 2 >= bytes.len() {
return Err(Error::InvalidUrl(
"incomplete percent-encoding at end of URL".to_string(),
));
}
let hex1 = bytes[i + 1];
let hex2 = bytes[i + 2];
if !hex1.is_ascii_hexdigit() || !hex2.is_ascii_hexdigit() {
return Err(Error::InvalidUrl(format!(
"invalid percent-encoding `%{}{}`",
char::from(hex1),
char::from(hex2)
)));
}
i += 3;
} else {
i += 1;
}
}
Ok(())
}