use bollard::models::PortBinding;
use std::collections::HashMap;
use crate::compose::types::{PortMapping, StringOrU16};
use crate::error::{ComposeError, Result};
#[derive(Debug, Clone)]
pub struct ParsedPort {
pub container_port: u16,
pub protocol: String,
pub host_ip: String,
pub host_port: Option<u16>,
}
pub fn parse_ports(ports: &[PortMapping]) -> Result<Vec<ParsedPort>> {
let mut result = Vec::new();
for mapping in ports {
result.extend(parse_one(mapping)?);
}
Ok(result)
}
#[allow(clippy::type_complexity)]
pub fn to_bollard(
ports: &[ParsedPort],
) -> (
HashMap<String, Option<Vec<PortBinding>>>,
HashMap<String, HashMap<(), ()>>,
) {
let mut port_bindings: HashMap<String, Option<Vec<PortBinding>>> = HashMap::new();
let mut exposed_ports: HashMap<String, HashMap<(), ()>> = HashMap::new();
for p in ports {
let key = format!("{}/{}", p.container_port, p.protocol);
let host_ip = if p.host_ip.is_empty() {
"0.0.0.0".to_string()
} else {
p.host_ip.clone()
};
let host_port = match p.host_port {
Some(0) => Some(String::new()),
Some(n) => Some(n.to_string()),
None => None,
};
let binding = PortBinding {
host_ip: Some(host_ip),
host_port,
};
let bindings = port_bindings
.entry(key.clone())
.or_insert_with(|| Some(Vec::new()));
if let Some(v) = bindings {
v.push(binding);
}
exposed_ports.entry(key).or_default();
}
(port_bindings, exposed_ports)
}
fn parse_one(mapping: &PortMapping) -> Result<Vec<ParsedPort>> {
match mapping {
PortMapping::Short(s) => parse_short(s),
PortMapping::Long {
target,
published,
protocol,
host_ip,
..
} => {
let proto = protocol.clone().unwrap_or_else(|| "tcp".into());
let hip = host_ip.clone().unwrap_or_default();
let host_port = published
.as_ref()
.map(|p| match p {
StringOrU16::Number(n) => Ok(*n),
StringOrU16::String(s) => s.parse::<u16>().map_err(|_| {
ComposeError::InvalidPort(format!("invalid published port: {s}"))
}),
})
.transpose()?;
Ok(vec![ParsedPort {
container_port: *target,
protocol: proto,
host_ip: hip,
host_port,
}])
}
}
}
fn parse_short(s: &str) -> Result<Vec<ParsedPort>> {
let (rest, proto) = if let Some(idx) = s.rfind('/') {
(&s[..idx], s[idx + 1..].to_string())
} else {
(s, "tcp".to_string())
};
if let Some(rest) = rest.strip_prefix('[') {
let close = rest
.find(']')
.ok_or_else(|| ComposeError::InvalidPort(format!("unclosed `[` in {s}")))?;
let ip = &rest[..close];
let after = &rest[close + 1..];
let after = after.strip_prefix(':').unwrap_or(after);
return parse_with_ip(ip, after, &proto, s);
}
let colon_count = rest.chars().filter(|&c| c == ':').count();
match colon_count {
0 => {
let ports = expand_port_range(rest)?;
Ok(ports
.into_iter()
.map(|cp| ParsedPort {
container_port: cp,
protocol: proto.clone(),
host_ip: String::new(),
host_port: None,
})
.collect())
}
1 => {
let (left, right) = split_last_colon(rest);
let host_ports = expand_port_range(left)?;
let container_ports = expand_port_range(right)?;
if host_ports.len() != container_ports.len() && host_ports.len() != 1 {
return Err(ComposeError::InvalidPort(format!(
"port range mismatch: {s}"
)));
}
Ok(host_ports
.into_iter()
.zip(container_ports)
.map(|(hp, cp)| ParsedPort {
container_port: cp,
protocol: proto.clone(),
host_ip: String::new(),
host_port: Some(hp),
})
.collect())
}
_ => {
let parts: Vec<&str> = rest.splitn(3, ':').collect();
if parts.len() < 3 {
return Err(ComposeError::InvalidPort(format!("invalid port spec: {s}")));
}
parse_with_ip(parts[0], &format!("{}:{}", parts[1], parts[2]), &proto, s)
}
}
}
fn parse_with_ip(ip: &str, after: &str, proto: &str, full: &str) -> Result<Vec<ParsedPort>> {
if let Some((left, right)) = after.split_once(':') {
let host_ports = expand_port_range(left)?;
let container_ports = expand_port_range(right)?;
if host_ports.len() != container_ports.len() && host_ports.len() != 1 {
return Err(ComposeError::InvalidPort(format!(
"port range mismatch: {full}"
)));
}
Ok(host_ports
.into_iter()
.zip(container_ports)
.map(|(hp, cp)| ParsedPort {
container_port: cp,
protocol: proto.to_string(),
host_ip: ip.to_string(),
host_port: Some(hp),
})
.collect())
} else {
let cp: u16 = after
.parse()
.map_err(|_| ComposeError::InvalidPort(format!("bad port: {full}")))?;
Ok(vec![ParsedPort {
container_port: cp,
protocol: proto.to_string(),
host_ip: ip.to_string(),
host_port: None,
}])
}
}
fn split_last_colon(s: &str) -> (&str, &str) {
if let Some(idx) = s.rfind(':') {
(&s[..idx], &s[idx + 1..])
} else {
("", s)
}
}
const MAX_PORT_RANGE: usize = 1024;
fn expand_port_range(s: &str) -> Result<Vec<u16>> {
let s = s.trim();
if let Some(idx) = s.find('-') {
let start: u16 = s[..idx]
.parse()
.map_err(|_| ComposeError::InvalidPort(format!("bad port: {s}")))?;
let end: u16 = s[idx + 1..]
.parse()
.map_err(|_| ComposeError::InvalidPort(format!("bad port: {s}")))?;
if start > end {
return Err(ComposeError::InvalidPort(format!(
"start > end in range: {s}"
)));
}
let count = (end as usize) - (start as usize) + 1;
if count > MAX_PORT_RANGE {
return Err(ComposeError::InvalidPort(format!(
"port range too large ({count} ports, max {MAX_PORT_RANGE}): {s}"
)));
}
Ok((start..=end).collect())
} else {
let p: u16 = s
.parse()
.map_err(|_| ComposeError::InvalidPort(format!("bad port: {s}")))?;
Ok(vec![p])
}
}