use crate::error::PluginError;
use crate::wire::{self, WireError};
pub const FRAME_ITEM: u8 = 0;
pub const FRAME_END: u8 = 1;
pub const FRAME_ERROR: u8 = 2;
pub const FRAME_HEADER_LEN: usize = 1 + 4;
#[derive(Debug, Clone, PartialEq)]
pub enum Frame {
Item(Vec<u8>),
End,
Error(PluginError),
}
#[derive(Debug, thiserror::Error)]
pub enum FrameError {
#[error("truncated frame: needed {needed} bytes, had {had}")]
Truncated { needed: usize, had: usize },
#[error("unknown frame tag: {0}")]
UnknownTag(u8),
#[error("malformed error-frame payload: {0}")]
Payload(#[from] WireError),
#[error("malformed frame: {0}")]
Malformed(String),
}
impl Frame {
pub fn encode(&self) -> Result<Vec<u8>, WireError> {
let (tag, payload): (u8, Vec<u8>) = match self {
Frame::Item(bytes) => (FRAME_ITEM, bytes.clone()),
Frame::End => (FRAME_END, Vec::new()),
Frame::Error(err) => (FRAME_ERROR, wire::serialize(err)?),
};
let mut out = Vec::with_capacity(FRAME_HEADER_LEN + payload.len());
out.push(tag);
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(&payload);
Ok(out)
}
pub fn decode(bytes: &[u8]) -> Result<Frame, FrameError> {
let (frame, consumed) = Frame::read(bytes)?;
if consumed != bytes.len() {
return Err(FrameError::Malformed(format!(
"{} trailing byte(s) after frame",
bytes.len() - consumed
)));
}
Ok(frame)
}
pub fn read(bytes: &[u8]) -> Result<(Frame, usize), FrameError> {
if bytes.len() < FRAME_HEADER_LEN {
return Err(FrameError::Truncated {
needed: FRAME_HEADER_LEN,
had: bytes.len(),
});
}
let tag = bytes[0];
let len = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
let end = FRAME_HEADER_LEN + len;
if bytes.len() < end {
return Err(FrameError::Truncated {
needed: end,
had: bytes.len(),
});
}
let payload = &bytes[FRAME_HEADER_LEN..end];
let frame = match tag {
FRAME_ITEM => Frame::Item(payload.to_vec()),
FRAME_END => {
if len != 0 {
return Err(FrameError::Malformed(format!(
"END frame carries {len} payload byte(s)"
)));
}
Frame::End
}
FRAME_ERROR => Frame::Error(wire::deserialize::<PluginError>(payload)?),
other => return Err(FrameError::UnknownTag(other)),
};
Ok((frame, end))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn item(payload: &[u8]) -> Frame {
Frame::Item(payload.to_vec())
}
#[test]
fn item_round_trip() {
let f = item(&[1, 2, 3, 4]);
let bytes = f.encode().unwrap();
assert_eq!(bytes[0], FRAME_ITEM);
assert_eq!(Frame::decode(&bytes).unwrap(), f);
}
#[test]
fn end_round_trip() {
let bytes = Frame::End.encode().unwrap();
assert_eq!(bytes[0], FRAME_END);
assert_eq!(bytes.len(), FRAME_HEADER_LEN);
assert_eq!(Frame::decode(&bytes).unwrap(), Frame::End);
}
#[test]
fn error_round_trip() {
let err = PluginError::new("BOOM", "it broke");
let f = Frame::Error(err.clone());
let bytes = f.encode().unwrap();
assert_eq!(bytes[0], FRAME_ERROR);
assert_eq!(Frame::decode(&bytes).unwrap(), Frame::Error(err));
}
#[test]
fn empty_item_is_valid() {
let f = item(&[]);
let bytes = f.encode().unwrap();
assert_eq!(Frame::decode(&bytes).unwrap(), f);
}
#[test]
fn read_walks_concatenated_frames() {
let mut buf = Vec::new();
buf.extend_from_slice(&item(&[9]).encode().unwrap());
buf.extend_from_slice(&item(&[8, 7]).encode().unwrap());
buf.extend_from_slice(&Frame::End.encode().unwrap());
let (f1, n1) = Frame::read(&buf).unwrap();
assert_eq!(f1, item(&[9]));
let (f2, n2) = Frame::read(&buf[n1..]).unwrap();
assert_eq!(f2, item(&[8, 7]));
let (f3, n3) = Frame::read(&buf[n1 + n2..]).unwrap();
assert_eq!(f3, Frame::End);
assert_eq!(n1 + n2 + n3, buf.len());
}
#[test]
fn truncated_header_is_rejected() {
let err = Frame::read(&[FRAME_ITEM, 0, 0]).unwrap_err();
assert!(matches!(err, FrameError::Truncated { .. }));
}
#[test]
fn truncated_payload_is_rejected() {
let mut bytes = vec![FRAME_ITEM];
bytes.extend_from_slice(&8u32.to_le_bytes());
bytes.extend_from_slice(&[1, 2]);
let err = Frame::decode(&bytes).unwrap_err();
assert!(matches!(err, FrameError::Truncated { needed: 13, had: 7 }));
}
#[test]
fn unknown_tag_is_rejected() {
let mut bytes = vec![99u8];
bytes.extend_from_slice(&0u32.to_le_bytes());
assert!(matches!(
Frame::decode(&bytes).unwrap_err(),
FrameError::UnknownTag(99)
));
}
#[test]
fn end_with_payload_is_rejected() {
let mut bytes = vec![FRAME_END];
bytes.extend_from_slice(&3u32.to_le_bytes());
bytes.extend_from_slice(&[1, 2, 3]);
assert!(matches!(
Frame::decode(&bytes).unwrap_err(),
FrameError::Malformed(_)
));
}
#[test]
fn trailing_bytes_after_single_decode_rejected() {
let mut bytes = item(&[1]).encode().unwrap();
bytes.push(0xFF);
assert!(matches!(
Frame::decode(&bytes).unwrap_err(),
FrameError::Malformed(_)
));
}
#[test]
fn garbage_is_rejected_not_panicking() {
for g in [vec![], vec![0u8], vec![2u8, 0, 0, 0, 1]] {
let _ = Frame::read(&g); }
}
}