1use super::Located;
4use crate::source::SourceSpan;
5use std::net::IpAddr;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ExtraHosts {
10 Short {
12 span: SourceSpan,
14 entries: Vec<ShortExtraHost>,
16 },
17 Long {
19 span: SourceSpan,
21 entries: Vec<LongExtraHost>,
23 },
24}
25
26impl ExtraHosts {
27 #[must_use]
29 pub const fn span(&self) -> SourceSpan {
30 match self {
31 Self::Short { span, .. } | Self::Long { span, .. } => *span,
32 }
33 }
34
35 #[must_use]
37 pub fn contains_host_gateway(&self) -> bool {
38 match self {
39 Self::Short { entries, .. } => entries
40 .iter()
41 .any(|entry| entry.address().is_some_and(HostAddress::is_host_gateway)),
42 Self::Long { entries, .. } => entries.iter().any(|entry| entry.address().value().is_host_gateway()),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum ExtraHostSeparator {
50 Equals,
52 Colon,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct ShortExtraHost {
59 raw: Located<String>,
60 hostname: Option<String>,
61 address: Option<HostAddress>,
62 separator: Option<ExtraHostSeparator>,
63}
64
65impl ShortExtraHost {
66 pub(crate) fn parse(raw: Located<String>) -> Self {
67 let (hostname, address, separator) =
68 split_short_entry(raw.value()).map_or((None, None, None), |(hostname, address, separator)| {
69 (
70 Some(hostname.to_owned()),
71 Some(HostAddress::parse(address.to_owned())),
72 Some(separator),
73 )
74 });
75 Self {
76 raw,
77 hostname,
78 address,
79 separator,
80 }
81 }
82
83 #[must_use]
85 pub const fn raw(&self) -> &Located<String> {
86 &self.raw
87 }
88
89 #[must_use]
91 pub fn hostname(&self) -> Option<&str> {
92 self.hostname.as_deref()
93 }
94
95 #[must_use]
97 pub const fn address(&self) -> Option<&HostAddress> {
98 self.address.as_ref()
99 }
100
101 #[must_use]
103 pub const fn separator(&self) -> Option<ExtraHostSeparator> {
104 self.separator
105 }
106
107 #[must_use]
109 pub const fn is_complete(&self) -> bool {
110 self.hostname.is_some() && self.address.is_some()
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct LongExtraHost {
117 hostname: Located<String>,
118 address: Located<HostAddress>,
119 span: SourceSpan,
120}
121
122impl LongExtraHost {
123 pub(super) const fn new(hostname: Located<String>, address: Located<HostAddress>, span: SourceSpan) -> Self {
124 Self {
125 hostname,
126 address,
127 span,
128 }
129 }
130
131 #[must_use]
133 pub const fn hostname(&self) -> &Located<String> {
134 &self.hostname
135 }
136
137 #[must_use]
139 pub const fn address(&self) -> &Located<HostAddress> {
140 &self.address
141 }
142
143 #[must_use]
145 pub const fn span(&self) -> SourceSpan {
146 self.span
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct HostAddress {
153 raw: String,
154 kind: HostAddressKind,
155}
156
157impl HostAddress {
158 pub(crate) fn parse(raw: String) -> Self {
159 let unbracketed = raw
160 .strip_prefix('[')
161 .and_then(|value| value.strip_suffix(']'))
162 .unwrap_or(&raw);
163 let kind = if raw == "host-gateway" {
164 HostAddressKind::HostGateway
165 } else {
166 match unbracketed.parse::<IpAddr>() {
167 Ok(IpAddr::V4(_)) => HostAddressKind::Ipv4,
168 Ok(IpAddr::V6(_)) => HostAddressKind::Ipv6 {
169 bracketed: raw.starts_with('[') && raw.ends_with(']'),
170 },
171 Err(_) => HostAddressKind::Other,
172 }
173 };
174 Self { raw, kind }
175 }
176
177 #[must_use]
179 pub fn raw(&self) -> &str {
180 &self.raw
181 }
182
183 #[must_use]
185 pub const fn kind(&self) -> HostAddressKind {
186 self.kind
187 }
188
189 #[must_use]
191 pub const fn is_host_gateway(&self) -> bool {
192 matches!(self.kind, HostAddressKind::HostGateway)
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
198pub enum HostAddressKind {
199 Ipv4,
201 Ipv6 {
203 bracketed: bool,
205 },
206 HostGateway,
208 Other,
210}
211
212fn split_short_entry(value: &str) -> Option<(&str, &str, ExtraHostSeparator)> {
213 if let Some((hostname, address)) = value.split_once('=') {
214 return (!hostname.is_empty() && !address.is_empty()).then_some((
215 hostname,
216 address,
217 ExtraHostSeparator::Equals,
218 ));
219 }
220 let (hostname, address) = value.split_once(':')?;
221 (!hostname.is_empty() && !address.is_empty()).then_some((hostname, address, ExtraHostSeparator::Colon))
222}
223
224#[cfg(test)]
225mod tests {
226 use super::{ExtraHostSeparator, HostAddressKind, ShortExtraHost};
227 use crate::model::Located;
228 use crate::source::{SourceId, SourceSpan};
229
230 fn entry(value: &str) -> Result<ShortExtraHost, &'static str> {
231 let span = SourceSpan::new(SourceId::new(1), 0, value.len()).ok_or("valid test span expected")?;
232 Ok(ShortExtraHost::parse(Located::new(value.to_owned(), span)))
233 }
234
235 #[test]
236 fn preserves_ipv6_and_legacy_separator_spelling() -> Result<(), &'static str> {
237 let unbracketed = entry("myhostv6:::1")?;
238 assert_eq!(unbracketed.hostname(), Some("myhostv6"));
239 assert_eq!(unbracketed.address().map(super::HostAddress::raw), Some("::1"));
240 assert_eq!(unbracketed.separator(), Some(ExtraHostSeparator::Colon));
241 assert_eq!(
242 unbracketed.address().map(super::HostAddress::kind),
243 Some(HostAddressKind::Ipv6 { bracketed: false })
244 );
245
246 let bracketed = entry("myhostv6=[::1]")?;
247 assert_eq!(
248 bracketed.address().map(super::HostAddress::kind),
249 Some(HostAddressKind::Ipv6 { bracketed: true })
250 );
251 Ok(())
252 }
253}