use std::num::NonZeroUsize;
use tocat_api::normalize;
use crate::{
config::ByteSize,
endpoint::{
EndpointSpec,
exec::{Exec, System},
file::File,
pipe::Pipe,
stdio::Stdio,
sys::Mode,
tcp::{Tcp, TcpListen},
udp::{Udp, UdpListen},
unix::{Unix, UnixListen},
},
};
#[derive(Debug, PartialEq)]
pub enum ParseEndpointError {
Empty,
UnknownScheme(String),
UnsupportedOption {
scheme: &'static str,
option: String,
},
InvalidPort(String),
InvalidMode(String),
InvalidSize(String),
InvalidFlag(String),
MissingValue(String),
InvalidNumber(String),
}
impl std::fmt::Display for ParseEndpointError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseEndpointError::Empty => write!(f, "empty endpoint"),
ParseEndpointError::UnknownScheme(body) => write!(f, "unknown scheme: {body}"),
ParseEndpointError::UnsupportedOption { scheme, option } => {
write!(f, "unsupported option for {scheme}: {option}")
}
ParseEndpointError::InvalidPort(body) => write!(f, "invalid port: {body}"),
ParseEndpointError::InvalidMode(body) => write!(f, "invalid permissions: {body}"),
ParseEndpointError::InvalidSize(body) => write!(f, "invalid size: {body}"),
ParseEndpointError::InvalidFlag(body) => write!(f, "invalid flag: {body}"),
ParseEndpointError::MissingValue(body) => write!(f, "missing value: {body}"),
ParseEndpointError::InvalidNumber(body) => write!(f, "invalid number: {body}"),
}
}
}
impl std::error::Error for ParseEndpointError {}
pub(super) struct Opt<'a> {
pub(super) key: &'a str,
value: Option<&'a str>,
}
impl<'a> Opt<'a> {
pub(super) fn flag(&self) -> Result<bool, ParseEndpointError> {
match self.value {
None => Ok(true),
Some(v) => v
.parse()
.map_err(|_| ParseEndpointError::InvalidFlag(v.to_string())),
}
}
pub(super) fn text(&self) -> Result<&'a str, ParseEndpointError> {
self.value
.ok_or_else(|| ParseEndpointError::MissingValue(self.key.to_string()))
}
pub(super) fn string(&self) -> Result<String, ParseEndpointError> {
self.text().map(str::to_string)
}
pub(super) fn size(&self) -> Result<ByteSize, ParseEndpointError> {
self.text()?
.parse()
.map_err(|e| ParseEndpointError::InvalidSize(format!("{e}")))
}
pub(super) fn mode(&self) -> Result<Mode, ParseEndpointError> {
self.text()?.parse()
}
pub(super) fn count(&self) -> Result<NonZeroUsize, ParseEndpointError> {
self.text()?
.parse()
.map_err(|_| ParseEndpointError::InvalidNumber(self.key.to_string()))
}
pub(super) fn unsupported(&self, scheme: &'static str) -> ParseEndpointError {
ParseEndpointError::UnsupportedOption {
scheme,
option: self.key.to_string(),
}
}
}
pub(super) fn options<'a>(parts: std::str::Split<'a, char>) -> impl Iterator<Item = Opt<'a>> {
parts.map(|opt| match opt.split_once('=') {
Some((key, value)) => Opt {
key,
value: Some(value),
},
None => Opt {
key: opt,
value: None,
},
})
}
pub(super) fn host_port(body: &str) -> Result<(Option<String>, Option<u16>), ParseEndpointError> {
let (host, port) = if body.is_empty() {
(None, None)
} else if let Some((h, p)) = body.rsplit_once(':') {
let parsed_port = p
.parse::<u16>()
.map_err(|_| ParseEndpointError::InvalidPort(p.to_string()))?;
let host_opt = if h.is_empty() {
None
} else {
Some(h.to_string())
};
(host_opt, Some(parsed_port))
} else if let Ok(parsed_port) = body.parse::<u16>() {
(None, Some(parsed_port))
} else {
(Some(body.to_string()), None)
};
Ok((host, port))
}
impl std::str::FromStr for EndpointSpec {
type Err = ParseEndpointError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Err(Self::Err::Empty);
}
if s == "-" {
return Ok(Self::Stdio(Stdio { name: None }));
}
let mut parts = s.split(',');
let target = parts.next().unwrap_or("");
let opts = options(parts);
let (scheme, body) = target.split_once(':').unwrap_or((target, ""));
match normalize(scheme).as_str() {
"exec" => Exec::parse(body, opts).map(Self::Exec),
"file" | "open" => File::parse(body, opts).map(Self::File),
"pipe" | "fifo" => Pipe::parse(body, opts).map(Self::Pipe),
"stdio" => Stdio::parse(body, opts).map(Self::Stdio),
"system" => System::parse(body, opts).map(Self::System),
"tcp" | "tcpconnect" | "connect" => Tcp::parse(body, opts).map(Self::Tcp),
"tcplisten" | "listen" => TcpListen::parse(body, opts).map(Self::TcpListen),
"udp" | "udpconnect" => Udp::parse(body, opts).map(Self::Udp),
"udplisten" => UdpListen::parse(body, opts).map(Self::UdpListen),
"unix" | "unix-connect" => Unix::parse(body, opts).map(Self::Unix),
"unixlisten" => UnixListen::parse(body, opts).map(Self::UnixListen),
other => Err(Self::Err::UnknownScheme(other.to_owned())),
}
}
}