use super::Located;
use crate::source::SourceSpan;
use std::net::IpAddr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ExtraHosts {
Short {
span: SourceSpan,
entries: Vec<ShortExtraHost>,
},
Long {
span: SourceSpan,
entries: Vec<LongExtraHost>,
},
}
impl ExtraHosts {
#[must_use]
pub const fn span(&self) -> SourceSpan {
match self {
Self::Short { span, .. } | Self::Long { span, .. } => *span,
}
}
#[must_use]
pub fn contains_host_gateway(&self) -> bool {
match self {
Self::Short { entries, .. } => entries
.iter()
.any(|entry| entry.address().is_some_and(HostAddress::is_host_gateway)),
Self::Long { entries, .. } => entries.iter().any(|entry| entry.address().value().is_host_gateway()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExtraHostSeparator {
Equals,
Colon,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShortExtraHost {
raw: Located<String>,
hostname: Option<String>,
address: Option<HostAddress>,
separator: Option<ExtraHostSeparator>,
}
impl ShortExtraHost {
pub(crate) fn parse(raw: Located<String>) -> Self {
let (hostname, address, separator) =
split_short_entry(raw.value()).map_or((None, None, None), |(hostname, address, separator)| {
(
Some(hostname.to_owned()),
Some(HostAddress::parse(address.to_owned())),
Some(separator),
)
});
Self {
raw,
hostname,
address,
separator,
}
}
#[must_use]
pub const fn raw(&self) -> &Located<String> {
&self.raw
}
#[must_use]
pub fn hostname(&self) -> Option<&str> {
self.hostname.as_deref()
}
#[must_use]
pub const fn address(&self) -> Option<&HostAddress> {
self.address.as_ref()
}
#[must_use]
pub const fn separator(&self) -> Option<ExtraHostSeparator> {
self.separator
}
#[must_use]
pub const fn is_complete(&self) -> bool {
self.hostname.is_some() && self.address.is_some()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LongExtraHost {
hostname: Located<String>,
address: Located<HostAddress>,
span: SourceSpan,
}
impl LongExtraHost {
pub(super) const fn new(hostname: Located<String>, address: Located<HostAddress>, span: SourceSpan) -> Self {
Self {
hostname,
address,
span,
}
}
#[must_use]
pub const fn hostname(&self) -> &Located<String> {
&self.hostname
}
#[must_use]
pub const fn address(&self) -> &Located<HostAddress> {
&self.address
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.span
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HostAddress {
raw: String,
kind: HostAddressKind,
}
impl HostAddress {
pub(crate) fn parse(raw: String) -> Self {
let unbracketed = raw
.strip_prefix('[')
.and_then(|value| value.strip_suffix(']'))
.unwrap_or(&raw);
let kind = if raw == "host-gateway" {
HostAddressKind::HostGateway
} else {
match unbracketed.parse::<IpAddr>() {
Ok(IpAddr::V4(_)) => HostAddressKind::Ipv4,
Ok(IpAddr::V6(_)) => HostAddressKind::Ipv6 {
bracketed: raw.starts_with('[') && raw.ends_with(']'),
},
Err(_) => HostAddressKind::Other,
}
};
Self { raw, kind }
}
#[must_use]
pub fn raw(&self) -> &str {
&self.raw
}
#[must_use]
pub const fn kind(&self) -> HostAddressKind {
self.kind
}
#[must_use]
pub const fn is_host_gateway(&self) -> bool {
matches!(self.kind, HostAddressKind::HostGateway)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HostAddressKind {
Ipv4,
Ipv6 {
bracketed: bool,
},
HostGateway,
Other,
}
fn split_short_entry(value: &str) -> Option<(&str, &str, ExtraHostSeparator)> {
if let Some((hostname, address)) = value.split_once('=') {
return (!hostname.is_empty() && !address.is_empty()).then_some((
hostname,
address,
ExtraHostSeparator::Equals,
));
}
let (hostname, address) = value.split_once(':')?;
(!hostname.is_empty() && !address.is_empty()).then_some((hostname, address, ExtraHostSeparator::Colon))
}
#[cfg(test)]
mod tests {
use super::{ExtraHostSeparator, HostAddressKind, ShortExtraHost};
use crate::model::Located;
use crate::source::{SourceId, SourceSpan};
fn entry(value: &str) -> Result<ShortExtraHost, &'static str> {
let span = SourceSpan::new(SourceId::new(1), 0, value.len()).ok_or("valid test span expected")?;
Ok(ShortExtraHost::parse(Located::new(value.to_owned(), span)))
}
#[test]
fn preserves_ipv6_and_legacy_separator_spelling() -> Result<(), &'static str> {
let unbracketed = entry("myhostv6:::1")?;
assert_eq!(unbracketed.hostname(), Some("myhostv6"));
assert_eq!(unbracketed.address().map(super::HostAddress::raw), Some("::1"));
assert_eq!(unbracketed.separator(), Some(ExtraHostSeparator::Colon));
assert_eq!(
unbracketed.address().map(super::HostAddress::kind),
Some(HostAddressKind::Ipv6 { bracketed: false })
);
let bracketed = entry("myhostv6=[::1]")?;
assert_eq!(
bracketed.address().map(super::HostAddress::kind),
Some(HostAddressKind::Ipv6 { bracketed: true })
);
Ok(())
}
}