use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::error::RiakError;
pub const MAX_FRAME_LEN: u32 = 16 * 1024 * 1024;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Frame {
pub code: u8,
pub body: Vec<u8>,
}
impl Frame {
#[must_use]
pub fn new(code: u8, body: Vec<u8>) -> Self {
Self { code, body }
}
#[must_use]
pub fn wire_len(&self) -> usize {
4 + 1 + self.body.len()
}
}
pub async fn read_frame<R>(r: &mut R) -> Result<Frame, RiakError>
where
R: AsyncRead + Unpin,
{
let mut len_buf = [0u8; 4];
read_exact_or_classify(r, &mut len_buf, 4).await?;
let announced = u32::from_be_bytes(len_buf);
if announced == 0 {
return Err(RiakError::EmptyFrame);
}
if announced > MAX_FRAME_LEN {
return Err(RiakError::FrameTooLarge {
announced,
max: MAX_FRAME_LEN,
});
}
let mut code_buf = [0u8; 1];
read_exact_or_classify(r, &mut code_buf, 1).await?;
let code = code_buf[0];
let body_len = (announced - 1) as usize;
let mut body = vec![0u8; body_len];
if body_len > 0 {
read_exact_or_classify(r, &mut body, body_len).await?;
}
Ok(Frame { code, body })
}
pub async fn write_frame<W>(w: &mut W, frame: &Frame) -> Result<(), RiakError>
where
W: AsyncWrite + Unpin,
{
let announced = u32::try_from(1 + frame.body.len()).map_err(|_| RiakError::FrameTooLarge {
announced: u32::MAX,
max: MAX_FRAME_LEN,
})?;
if announced > MAX_FRAME_LEN {
return Err(RiakError::FrameTooLarge {
announced,
max: MAX_FRAME_LEN,
});
}
w.write_all(&announced.to_be_bytes()).await?;
w.write_all(&[frame.code]).await?;
if !frame.body.is_empty() {
w.write_all(&frame.body).await?;
}
w.flush().await?;
Ok(())
}
async fn read_exact_or_classify<R>(
r: &mut R,
buf: &mut [u8],
expected: usize,
) -> Result<(), RiakError>
where
R: AsyncRead + Unpin,
{
match r.read_exact(buf).await {
Ok(_) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
Err(RiakError::UnexpectedEof { read: 0, expected })
}
Err(e) => Err(RiakError::Io(e)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::duplex;
#[tokio::test]
async fn ping_frame_round_trips() {
let (mut a, mut b) = duplex(1024);
let f = Frame::new(1, Vec::new());
write_frame(&mut a, &f).await.unwrap();
let back = read_frame(&mut b).await.unwrap();
assert_eq!(back, f);
}
#[tokio::test]
async fn body_frame_round_trips() {
let (mut a, mut b) = duplex(4096);
let f = Frame::new(11, b"hello".to_vec());
write_frame(&mut a, &f).await.unwrap();
let back = read_frame(&mut b).await.unwrap();
assert_eq!(back, f);
assert_eq!(back.wire_len(), 4 + 1 + 5);
}
#[tokio::test]
async fn rejects_zero_length_announcement() {
let (mut a, mut b) = duplex(64);
a.write_all(&0u32.to_be_bytes()).await.unwrap();
a.flush().await.unwrap();
drop(a);
let err = read_frame(&mut b).await.expect_err("zero length");
assert!(matches!(err, RiakError::EmptyFrame));
}
#[tokio::test]
async fn rejects_oversized_announcement() {
let (mut a, mut b) = duplex(64);
let bad = MAX_FRAME_LEN + 1;
a.write_all(&bad.to_be_bytes()).await.unwrap();
a.flush().await.unwrap();
drop(a);
let err = read_frame(&mut b).await.expect_err("too big");
assert!(matches!(
err,
RiakError::FrameTooLarge { announced, max }
if announced == MAX_FRAME_LEN + 1 && max == MAX_FRAME_LEN
));
}
#[tokio::test]
async fn unexpected_eof_reports_error() {
let (a, mut b) = duplex(64);
drop(a);
let err = read_frame(&mut b).await.expect_err("eof");
assert!(matches!(err, RiakError::UnexpectedEof { .. }));
}
#[tokio::test]
async fn truncated_body_reports_unexpected_eof() {
let (mut a, mut b) = duplex(64);
a.write_all(&6u32.to_be_bytes()).await.unwrap();
a.write_all(&[11]).await.unwrap();
a.write_all(b"he").await.unwrap();
a.flush().await.unwrap();
drop(a);
let err = read_frame(&mut b).await.expect_err("truncated");
assert!(matches!(err, RiakError::UnexpectedEof { .. }));
}
#[tokio::test]
async fn write_frame_rejects_body_over_max() {
let body = vec![0u8; MAX_FRAME_LEN as usize]; let frame = Frame::new(11, body);
let (mut a, _b) = duplex(64);
let err = write_frame(&mut a, &frame).await.expect_err("too large");
assert!(matches!(
err,
RiakError::FrameTooLarge { announced, max }
if announced == MAX_FRAME_LEN + 1 && max == MAX_FRAME_LEN
));
}
#[tokio::test]
async fn read_exact_classifies_non_eof_io_error() {
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::io::{AsyncRead, ReadBuf};
struct BrokenReader;
impl AsyncRead for BrokenReader {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::ConnectionReset,
"reset",
)))
}
}
let mut r = BrokenReader;
let err = read_frame(&mut r).await.expect_err("io error");
assert!(matches!(err, RiakError::Io(_)));
}
}