use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use elasticctl_core::Result;
use super::{ValueListType, invalid};
pub fn canonical_value(kind: &ValueListType, value: &str) -> Result<Option<String>> {
kind.require_supported()?;
let value = value.trim_matches([' ', '\t']);
if value.is_empty() {
return Err(invalid("empty value"));
}
if value.chars().any(char::is_control) {
return Err(invalid(format!("control character in value {value:?}")));
}
match kind {
ValueListType::Keyword | ValueListType::Text => Ok(Some(value.to_owned())),
ValueListType::Ip => Ok(Some(address(value)?.to_string())),
ValueListType::IpRange => range(value),
ValueListType::Unsupported(_) => unreachable!(),
}
}
fn address(value: &str) -> Result<IpAddr> {
value
.parse()
.map_err(|_| invalid(format!("invalid IP address {value:?}")))
}
fn range(value: &str) -> Result<Option<String>> {
if let Some((start, end)) = value.split_once('-') {
let (start, end) = (address(start)?, address(end)?);
if mapped_address(start) > mapped_address(end) {
return Err(invalid(format!("reversed IP range {value:?}")));
}
if start.is_ipv4() != end.is_ipv4() {
return Ok(None);
}
return Ok(Some(format!("{start}-{end}")));
}
if let Some((ip, prefix)) = value.split_once('/') {
let ip = address(ip)?;
let bits = if ip.is_ipv4() { 32 } else { 128 };
let prefix: u32 = prefix
.parse()
.ok()
.filter(|length| *length <= bits)
.filter(|_| !prefix.is_empty() && prefix.bytes().all(|b| b.is_ascii_digit()))
.ok_or_else(|| invalid(format!("invalid CIDR prefix in {value:?}")))?;
let network = match ip {
IpAddr::V4(ip) => {
let mask = u32::MAX.checked_shl(32 - prefix).unwrap_or(0);
IpAddr::V4(Ipv4Addr::from(u32::from(ip) & mask))
}
IpAddr::V6(ip) => {
let mask = u128::MAX.checked_shl(128 - prefix).unwrap_or(0);
IpAddr::V6(Ipv6Addr::from(u128::from(ip) & mask))
}
};
return Ok(Some(format!("{network}/{prefix}")));
}
let ip = address(value)?;
Ok(Some(format!(
"{ip}/{}",
if ip.is_ipv4() { 32 } else { 128 }
)))
}
fn mapped_address(ip: IpAddr) -> Ipv6Addr {
match ip {
IpAddr::V4(ip) => ip.to_ipv6_mapped(),
IpAddr::V6(ip) => ip,
}
}
pub(super) fn rewrite_bare_range(kind: &ValueListType, value: &str) -> String {
if *kind == ValueListType::IpRange
&& let Ok(ip) = value.parse::<IpAddr>()
{
return format!("{value}/{}", if ip.is_ipv4() { 32 } else { 128 });
}
value.to_owned()
}