Skip to main content

compose_lens/model/
host.rs

1//! Source-aware extra-host mappings.
2
3use super::Located;
4use crate::source::SourceSpan;
5use std::net::IpAddr;
6
7/// The authored collection form of `extra_hosts`.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum ExtraHosts {
10    /// Sequence-based short syntax.
11    Short {
12        /// The complete sequence span.
13        span: SourceSpan,
14        /// Host mappings in authored order.
15        entries: Vec<ShortExtraHost>,
16    },
17    /// Mapping-based long syntax.
18    Long {
19        /// The complete mapping span.
20        span: SourceSpan,
21        /// Host mappings in authored order.
22        entries: Vec<LongExtraHost>,
23    },
24}
25
26impl ExtraHosts {
27    /// Returns the complete collection span.
28    #[must_use]
29    pub const fn span(&self) -> SourceSpan {
30        match self {
31            Self::Short { span, .. } | Self::Long { span, .. } => *span,
32        }
33    }
34
35    /// Reports whether any entry uses the implementation token `host-gateway`.
36    #[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/// The separator used by one short `extra_hosts` entry.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub enum ExtraHostSeparator {
50    /// Preferred `HOST=ADDRESS` spelling.
51    Equals,
52    /// Compatibility `HOST:ADDRESS` spelling.
53    Colon,
54}
55
56/// One short-syntax `extra_hosts` entry.
57#[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    /// Returns the complete unquoted scalar and its source span.
84    #[must_use]
85    pub const fn raw(&self) -> &Located<String> {
86        &self.raw
87    }
88
89    /// Returns the conservatively extracted hostname.
90    #[must_use]
91    pub fn hostname(&self) -> Option<&str> {
92        self.hostname.as_deref()
93    }
94
95    /// Returns the raw-preserving address or implementation token.
96    #[must_use]
97    pub const fn address(&self) -> Option<&HostAddress> {
98        self.address.as_ref()
99    }
100
101    /// Returns the authored separator.
102    #[must_use]
103    pub const fn separator(&self) -> Option<ExtraHostSeparator> {
104        self.separator
105    }
106
107    /// Reports whether this entry contains a hostname and address.
108    #[must_use]
109    pub const fn is_complete(&self) -> bool {
110        self.hostname.is_some() && self.address.is_some()
111    }
112}
113
114/// One mapping-syntax `extra_hosts` entry.
115#[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    /// Returns the hostname mapping key.
132    #[must_use]
133    pub const fn hostname(&self) -> &Located<String> {
134        &self.hostname
135    }
136
137    /// Returns the raw-preserving address or implementation token.
138    #[must_use]
139    pub const fn address(&self) -> &Located<HostAddress> {
140        &self.address
141    }
142
143    /// Returns the complete entry span.
144    #[must_use]
145    pub const fn span(&self) -> SourceSpan {
146        self.span
147    }
148}
149
150/// A host address classified without normalizing its authored spelling.
151#[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    /// Returns the address exactly as represented by the YAML scalar.
178    #[must_use]
179    pub fn raw(&self) -> &str {
180        &self.raw
181    }
182
183    /// Returns the non-destructive address classification.
184    #[must_use]
185    pub const fn kind(&self) -> HostAddressKind {
186        self.kind
187    }
188
189    /// Reports whether this is the implementation token `host-gateway`.
190    #[must_use]
191    pub const fn is_host_gateway(&self) -> bool {
192        matches!(self.kind, HostAddressKind::HostGateway)
193    }
194}
195
196/// The lexical kind of a host address.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
198pub enum HostAddressKind {
199    /// An IPv4 address.
200    Ipv4,
201    /// An IPv6 address, retaining whether brackets were authored.
202    Ipv6 {
203        /// Whether the address used `[::1]` spelling.
204        bracketed: bool,
205    },
206    /// The runtime-specific `host-gateway` token.
207    HostGateway,
208    /// A deferred expression or implementation-specific value.
209    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}