use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use std::str::FromStr;
const DEFAULT_PORT: i32 = 80;
const DEFAULT_LOCAL_PORT: i32 = 80;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Protocol {
#[default]
Tcp,
Udp,
}
impl FromStr for Protocol {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.trim().to_ascii_uppercase().as_str() {
"TCP" => Ok(Protocol::Tcp),
"UDP" => Ok(Protocol::Udp),
other => Err(anyhow!("unknown protocol {other:?}, expected TCP or UDP")),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct ServicePort {
pub port: i32,
pub local_port: i32,
pub protocol: Protocol,
}
impl ServicePort {
pub fn from_str_multi(port_spec: &str) -> Result<Vec<ServicePort>> {
port_spec.split(',').map(ServicePort::try_from).collect()
}
}
impl Default for ServicePort {
fn default() -> Self {
ServicePort {
port: DEFAULT_PORT,
local_port: DEFAULT_LOCAL_PORT,
protocol: Protocol::default(),
}
}
}
impl TryFrom<&str> for ServicePort {
type Error = anyhow::Error;
fn try_from(port_spec: &str) -> Result<Self> {
let (port_part, protocol) = match port_spec.split_once('/') {
Some((ports, proto)) => (ports, proto.parse()?),
None => (port_spec, Protocol::default()),
};
let (port, local_port) = match port_part.split_once(':') {
Some((remote, local)) => (remote.parse()?, local.parse()?),
None => {
let p: i32 = port_part.parse()?;
(p, p)
}
};
Ok(ServicePort {
port,
local_port,
protocol,
})
}
}
pub fn clean_name(name: &str) -> String {
let mut orig = String::from(name);
if orig == "innisfree" {
return orig;
}
orig = orig.replace("-innisfree", "");
orig = orig.replace("innisfree-", "");
let mut result = String::from("innisfree-");
result.push_str(&orig);
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn service_port_manual_creation() {
let s = ServicePort::default();
assert_eq!(s.port, 80);
assert_eq!(s.protocol, Protocol::Tcp);
}
#[test]
fn parse_web_ports() -> Result<()> {
let port_spec = "80/TCP,443/TCP";
let services = ServicePort::from_str_multi(port_spec)?;
assert_eq!(services.len(), 2);
let s1 = &services[0];
assert_eq!(s1.port, 80);
assert_eq!(s1.protocol, Protocol::Tcp);
let s2 = &services[1];
assert_eq!(s2.port, 443);
assert_eq!(s2.protocol, Protocol::Tcp);
Ok(())
}
#[test]
fn parse_different_ports() -> Result<()> {
let port_spec = "80:30080/TCP";
let s = ServicePort::try_from(port_spec)?;
assert_eq!(s.port, 80);
assert_eq!(s.local_port, 30080);
assert_eq!(s.protocol, Protocol::Tcp);
Ok(())
}
#[test]
fn parse_different_ports_multi() -> Result<()> {
let port_spec = "80:30080,443:30443";
let services = ServicePort::from_str_multi(port_spec)?;
assert_eq!(services.len(), 2);
let s1 = &services[0];
assert_eq!(s1.port, 80);
assert_eq!(s1.local_port, 30080);
assert_eq!(s1.protocol, Protocol::Tcp);
let s2 = &services[1];
assert_eq!(s2.port, 443);
assert_eq!(s2.local_port, 30443);
assert_eq!(s2.protocol, Protocol::Tcp);
Ok(())
}
#[test]
fn from_str_multi_propagates_parse_errors() {
let err = ServicePort::from_str_multi("80/TCP,not-a-port").unwrap_err();
assert!(
err.to_string().contains("invalid digit") || err.to_string().contains("not-a-port"),
"expected a parse error, got: {err}"
);
}
#[test]
fn parse_protocol_is_case_insensitive() -> Result<()> {
assert_eq!("tcp".parse::<Protocol>()?, Protocol::Tcp);
assert_eq!("Udp".parse::<Protocol>()?, Protocol::Udp);
assert!("sctp".parse::<Protocol>().is_err());
Ok(())
}
#[test]
fn protocol_serializes_as_uppercase() -> Result<()> {
let s = ServicePort {
port: 80,
local_port: 80,
protocol: Protocol::Udp,
};
let j = serde_json::to_string(&s)?;
assert!(j.contains("\"protocol\":\"UDP\""), "got: {j}");
Ok(())
}
#[test]
fn clean_service_name() {
let s_simple = "foo";
let r_simple = clean_name(s_simple);
assert!(r_simple == *"innisfree-foo");
let s_complex = "foo-innisfree";
let r_complex = clean_name(s_complex);
assert!(r_complex == *"innisfree-foo");
let s_default = "innisfree";
let r_default = clean_name(s_default);
assert!(r_default == *"innisfree");
}
}