use core::{result::Result as StdResult, time::Duration};
use std::{
env::{var, VarError},
io::{prelude::*, BufReader, Error as IoError, ErrorKind},
os::unix::net::UnixStream,
path::{Path, PathBuf},
thread::sleep,
time::Instant,
};
use log::trace;
use thiserror::Error as ThisError;
use crate::message::{Error as MessageError, Message};
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum Error {
#[error("runtime directory $XDG_RUNTIME_DIR not set correctly: {0}")]
NoRuntime(#[from] VarError),
#[error("failed to connect to {0}, {1}")]
ConnectionFailed(String, IoError),
#[error("failed to write message: {0}")]
WriteFailed(IoError),
#[error("failed to read message: {0}")]
ReadFailed(IoError),
#[error("operation failed: {0}")]
Other(#[from] IoError),
#[error("parsing failed: {0}")]
ParsingFailed(String),
#[error("parsing failed: {0}")]
ParsingMessageFailed(#[from] MessageError),
}
pub(crate) type Result<T> = StdResult<T, Error>;
pub(crate) struct NextCloudClientSocket {
stream: UnixStream,
reader: BufReader<UnixStream>,
}
impl NextCloudClientSocket {
#[expect(
clippy::single_call_fn,
reason = "builders should never be inlined to external code"
)]
pub(crate) fn new() -> Result<Self> {
let socket_file = PathBuf::from(var("XDG_RUNTIME_DIR")?)
.join("Nextcloud")
.join("socket");
NextCloudClientSocket::connect(&socket_file)
}
#[cfg(test)]
#[cfg_attr(
test,
expect(clippy::single_call_fn, reason = "builder called for test mock")
)]
pub(crate) fn new_test(socket_file: &Path) -> Result<Self> {
NextCloudClientSocket::connect(socket_file)
}
#[cfg_attr(
not(test),
expect(clippy::single_call_fn, reason = "abstraction for test mock")
)]
fn connect(socket_file: &Path) -> Result<Self> {
let stream = match UnixStream::connect(socket_file) {
Ok(stream) => stream,
Err(error) => {
return Err(Error::ConnectionFailed(
socket_file.to_string_lossy().to_string(),
error,
))
}
};
stream.set_read_timeout(Some(Duration::from_millis(100)))?;
let reader = BufReader::new(stream.try_clone()?);
Ok(Self { stream, reader })
}
pub(crate) fn write_message(&mut self, message: &Message) -> Result<()> {
trace!("write message: {message}");
self.stream
.write_all(message.to_string().as_bytes())
.map_err(Error::WriteFailed)?;
self.stream
.write_all("\n".as_bytes())
.map_err(Error::WriteFailed)
}
pub(crate) fn read_message(&mut self) -> Result<Option<Message>> {
let mut res = String::new();
match self.reader.read_line(&mut res) {
Ok(_) => Ok(Some(res.trim().try_into()?)),
Err(error) if error.kind() == ErrorKind::TimedOut => Ok(None),
Err(error) if error.kind() == ErrorKind::WouldBlock => Ok(None),
Err(error) => Err(Error::ReadFailed(error)),
}
}
pub(crate) fn read_until_settled(&mut self, timeout: Duration) -> Result<Vec<Message>> {
let mut result = Vec::new();
let delay = timeout.div_f64(2.0);
let mut last_activity = Instant::now();
loop {
if let Some(message) = self.read_message()? {
trace!("read line: {message}");
result.push(message);
last_activity = Instant::now();
continue;
}
if last_activity.elapsed() > timeout {
return Ok(result);
}
sleep(delay);
}
}
}