use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::time::Duration;
use futures_util::Stream;
use serde::Serialize;
use serde::de::DeserializeOwned;
use tokio::io::{AsyncBufReadExt, AsyncReadExt as _, BufReader};
use tokio::net::UnixStream;
use super::rpc::{MAX_FRAME_BYTES, UdsRpcError, dial_and_send};
use super::server::{RpcResponse, RpcStreamFrame, StreamPhase};
pub async fn send_framed_stream_request<Req, T>(
path: &Path,
request: &Req,
timeout: Duration,
) -> Result<FramedStream<T>, UdsRpcError>
where
Req: Serialize + ?Sized,
T: DeserializeOwned,
{
send_framed_stream_request_capped(path, request, timeout, MAX_FRAME_BYTES).await
}
pub async fn send_framed_stream_request_capped<Req, T>(
path: &Path,
request: &Req,
timeout: Duration,
max_frame_bytes: u64,
) -> Result<FramedStream<T>, UdsRpcError>
where
Req: Serialize + ?Sized,
T: DeserializeOwned,
{
let stream = match tokio::time::timeout(timeout, dial_and_send(path, request)).await {
Ok(result) => result?,
Err(_) => {
return Err(UdsRpcError::Timeout {
path: path.to_path_buf(),
timeout,
});
}
};
Ok(FramedStream {
reader: BufReader::new(stream),
path: path.to_path_buf(),
max_frame_bytes,
frame_timeout: timeout,
finished: false,
_item: PhantomData,
})
}
pub struct FramedStream<T> {
reader: BufReader<UnixStream>,
path: PathBuf,
max_frame_bytes: u64,
frame_timeout: Duration,
finished: bool,
_item: PhantomData<fn() -> T>,
}
impl<T> std::fmt::Debug for FramedStream<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FramedStream")
.field("path", &self.path)
.field("max_frame_bytes", &self.max_frame_bytes)
.field("finished", &self.finished)
.finish_non_exhaustive()
}
}
impl<T> FramedStream<T>
where
T: DeserializeOwned,
{
pub fn path(&self) -> &Path {
&self.path
}
pub async fn next_frame(&mut self) -> Option<Result<T, UdsRpcError>> {
if self.finished {
return None;
}
match self.read_next().await {
Ok(Some(item)) => Some(Ok(item)),
Ok(None) => {
self.finished = true;
None
}
Err(e) => {
self.finished = true;
Some(Err(e))
}
}
}
pub fn into_stream(self) -> impl Stream<Item = Result<T, UdsRpcError>> {
futures_util::stream::unfold(self, |mut reader| async move {
reader.next_frame().await.map(|item| (item, reader))
})
}
async fn read_next(&mut self) -> Result<Option<T>, UdsRpcError> {
let line = match tokio::time::timeout(self.frame_timeout, self.read_line()).await {
Ok(result) => result?,
Err(_) => {
return Err(UdsRpcError::Timeout {
path: self.path.clone(),
timeout: self.frame_timeout,
});
}
};
let Some(line) = line else {
return Err(UdsRpcError::NoResponse {
path: self.path.clone(),
});
};
let frame: RpcStreamFrame = match serde_json::from_slice(&line) {
Ok(frame) => frame,
Err(source) => return Err(self.classify_non_stream_frame(&line, source)),
};
match frame.stream {
StreamPhase::Item => {
let payload = frame.result.unwrap_or(serde_json::Value::Null);
serde_json::from_value(payload)
.map(Some)
.map_err(|source| UdsRpcError::Decode {
path: self.path.clone(),
source,
})
}
StreamPhase::End => Ok(None),
StreamPhase::Error => Err(UdsRpcError::Stream {
path: self.path.clone(),
error: frame.error.unwrap_or_else(|| {
super::server::RpcError::internal(
"terminal stream error frame carried no error",
)
}),
}),
}
}
fn classify_non_stream_frame(&self, line: &[u8], source: serde_json::Error) -> UdsRpcError {
match serde_json::from_slice::<RpcResponse>(line) {
Ok(response) => UdsRpcError::NotAStream {
path: self.path.clone(),
response: Box::new(response),
},
Err(_) => UdsRpcError::Decode {
path: self.path.clone(),
source,
},
}
}
async fn read_line(&mut self) -> Result<Option<Vec<u8>>, UdsRpcError> {
let mut line: Vec<u8> = Vec::new();
let mut bounded = (&mut self.reader).take(self.max_frame_bytes);
let read = match bounded.read_until(b'\n', &mut line).await {
Ok(read) => read,
Err(source) => {
return Err(super::rpc::classify_read_failure(
&self.path,
source,
line.is_empty(),
));
}
};
if read == 0 && line.is_empty() {
return Ok(None);
}
if !line.ends_with(b"\n") && line.len() as u64 >= self.max_frame_bytes {
return Err(UdsRpcError::FrameTooLarge {
path: self.path.clone(),
limit: self.max_frame_bytes,
});
}
Ok(Some(line))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::uds::bind_hardened;
use serde_json::json;
use tokio::io::AsyncWriteExt as _;
use tokio::net::UnixListener;
fn spawn_replaying(dir: &Path, bytes: Vec<u8>) -> PathBuf {
let sock = dir.join("sockets").join("stream.sock");
let listener: UnixListener = bind_hardened(&sock).expect("bind");
tokio::spawn(async move {
let Ok((mut conn, _)) = listener.accept().await else {
return;
};
let mut sink = Vec::new();
let _ = conn.read_to_end(&mut sink).await;
let _ = conn.write_all(&bytes).await;
let _ = conn.flush().await;
});
sock
}
async fn open(sock: &Path) -> FramedStream<String> {
send_framed_stream_request(sock, &json!({ "stream": true }), Duration::from_secs(5))
.await
.expect("open the stream")
}
#[tokio::test]
async fn stream_client_reads_items_until_the_terminal_frame() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_replaying(
tmp.path(),
b"{\"jsonrpc\":\"2.0\",\"id\":1,\"stream\":\"item\",\"result\":\"Hel\"}\n\
{\"jsonrpc\":\"2.0\",\"id\":1,\"stream\":\"item\",\"result\":\"lo\"}\n\
{\"jsonrpc\":\"2.0\",\"id\":1,\"stream\":\"end\"}\n"
.to_vec(),
);
let mut stream = open(&sock).await;
let mut got = Vec::new();
while let Some(item) = stream.next_frame().await {
got.push(item.expect("no error expected"));
}
assert_eq!(got, vec!["Hel".to_string(), "lo".to_string()]);
}
#[tokio::test]
async fn stream_reports_a_truncated_stream_rather_than_an_empty_success() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_replaying(
tmp.path(),
b"{\"jsonrpc\":\"2.0\",\"id\":1,\"stream\":\"item\",\"result\":\"Hel\"}\n".to_vec(),
);
let mut stream = open(&sock).await;
assert_eq!(
stream.next_frame().await.expect("an item").expect("ok"),
"Hel"
);
let err = stream
.next_frame()
.await
.expect("a truncated stream must report, not end")
.expect_err("EOF without a terminal frame is not a success");
assert!(
matches!(err, UdsRpcError::NoResponse { .. }),
"expected NoResponse, got {err:?}"
);
}
#[tokio::test]
async fn stream_is_finished_after_it_reports_an_error() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_replaying(tmp.path(), Vec::new());
let mut stream = open(&sock).await;
assert!(
stream.next_frame().await.expect("a report").is_err(),
"an empty answer is a protocol violation"
);
assert!(
stream.next_frame().await.is_none(),
"the failure is reported once, then the stream is done"
);
}
#[tokio::test]
async fn stream_client_reports_a_unary_answer_as_not_a_stream() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_replaying(
tmp.path(),
b"{\"jsonrpc\":\"2.0\",\"id\":1,\"error\":{\"code\":-32010,\"message\":\"no\"}}\n"
.to_vec(),
);
let mut stream = open(&sock).await;
let err = stream
.next_frame()
.await
.expect("a report")
.expect_err("a unary frame is not a stream item");
match err {
UdsRpcError::NotAStream { response, .. } => {
assert_eq!(response.error.expect("an error").code, -32010);
}
other => panic!("expected NotAStream, got {other:?}"),
}
}
#[tokio::test]
async fn stream_client_honours_a_caller_supplied_frame_budget() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_replaying(tmp.path(), vec![b'x'; 4096]);
let mut stream: FramedStream<String> = send_framed_stream_request_capped(
&sock,
&json!({ "stream": true }),
Duration::from_secs(5),
1024,
)
.await
.expect("open");
let err = stream
.next_frame()
.await
.expect("a report")
.expect_err("an unterminated flood past the budget must be refused");
assert!(
matches!(err, UdsRpcError::FrameTooLarge { limit, .. } if limit == 1024),
"expected FrameTooLarge at 1024, got {err:?}"
);
}
#[tokio::test]
async fn stream_into_stream_yields_the_same_items_as_next_frame() {
use futures_util::StreamExt as _;
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_replaying(
tmp.path(),
b"{\"jsonrpc\":\"2.0\",\"id\":1,\"stream\":\"item\",\"result\":\"a\"}\n\
{\"jsonrpc\":\"2.0\",\"id\":1,\"stream\":\"end\"}\n"
.to_vec(),
);
let items: Vec<String> = open(&sock)
.await
.into_stream()
.map(|item| item.expect("ok"))
.collect()
.await;
assert_eq!(items, vec!["a".to_string()]);
}
#[tokio::test]
async fn stream_client_reports_a_dial_failure_for_a_missing_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("absent.sock");
let err = send_framed_stream_request::<_, String>(
&sock,
&json!({ "stream": true }),
Duration::from_secs(5),
)
.await
.expect_err("no listener means no stream");
assert!(
matches!(err, UdsRpcError::Dial { .. }),
"expected Dial, got {err:?}"
);
}
}