use std::{
num::NonZeroUsize,
sync::atomic::{AtomicBool, Ordering},
};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use tocat_api::normalize;
use tokio_seqpacket::{UnixSeqpacket as SeqpacketSocket, UnixSeqpacketListener};
use tracing::{debug, info, warn};
use crate::endpoint::{
Connection, EndpointStream,
parse::{Opt, ParseEndpointError},
sys::Mode,
unix::{SocketPath, apply_mode, unlink_stale},
};
#[derive(Debug, Deserialize, Serialize)]
pub struct UnixSeqpacket {
pub path: SocketPath,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct UnixSeqpacketListen {
pub path: SocketPath,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub fork: bool,
#[serde(default, rename = "max-connections")]
pub max_connections: Option<NonZeroUsize>,
#[serde(default)]
pub unlink: bool,
#[serde(default)]
pub mode: Option<Mode>,
}
impl UnixSeqpacket {
const SCHEME: &'static str = "unix-seqpacket";
pub(in crate::endpoint) fn parse<'a>(
body: &str,
opts: impl Iterator<Item = Opt<'a>>,
) -> Result<Self, ParseEndpointError> {
let mut name = None;
for opt in opts {
match normalize(opt.key).as_str() {
"name" => name = Some(opt.string()?),
_ => return Err(opt.unsupported(Self::SCHEME)),
}
}
Ok(Self {
path: SocketPath::from_spec(body),
name,
})
}
pub(in crate::endpoint) fn label(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| format!("unix-seqpacket://{}", self.path))
}
pub(in crate::endpoint) async fn connect(&self) -> anyhow::Result<Connection> {
self.path.supported()?;
let socket = SeqpacketSocket::connect(self.path.as_path())
.await
.with_context(|| format!("connecting to {}", self.path))?;
Ok(EndpointStream::seqpacket(socket).into_connection())
}
}
impl UnixSeqpacketListen {
const SCHEME: &'static str = "unix-seqpacket-listen";
pub(in crate::endpoint) fn parse<'a>(
body: &str,
opts: impl Iterator<Item = Opt<'a>>,
) -> Result<Self, ParseEndpointError> {
let mut name = None;
let mut fork = false;
let mut max_connections = None;
let mut unlink = false;
let mut mode = None;
for opt in opts {
match normalize(opt.key).as_str() {
"fork" => fork = opt.flag()?,
"maxconnections" | "maxconn" => {
max_connections = Some(opt.count()?);
}
"mode" => mode = Some(opt.mode()?),
"name" => name = Some(opt.string()?),
"unlink" => unlink = opt.flag()?,
_ => return Err(opt.unsupported(Self::SCHEME)),
}
}
Ok(Self {
path: SocketPath::from_spec(body),
name,
fork,
max_connections,
unlink,
mode,
})
}
pub(in crate::endpoint) fn label(&self) -> String {
self.name
.clone()
.unwrap_or_else(|| format!("unix-seqpacket://{}", self.path))
}
pub async fn bind(&self) -> anyhow::Result<UnixSeqpacketListener> {
self.path.supported()?;
if self.unlink {
unlink_stale(&self.path, async {
SeqpacketSocket::connect(self.path.as_path())
.await
.map(drop)
})
.await?;
}
let listener = UnixSeqpacketListener::bind(self.path.as_path())
.with_context(|| format!("binding {}", self.path))?;
apply_mode(&self.path, self.mode)?;
Ok(listener)
}
pub(in crate::endpoint) async fn connect(&self) -> anyhow::Result<Connection> {
let mut listener = self.bind().await?;
let guard = self.path.guard();
info!(path = %self.path, "listening");
let socket = listener.accept().await?;
Ok(EndpointStream::seqpacket(socket).into_connection_with_guard(guard))
}
}
pub struct SeqpacketConn {
socket: SeqpacketSocket,
truncated: AtomicBool,
}
impl SeqpacketConn {
pub(in crate::endpoint) fn new(socket: SeqpacketSocket) -> Self {
Self {
socket,
truncated: AtomicBool::new(false),
}
}
pub(in crate::endpoint) async fn recv(&self, buf: &mut [u8]) -> std::io::Result<Option<usize>> {
let message = self.socket.recv(buf).await?;
if message.truncated() {
self.report_truncation(buf.len());
}
let bytes = message.bytes_read();
Ok((bytes > 0).then_some(bytes))
}
pub(in crate::endpoint) async fn send(&self, buf: &[u8]) -> std::io::Result<usize> {
self.socket.send(buf).await
}
pub(in crate::endpoint) fn finish(&self) {
let _ = self.socket.shutdown(std::net::Shutdown::Write);
}
fn report_truncation(&self, buffer: usize) {
if self.truncated.swap(true, Ordering::Relaxed) {
debug!(buffer, "truncated a message that did not fit the buffer");
} else {
warn!(
buffer,
"a message did not fit the buffer and the rest of it is lost; raise -b past the \
largest message the peer sends. Further truncations are logged at debug",
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::endpoint::EndpointSpec;
fn dial(s: &str) -> UnixSeqpacket {
match s.parse::<EndpointSpec>().expect("parses") {
EndpointSpec::UnixSeqpacket(e) => e,
other => panic!("wrong variant: {other:?}"),
}
}
fn listen(s: &str) -> UnixSeqpacketListen {
match s.parse::<EndpointSpec>().expect("parses") {
EndpointSpec::UnixSeqpacketListen(e) => e,
other => panic!("wrong variant: {other:?}"),
}
}
#[test]
fn the_scheme_answers_to_its_spellings() {
for spec in [
"unix-seqpacket:/tmp/tocat.sock",
"unix-seqpkt:/tmp/tocat.sock",
"uds-seqpacket:/tmp/tocat.sock",
"seqpacket:/tmp/tocat.sock",
] {
assert_eq!(dial(spec).path, SocketPath::from_spec("/tmp/tocat.sock"));
}
for spec in [
"unix-seqpacket-listen:/tmp/tocat.sock",
"seqpacket-listen:/tmp/tocat.sock",
] {
assert!(!listen(spec).fork);
}
}
#[test]
fn the_listening_options_match_the_stream_form() {
let e = listen("unix-seqpacket-listen:@tocat,fork,max-connections=8");
assert!(e.fork);
assert!(e.path.is_abstract());
assert_eq!(e.max_connections, NonZeroUsize::new(8));
}
#[test]
fn dialling_rejects_the_listening_options() {
assert!(matches!(
"unix-seqpacket:/tmp/tocat.sock,unlink"
.parse::<EndpointSpec>()
.expect_err("rejected"),
ParseEndpointError::UnsupportedOption { .. }
));
}
#[test]
fn the_label_says_which_socket_type_it_is() {
assert_eq!(
dial("unix-seqpacket:/tmp/tocat.sock").label(),
"unix-seqpacket:///tmp/tocat.sock"
);
assert_eq!(
listen("unix-seqpacket-listen:@tocat").label(),
"unix-seqpacket://@tocat"
);
}
}