use crate::source::SourceSpan;
use super::Located;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ExposeScalarKind {
Number,
String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ExposeProtocol {
Tcp,
Udp,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExposePort {
start: String,
end: Option<String>,
}
impl ExposePort {
#[must_use]
pub fn start(&self) -> &str {
&self.start
}
#[must_use]
pub fn end(&self) -> Option<&str> {
self.end.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExposeItemKind {
Documented {
port: ExposePort,
protocol: Option<ExposeProtocol>,
},
Sctp {
port: ExposePort,
},
UnknownProtocol {
port: ExposePort,
protocol: String,
},
Expression,
Malformed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExposeItem {
raw: Located<String>,
scalar_kind: ExposeScalarKind,
kind: ExposeItemKind,
}
impl ExposeItem {
pub(crate) fn parse(raw: Located<String>, scalar_kind: ExposeScalarKind) -> Self {
let kind = classify_expose_item(raw.value(), scalar_kind);
Self { raw, scalar_kind, kind }
}
#[must_use]
pub fn value(&self) -> &str {
self.raw.value()
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.raw.span()
}
#[must_use]
pub const fn scalar_kind(&self) -> ExposeScalarKind {
self.scalar_kind
}
#[must_use]
pub const fn kind(&self) -> &ExposeItemKind {
&self.kind
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Expose {
span: SourceSpan,
items: Vec<ExposeItem>,
}
impl Expose {
pub(crate) const fn new(span: SourceSpan, items: Vec<ExposeItem>) -> Self {
Self { span, items }
}
#[must_use]
pub const fn span(&self) -> SourceSpan {
self.span
}
#[must_use]
pub fn items(&self) -> &[ExposeItem] {
&self.items
}
}
pub(crate) fn classify_expose_item(value: &str, scalar_kind: ExposeScalarKind) -> ExposeItemKind {
if scalar_kind == ExposeScalarKind::String && value.contains('$') {
return ExposeItemKind::Expression;
}
let (port_value, protocol) = match value.split_once('/') {
Some((port, protocol)) if !protocol.is_empty() && !protocol.contains('/') => (port, Some(protocol)),
Some(_) => return ExposeItemKind::Malformed,
None => (value, None),
};
let Some(port) = parse_port(port_value) else {
return ExposeItemKind::Malformed;
};
match protocol {
None => ExposeItemKind::Documented { port, protocol: None },
Some("tcp") => ExposeItemKind::Documented {
port,
protocol: Some(ExposeProtocol::Tcp),
},
Some("udp") => ExposeItemKind::Documented {
port,
protocol: Some(ExposeProtocol::Udp),
},
Some("sctp") => ExposeItemKind::Sctp { port },
Some(protocol) => ExposeItemKind::UnknownProtocol {
port,
protocol: protocol.to_owned(),
},
}
}
pub(crate) fn valid_generated_expose_item(value: &str) -> bool {
matches!(
classify_expose_item(value, ExposeScalarKind::String),
ExposeItemKind::Documented { .. }
) && !value.contains(['$', '\r', '\n', '\0'])
}
fn parse_port(value: &str) -> Option<ExposePort> {
let (start, end) = value
.split_once('-')
.map_or((value, None), |(start, end)| (start, Some(end)));
if !decimal(start) || end.is_some_and(|end| !decimal(end)) {
return None;
}
Some(ExposePort {
start: start.to_owned(),
end: end.map(str::to_owned),
})
}
fn decimal(value: &str) -> bool {
!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_digit())
}
#[cfg(test)]
mod tests {
use super::{ExposeItemKind, ExposeProtocol, ExposeScalarKind, classify_expose_item, valid_generated_expose_item};
#[test]
fn classifies_without_fixed_width_or_default_protocol_normalization() {
let huge = "18446744073709551616000000000000000000000000000000";
assert!(matches!(
classify_expose_item(huge, ExposeScalarKind::Number),
ExposeItemKind::Documented { protocol: None, .. }
));
assert!(matches!(
classify_expose_item("080-090/tcp", ExposeScalarKind::String),
ExposeItemKind::Documented {
protocol: Some(ExposeProtocol::Tcp),
..
}
));
assert!(matches!(
classify_expose_item("53/udp", ExposeScalarKind::String),
ExposeItemKind::Documented {
protocol: Some(ExposeProtocol::Udp),
..
}
));
assert!(matches!(
classify_expose_item("80/sctp", ExposeScalarKind::String),
ExposeItemKind::Sctp { .. }
));
assert!(matches!(
classify_expose_item("80/HTTP", ExposeScalarKind::String),
ExposeItemKind::UnknownProtocol { protocol, .. } if protocol == "HTTP"
));
assert_eq!(
classify_expose_item("${PORT:-80}", ExposeScalarKind::String),
ExposeItemKind::Expression
);
for value in ["", "80-", "-90", "80-90-100", "80/", "80/tcp/extra", "port"] {
assert_eq!(
classify_expose_item(value, ExposeScalarKind::String),
ExposeItemKind::Malformed
);
}
}
#[test]
fn generated_items_accept_only_resolved_documented_grammar() {
for value in ["0", "80", "080-090", "53/udp", "80/tcp"] {
assert!(valid_generated_expose_item(value));
}
for value in ["", "$PORT", "80/sctp", "80/HTTP", "80-", "80\n90"] {
assert!(!valid_generated_expose_item(value));
}
}
}