use std::borrow::Cow;
use std::fmt::Display;
use std::iter;
use std::net;
use std::num::NonZeroU16;
use educe::Educe;
use either::Either;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString, IntoStaticStr};
#[derive(Clone, Copy, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)]
#[allow(clippy::exhaustive_enums)] #[derive(Serialize, Deserialize)]
#[serde(try_from = "BoolOrAutoSerde", into = "BoolOrAutoSerde")]
#[derive(Educe)]
#[educe(Default)]
pub enum BoolOrAuto {
#[educe(Default)]
Auto,
Explicit(bool),
}
impl BoolOrAuto {
pub fn as_bool(self) -> Option<bool> {
match self {
BoolOrAuto::Auto => None,
BoolOrAuto::Explicit(v) => Some(v),
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum BoolOrAutoSerde {
String(Cow<'static, str>),
Bool(bool),
}
impl From<BoolOrAuto> for BoolOrAutoSerde {
fn from(boa: BoolOrAuto) -> BoolOrAutoSerde {
use BoolOrAutoSerde as BoAS;
boa.as_bool()
.map(BoAS::Bool)
.unwrap_or_else(|| BoAS::String("auto".into()))
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
#[error(r#"Invalid value, expected boolean or "auto""#)]
pub struct InvalidBoolOrAuto {}
impl TryFrom<BoolOrAutoSerde> for BoolOrAuto {
type Error = InvalidBoolOrAuto;
fn try_from(pls: BoolOrAutoSerde) -> Result<BoolOrAuto, Self::Error> {
use BoolOrAuto as BoA;
use BoolOrAutoSerde as BoAS;
Ok(match pls {
BoAS::Bool(v) => BoA::Explicit(v),
BoAS::String(s) if s == "false" => BoA::Explicit(false),
BoAS::String(s) if s == "true" => BoA::Explicit(true),
BoAS::String(s) if s == "auto" => BoA::Auto,
_ => return Err(InvalidBoolOrAuto {}),
})
}
}
#[derive(Clone, Copy, Hash, Debug, Ord, PartialOrd, Eq, PartialEq)]
#[allow(clippy::exhaustive_enums)] #[derive(Serialize, Deserialize)]
#[serde(try_from = "PaddingLevelSerde", into = "PaddingLevelSerde")]
#[derive(Display, EnumString, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
#[derive(Educe)]
#[educe(Default)]
pub enum PaddingLevel {
None,
Reduced,
#[educe(Default)]
Normal,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum PaddingLevelSerde {
String(Cow<'static, str>),
Bool(bool),
}
impl From<PaddingLevel> for PaddingLevelSerde {
fn from(pl: PaddingLevel) -> PaddingLevelSerde {
PaddingLevelSerde::String(<&str>::from(&pl).into())
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
#[error("Invalid padding level")]
struct InvalidPaddingLevel {}
impl TryFrom<PaddingLevelSerde> for PaddingLevel {
type Error = InvalidPaddingLevel;
fn try_from(pls: PaddingLevelSerde) -> Result<PaddingLevel, Self::Error> {
Ok(match pls {
PaddingLevelSerde::String(s) => {
s.as_ref().try_into().map_err(|_| InvalidPaddingLevel {})?
}
PaddingLevelSerde::Bool(false) => PaddingLevel::None,
PaddingLevelSerde::Bool(true) => PaddingLevel::Normal,
})
}
}
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "ListenSerde", into = "ListenSerde")]
#[derive(Educe)]
#[educe(Default)]
pub struct Listen(Vec<ListenItem>);
impl Listen {
pub fn new_none() -> Listen {
Listen(vec![])
}
pub fn new_localhost(port: u16) -> Listen {
Listen(
port.try_into()
.ok()
.map(ListenItem::Localhost)
.into_iter()
.collect_vec(),
)
}
pub fn new_localhost_optional(port: Option<u16>) -> Listen {
Self::new_localhost(port.unwrap_or_default())
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn ip_addrs(
&self,
) -> Result<
impl Iterator<Item = impl Iterator<Item = net::SocketAddr> + '_> + '_,
ListenUnsupported,
> {
Ok(self.0.iter().map(|i| i.iter()))
}
pub fn localhost_port_legacy(&self) -> Result<Option<u16>, ListenUnsupported> {
use ListenItem as LI;
Ok(match &*self.0 {
[] => None,
[LI::Localhost(port)] => Some((*port).into()),
_ => return Err(ListenUnsupported {}),
})
}
}
impl Display for Listen {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let mut sep = "";
for a in &self.0 {
write!(f, "{sep}{a}")?;
sep = ", ";
}
Ok(())
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
#[error("Unsupported listening configuration")]
pub struct ListenUnsupported {}
#[derive(Clone, Hash, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
enum ListenItem {
Localhost(NonZeroU16),
General(net::SocketAddr),
}
impl ListenItem {
fn iter(&self) -> impl Iterator<Item = net::SocketAddr> + '_ {
use net::{IpAddr, Ipv4Addr, Ipv6Addr};
use ListenItem as LI;
match self {
&LI::Localhost(port) => Either::Left({
let port = port.into();
let addrs: [IpAddr; 2] = [Ipv6Addr::LOCALHOST.into(), Ipv4Addr::LOCALHOST.into()];
addrs
.into_iter()
.map(move |ip| net::SocketAddr::new(ip, port))
}),
LI::General(addr) => Either::Right(iter::once(addr).cloned()),
}
}
}
impl Display for ListenItem {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ListenItem::Localhost(port) => write!(f, "localhost port {}", port)?,
ListenItem::General(addr) => write!(f, "{}", addr)?,
}
Ok(())
}
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum ListenSerde {
Bool(bool),
One(ListenItemSerde),
List(Vec<ListenItemSerde>),
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum ListenItemSerde {
Port(u16),
String(String),
}
#[allow(clippy::fallible_impl_from)]
impl From<Listen> for ListenSerde {
fn from(l: Listen) -> ListenSerde {
let l = l.0;
match l.len() {
0 => ListenSerde::Bool(false),
1 => ListenSerde::One(l.into_iter().next().expect("len=1 but no next").into()),
_ => ListenSerde::List(l.into_iter().map(Into::into).collect()),
}
}
}
impl From<ListenItem> for ListenItemSerde {
fn from(i: ListenItem) -> ListenItemSerde {
use ListenItem as LI;
use ListenItemSerde as LIS;
match i {
LI::Localhost(port) => LIS::Port(port.into()),
LI::General(addr) => LIS::String(addr.to_string()),
}
}
}
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
pub enum InvalidListen {
#[error("Invalid listen specification: need actual addr/port, or `false`; not `true`")]
InvalidBool,
#[error("Invalid listen specification: failed to parse string: {0}")]
InvalidString(#[from] net::AddrParseError),
#[error("Invalid listen specification: zero (for no port) not permitted in list")]
ZeroPortInList,
}
impl TryFrom<ListenSerde> for Listen {
type Error = InvalidListen;
fn try_from(l: ListenSerde) -> Result<Listen, Self::Error> {
use ListenSerde as LS;
Ok(Listen(match l {
LS::Bool(false) => vec![],
LS::Bool(true) => return Err(InvalidListen::InvalidBool),
LS::One(i) if i.means_none() => vec![],
LS::One(i) => vec![i.try_into()?],
LS::List(l) => l.into_iter().map(|i| i.try_into()).try_collect()?,
}))
}
}
impl ListenItemSerde {
fn means_none(&self) -> bool {
use ListenItemSerde as LIS;
match self {
&LIS::Port(port) => port == 0,
LIS::String(s) => s.is_empty(),
}
}
}
impl TryFrom<ListenItemSerde> for ListenItem {
type Error = InvalidListen;
fn try_from(i: ListenItemSerde) -> Result<ListenItem, Self::Error> {
use ListenItem as LI;
use ListenItemSerde as LIS;
Ok(match i {
LIS::String(s) => LI::General(s.parse()?),
LIS::Port(p) => LI::Localhost(p.try_into().map_err(|_| InvalidListen::ZeroPortInList)?),
})
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
#[derive(Debug, Deserialize, Serialize)]
struct TestConfigFile {
#[serde(default)]
something_enabled: BoolOrAuto,
#[serde(default)]
padding: PaddingLevel,
#[serde(default)]
listen: Option<Listen>,
}
#[test]
fn bool_or_auto() {
use BoolOrAuto as BoA;
let chk = |pl, s| {
let tc: TestConfigFile = toml::from_str(s).expect(s);
assert_eq!(pl, tc.something_enabled, "{:?}", s);
};
chk(BoA::Auto, "");
chk(BoA::Auto, r#"something_enabled = "auto""#);
chk(BoA::Explicit(true), r#"something_enabled = true"#);
chk(BoA::Explicit(true), r#"something_enabled = "true""#);
chk(BoA::Explicit(false), r#"something_enabled = false"#);
chk(BoA::Explicit(false), r#"something_enabled = "false""#);
let chk_e = |s| {
let tc: Result<TestConfigFile, _> = toml::from_str(s);
let _ = tc.expect_err(s);
};
chk_e(r#"something_enabled = 1"#);
chk_e(r#"something_enabled = "unknown""#);
chk_e(r#"something_enabled = "True""#);
}
#[test]
fn padding_level() {
use PaddingLevel as PL;
let chk = |pl, s| {
let tc: TestConfigFile = toml::from_str(s).expect(s);
assert_eq!(pl, tc.padding, "{:?}", s);
};
chk(PL::None, r#"padding = "none""#);
chk(PL::None, r#"padding = false"#);
chk(PL::Reduced, r#"padding = "reduced""#);
chk(PL::Normal, r#"padding = "normal""#);
chk(PL::Normal, r#"padding = true"#);
chk(PL::Normal, "");
let chk_e = |s| {
let tc: Result<TestConfigFile, _> = toml::from_str(s);
let _ = tc.expect_err(s);
};
chk_e(r#"padding = 1"#);
chk_e(r#"padding = "unknown""#);
chk_e(r#"padding = "Normal""#);
}
#[test]
fn listen_parse() {
use net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use ListenItem as LI;
let localhost6 = |p| SocketAddr::new(Ipv6Addr::LOCALHOST.into(), p);
let localhost4 = |p| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), p);
let unspec6 = |p| SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), p);
#[allow(clippy::needless_pass_by_value)] fn chk(
exp_i: Vec<ListenItem>,
exp_addrs: Result<Vec<Vec<SocketAddr>>, ()>,
exp_lpd: Result<Option<u16>, ()>,
s: &str,
) {
let tc: TestConfigFile = toml::from_str(s).expect(s);
let ll = tc.listen.unwrap();
eprintln!("s={:?} ll={:?}", &s, &ll);
assert_eq!(ll, Listen(exp_i));
assert_eq!(
ll.ip_addrs()
.map(|a| a.map(|l| l.collect_vec()).collect_vec())
.map_err(|_| ()),
exp_addrs
);
assert_eq!(ll.localhost_port_legacy().map_err(|_| ()), exp_lpd);
}
let chk_err = |exp, s: &str| {
let got: Result<TestConfigFile, _> = toml::from_str(s);
let got = got.expect_err(s).to_string();
assert!(got.contains(exp), "s={:?} got={:?} exp={:?}", s, got, exp);
};
let chk_none = |s: &str| {
chk(vec![], Ok(vec![]), Ok(None), &format!("listen = {}", s));
chk_err(
"", &format!("listen = [ {} ]", s),
);
};
let chk_1 = |v: ListenItem, addrs: Vec<Vec<SocketAddr>>, port, s| {
chk(
vec![v.clone()],
Ok(addrs.clone()),
port,
&format!("listen = {}", s),
);
chk(
vec![v.clone()],
Ok(addrs.clone()),
port,
&format!("listen = [ {} ]", s),
);
chk(
vec![v, LI::Localhost(23.try_into().unwrap())],
Ok([addrs, vec![vec![localhost6(23), localhost4(23)]]]
.into_iter()
.flatten()
.collect()),
Err(()),
&format!("listen = [ {}, 23 ]", s),
);
};
chk_none(r#""""#);
chk_none(r#"0"#);
chk_none(r#"false"#);
chk(vec![], Ok(vec![]), Ok(None), r#"listen = []"#);
chk_1(
LI::Localhost(42.try_into().unwrap()),
vec![vec![localhost6(42), localhost4(42)]],
Ok(Some(42)),
"42",
);
chk_1(
LI::General(unspec6(56)),
vec![vec![unspec6(56)]],
Err(()),
r#""[::]:56""#,
);
let chk_err_1 = |e, el, s| {
chk_err(e, &format!("listen = {}", s));
chk_err(el, &format!("listen = [ {} ]", s));
chk_err(el, &format!("listen = [ 23, {}, 77 ]", s));
};
chk_err_1("need actual addr/port", "did not match any variant", "true");
chk_err("did not match any variant", r#"listen = [ [] ]"#);
}
#[test]
fn display_listen() {
let empty = Listen::new_none();
assert_eq!(empty.to_string(), "");
let one_port = Listen::new_localhost(1234);
assert_eq!(one_port.to_string(), "localhost port 1234");
let multi_port = Listen(vec![
ListenItem::Localhost(1111.try_into().unwrap()),
ListenItem::Localhost(2222.try_into().unwrap()),
]);
assert_eq!(
multi_port.to_string(),
"localhost port 1111, localhost port 2222"
);
let multi_addr = Listen(vec![
ListenItem::Localhost(1234.try_into().unwrap()),
ListenItem::General("1.2.3.4:5678".parse().unwrap()),
]);
assert_eq!(multi_addr.to_string(), "localhost port 1234, 1.2.3.4:5678");
}
}