1use crate::source::SourceSpan;
4
5use super::Located;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[non_exhaustive]
10pub enum ExposeScalarKind {
11 Number,
13 String,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum ExposeProtocol {
21 Tcp,
23 Udp,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct ExposePort {
30 start: String,
31 end: Option<String>,
32}
33
34impl ExposePort {
35 #[must_use]
37 pub fn start(&self) -> &str {
38 &self.start
39 }
40
41 #[must_use]
43 pub fn end(&self) -> Option<&str> {
44 self.end.as_deref()
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub enum ExposeItemKind {
52 Documented {
54 port: ExposePort,
56 protocol: Option<ExposeProtocol>,
58 },
59 Sctp {
61 port: ExposePort,
63 },
64 UnknownProtocol {
66 port: ExposePort,
68 protocol: String,
70 },
71 Expression,
73 Malformed,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct ExposeItem {
80 raw: Located<String>,
81 scalar_kind: ExposeScalarKind,
82 kind: ExposeItemKind,
83}
84
85impl ExposeItem {
86 pub(crate) fn parse(raw: Located<String>, scalar_kind: ExposeScalarKind) -> Self {
87 let kind = classify_expose_item(raw.value(), scalar_kind);
88 Self { raw, scalar_kind, kind }
89 }
90
91 #[must_use]
93 pub fn value(&self) -> &str {
94 self.raw.value()
95 }
96
97 #[must_use]
99 pub const fn span(&self) -> SourceSpan {
100 self.raw.span()
101 }
102
103 #[must_use]
105 pub const fn scalar_kind(&self) -> ExposeScalarKind {
106 self.scalar_kind
107 }
108
109 #[must_use]
111 pub const fn kind(&self) -> &ExposeItemKind {
112 &self.kind
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct Expose {
119 span: SourceSpan,
120 items: Vec<ExposeItem>,
121}
122
123impl Expose {
124 pub(crate) const fn new(span: SourceSpan, items: Vec<ExposeItem>) -> Self {
125 Self { span, items }
126 }
127
128 #[must_use]
130 pub const fn span(&self) -> SourceSpan {
131 self.span
132 }
133
134 #[must_use]
136 pub fn items(&self) -> &[ExposeItem] {
137 &self.items
138 }
139}
140
141pub(crate) fn classify_expose_item(value: &str, scalar_kind: ExposeScalarKind) -> ExposeItemKind {
142 if scalar_kind == ExposeScalarKind::String && value.contains('$') {
143 return ExposeItemKind::Expression;
144 }
145 let (port_value, protocol) = match value.split_once('/') {
146 Some((port, protocol)) if !protocol.is_empty() && !protocol.contains('/') => (port, Some(protocol)),
147 Some(_) => return ExposeItemKind::Malformed,
148 None => (value, None),
149 };
150 let Some(port) = parse_port(port_value) else {
151 return ExposeItemKind::Malformed;
152 };
153 match protocol {
154 None => ExposeItemKind::Documented { port, protocol: None },
155 Some("tcp") => ExposeItemKind::Documented {
156 port,
157 protocol: Some(ExposeProtocol::Tcp),
158 },
159 Some("udp") => ExposeItemKind::Documented {
160 port,
161 protocol: Some(ExposeProtocol::Udp),
162 },
163 Some("sctp") => ExposeItemKind::Sctp { port },
164 Some(protocol) => ExposeItemKind::UnknownProtocol {
165 port,
166 protocol: protocol.to_owned(),
167 },
168 }
169}
170
171pub(crate) fn valid_generated_expose_item(value: &str) -> bool {
172 matches!(
173 classify_expose_item(value, ExposeScalarKind::String),
174 ExposeItemKind::Documented { .. }
175 ) && !value.contains(['$', '\r', '\n', '\0'])
176}
177
178fn parse_port(value: &str) -> Option<ExposePort> {
179 let (start, end) = value
180 .split_once('-')
181 .map_or((value, None), |(start, end)| (start, Some(end)));
182 if !decimal(start) || end.is_some_and(|end| !decimal(end)) {
183 return None;
184 }
185 Some(ExposePort {
186 start: start.to_owned(),
187 end: end.map(str::to_owned),
188 })
189}
190
191fn decimal(value: &str) -> bool {
192 !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())
193}
194
195#[cfg(test)]
196mod tests {
197 use super::{ExposeItemKind, ExposeProtocol, ExposeScalarKind, classify_expose_item, valid_generated_expose_item};
198
199 #[test]
200 fn classifies_without_fixed_width_or_default_protocol_normalization() {
201 let huge = "18446744073709551616000000000000000000000000000000";
202 assert!(matches!(
203 classify_expose_item(huge, ExposeScalarKind::Number),
204 ExposeItemKind::Documented { protocol: None, .. }
205 ));
206 assert!(matches!(
207 classify_expose_item("080-090/tcp", ExposeScalarKind::String),
208 ExposeItemKind::Documented {
209 protocol: Some(ExposeProtocol::Tcp),
210 ..
211 }
212 ));
213 assert!(matches!(
214 classify_expose_item("53/udp", ExposeScalarKind::String),
215 ExposeItemKind::Documented {
216 protocol: Some(ExposeProtocol::Udp),
217 ..
218 }
219 ));
220 assert!(matches!(
221 classify_expose_item("80/sctp", ExposeScalarKind::String),
222 ExposeItemKind::Sctp { .. }
223 ));
224 assert!(matches!(
225 classify_expose_item("80/HTTP", ExposeScalarKind::String),
226 ExposeItemKind::UnknownProtocol { protocol, .. } if protocol == "HTTP"
227 ));
228 assert_eq!(
229 classify_expose_item("${PORT:-80}", ExposeScalarKind::String),
230 ExposeItemKind::Expression
231 );
232 for value in ["", "80-", "-90", "80-90-100", "80/", "80/tcp/extra", "port"] {
233 assert_eq!(
234 classify_expose_item(value, ExposeScalarKind::String),
235 ExposeItemKind::Malformed
236 );
237 }
238 }
239
240 #[test]
241 fn generated_items_accept_only_resolved_documented_grammar() {
242 for value in ["0", "80", "080-090", "53/udp", "80/tcp"] {
243 assert!(valid_generated_expose_item(value));
244 }
245 for value in ["", "$PORT", "80/sctp", "80/HTTP", "80-", "80\n90"] {
246 assert!(!valid_generated_expose_item(value));
247 }
248 }
249}