Skip to main content

compose_lens/model/
port.rs

1//! Service port forms.
2
3use super::{FieldReference, Located};
4use crate::source::SourceSpan;
5
6/// A short-syntax published port.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct ShortPort {
9    raw: Located<String>,
10    host_ip: Option<String>,
11    published: Option<String>,
12    target: String,
13    protocol: Option<String>,
14}
15
16impl ShortPort {
17    pub(crate) fn parse(raw: Located<String>) -> Self {
18        let (without_protocol, protocol) = raw
19            .value()
20            .rsplit_once('/')
21            .map_or((raw.value().as_str(), None), |(value, protocol)| {
22                (value, Some(protocol.to_owned()))
23            });
24        let fields = split_port_fields(without_protocol);
25        let (host_ip, published, target) = match fields.as_slice() {
26            [] => (None, None, String::new()),
27            [target] => (None, None, (*target).to_owned()),
28            [published, target] => (None, Some((*published).to_owned()), (*target).to_owned()),
29            [host @ .., published, target] => (
30                Some(host.join(":")),
31                Some((*published).to_owned()),
32                (*target).to_owned(),
33            ),
34        };
35        Self {
36            raw,
37            host_ip,
38            published,
39            target,
40            protocol,
41        }
42    }
43
44    /// Returns the complete semantic short string and source span.
45    #[must_use]
46    pub const fn raw(&self) -> &Located<String> {
47        &self.raw
48    }
49
50    /// Returns the conservatively parsed host address.
51    #[must_use]
52    pub fn host_ip(&self) -> Option<&str> {
53        self.host_ip.as_deref()
54    }
55
56    /// Returns the conservatively parsed published port or range.
57    #[must_use]
58    pub fn published(&self) -> Option<&str> {
59        self.published.as_deref()
60    }
61
62    /// Returns the conservatively parsed container port or range.
63    #[must_use]
64    pub fn target(&self) -> &str {
65        &self.target
66    }
67
68    /// Returns the explicitly authored protocol.
69    #[must_use]
70    pub fn protocol(&self) -> Option<&str> {
71        self.protocol.as_deref()
72    }
73}
74
75/// A long-syntax published port.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct LongPort {
78    span: SourceSpan,
79    target: Option<Located<String>>,
80    published: Option<Located<String>>,
81    host_ip: Option<Located<String>>,
82    protocol: Option<Located<String>>,
83    app_protocol: Option<Located<String>>,
84    mode: Option<Located<String>>,
85    name: Option<Located<String>>,
86    extension_fields: Vec<FieldReference>,
87    unknown_fields: Vec<FieldReference>,
88}
89
90impl LongPort {
91    pub(crate) fn new(span: SourceSpan) -> Self {
92        Self {
93            span,
94            target: None,
95            published: None,
96            host_ip: None,
97            protocol: None,
98            app_protocol: None,
99            mode: None,
100            name: None,
101            extension_fields: Vec::new(),
102            unknown_fields: Vec::new(),
103        }
104    }
105
106    pub(crate) fn set_target(&mut self, value: Located<String>) {
107        self.target = Some(value);
108    }
109    pub(crate) fn set_published(&mut self, value: Located<String>) {
110        self.published = Some(value);
111    }
112    pub(crate) fn set_host_ip(&mut self, value: Located<String>) {
113        self.host_ip = Some(value);
114    }
115    pub(crate) fn set_protocol(&mut self, value: Located<String>) {
116        self.protocol = Some(value);
117    }
118    pub(crate) fn set_app_protocol(&mut self, value: Located<String>) {
119        self.app_protocol = Some(value);
120    }
121    pub(crate) fn set_mode(&mut self, value: Located<String>) {
122        self.mode = Some(value);
123    }
124    pub(crate) fn set_name(&mut self, value: Located<String>) {
125        self.name = Some(value);
126    }
127    pub(super) fn push_extension(&mut self, value: FieldReference) {
128        self.extension_fields.push(value);
129    }
130    pub(super) fn push_unknown(&mut self, value: FieldReference) {
131        self.unknown_fields.push(value);
132    }
133
134    /// Returns the complete mapping span.
135    #[must_use]
136    pub const fn span(&self) -> SourceSpan {
137        self.span
138    }
139    /// Returns the container port or range.
140    #[must_use]
141    pub const fn target(&self) -> Option<&Located<String>> {
142        self.target.as_ref()
143    }
144    /// Returns the published port or range.
145    #[must_use]
146    pub const fn published(&self) -> Option<&Located<String>> {
147        self.published.as_ref()
148    }
149    /// Returns the host address.
150    #[must_use]
151    pub const fn host_ip(&self) -> Option<&Located<String>> {
152        self.host_ip.as_ref()
153    }
154    /// Returns the protocol.
155    #[must_use]
156    pub const fn protocol(&self) -> Option<&Located<String>> {
157        self.protocol.as_ref()
158    }
159    /// Returns the application protocol hint.
160    #[must_use]
161    pub const fn app_protocol(&self) -> Option<&Located<String>> {
162        self.app_protocol.as_ref()
163    }
164    /// Returns the publication mode.
165    #[must_use]
166    pub const fn mode(&self) -> Option<&Located<String>> {
167        self.mode.as_ref()
168    }
169    /// Returns the human-readable port name.
170    #[must_use]
171    pub const fn name(&self) -> Option<&Located<String>> {
172        self.name.as_ref()
173    }
174    /// Returns retained `x-` fields.
175    #[must_use]
176    pub fn extension_fields(&self) -> &[FieldReference] {
177        &self.extension_fields
178    }
179    /// Returns unrecognized fields.
180    #[must_use]
181    pub fn unknown_fields(&self) -> &[FieldReference] {
182        &self.unknown_fields
183    }
184}
185
186/// A service port with its authored short or long form retained.
187#[derive(Debug, Clone, PartialEq, Eq)]
188pub enum Port {
189    /// String or numeric scalar short syntax.
190    Short(ShortPort),
191    /// Mapping-based long syntax.
192    Long(Box<LongPort>),
193}
194
195impl Port {
196    /// Returns the complete port value span.
197    #[must_use]
198    pub const fn span(&self) -> SourceSpan {
199        match self {
200            Self::Short(value) => value.raw().span(),
201            Self::Long(value) => value.span(),
202        }
203    }
204}
205
206fn split_port_fields(value: &str) -> Vec<&str> {
207    if let Some(close) = value.find(']').filter(|_| value.starts_with('[')) {
208        let host = &value[..=close];
209        let suffix = value[close + 1..].strip_prefix(':').unwrap_or(&value[close + 1..]);
210        let mut fields = vec![host];
211        fields.extend(suffix.split(':'));
212        fields
213    } else {
214        value.split(':').collect()
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::ShortPort;
221    use crate::model::Located;
222    use crate::source::{SourceId, SourceSpan};
223
224    fn located(value: &str) -> Located<String> {
225        Located::new(
226            value.to_owned(),
227            SourceSpan::from_valid_offsets(SourceId::new(1), 0, value.len()),
228        )
229    }
230
231    #[test]
232    fn splits_bracketed_ipv6_and_protocol() {
233        let port = ShortPort::parse(located("[::1]:6001:6001/udp"));
234        assert_eq!(port.host_ip(), Some("[::1]"));
235        assert_eq!(port.published(), Some("6001"));
236        assert_eq!(port.target(), "6001");
237        assert_eq!(port.protocol(), Some("udp"));
238    }
239}