use std::collections::BTreeSet;
use std::str::FromStr;
pub const LABEL: &str = "dev.arcbox.http-port";
pub const HTTP_PORT: u16 = 80;
pub const HTTPS_PORT: u16 = 443;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pin {
Off,
Port(u16),
}
impl FromStr for Pin {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let value = value.trim();
if value.eq_ignore_ascii_case("off") {
return Ok(Self::Off);
}
match value.parse::<u16>() {
Ok(port) if port != 0 => Ok(Self::Port(port)),
_ => Err(format!(
"{LABEL}={value:?} is neither a port number nor \"off\""
)),
}
}
}
#[must_use]
pub fn choose(pin: Option<Pin>, listening: &BTreeSet<u16>, exposed: &BTreeSet<u16>) -> Option<u16> {
match pin {
Some(Pin::Off) => None,
Some(Pin::Port(port)) => Some(port),
None if listening.contains(&HTTP_PORT) => Some(HTTP_PORT),
None => {
let mut candidates = listening.iter().copied().filter(|&p| p != HTTPS_PORT);
candidates
.clone()
.find(|port| exposed.contains(port))
.or_else(|| candidates.next())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ports(list: &[u16]) -> BTreeSet<u16> {
list.iter().copied().collect()
}
#[test]
fn the_label_wins_over_what_the_container_listens_on() {
let listening = ports(&[80, 3000]);
assert_eq!(choose(Some(Pin::Off), &listening, &ports(&[])), None);
assert_eq!(
choose(Some(Pin::Port(8080)), &listening, &ports(&[])),
Some(8080)
);
}
#[test]
fn a_container_listening_on_80_keeps_it() {
assert_eq!(choose(None, &ports(&[3000, 80]), &ports(&[3000])), Some(80));
}
#[test]
fn an_exposed_listener_beats_a_lower_unexposed_one() {
assert_eq!(
choose(None, &ports(&[2112, 8080, 9000]), &ports(&[8080, 9000])),
Some(8080)
);
}
#[test]
fn without_exposed_listeners_the_lowest_port_serves() {
assert_eq!(choose(None, &ports(&[9229, 3000]), &ports(&[])), Some(3000));
assert_eq!(
choose(None, &ports(&[9229, 3000]), &ports(&[5000])),
Some(3000),
"an exposed port nothing listens on does not count"
);
}
#[test]
fn a_tls_listener_on_443_is_never_the_http_port() {
assert_eq!(choose(None, &ports(&[443]), &ports(&[443])), None);
assert_eq!(
choose(None, &ports(&[443, 8080]), &ports(&[443])),
Some(8080)
);
assert_eq!(choose(None, &ports(&[]), &ports(&[3000])), None);
}
#[test]
fn label_values_parse_strictly() {
assert_eq!("off".parse(), Ok(Pin::Off));
assert_eq!(" OFF ".parse(), Ok(Pin::Off));
assert_eq!("3000".parse(), Ok(Pin::Port(3000)));
assert!("0".parse::<Pin>().is_err());
assert!("65536".parse::<Pin>().is_err());
assert!("http".parse::<Pin>().is_err());
assert!("".parse::<Pin>().is_err());
}
}