#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameType {
Stdout = 0x01,
Stderr = 0x02,
Started = 0x03,
Exited = 0x04,
Stdin = 0x10,
Resize = 0x11,
Signal = 0x12,
Eof = 0x13,
}
impl FrameType {
pub fn from_byte(b: u8) -> Option<Self> {
Some(match b {
0x01 => Self::Stdout,
0x02 => Self::Stderr,
0x03 => Self::Started,
0x04 => Self::Exited,
0x10 => Self::Stdin,
0x11 => Self::Resize,
0x12 => Self::Signal,
0x13 => Self::Eof,
_ => return None,
})
}
}
pub const HEADER_LEN: usize = 5;
pub struct Frame {
pub frame_type: FrameType,
pub data: Vec<u8>,
}
impl Frame {
pub fn new(frame_type: FrameType, data: Vec<u8>) -> Self {
Self { frame_type, data }
}
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(HEADER_LEN + self.data.len());
buf.push(self.frame_type as u8);
buf.extend_from_slice(&(self.data.len() as u32).to_be_bytes());
buf.extend_from_slice(&self.data);
buf
}
#[allow(dead_code)]
pub fn encoded_len(&self) -> usize {
HEADER_LEN + self.data.len()
}
pub fn stdout(data: impl Into<Vec<u8>>) -> Self {
Self::new(FrameType::Stdout, data.into())
}
pub fn stderr(data: impl Into<Vec<u8>>) -> Self {
Self::new(FrameType::Stderr, data.into())
}
pub fn started(pid: u32) -> Self {
Self::new(FrameType::Started, pid.to_be_bytes().to_vec())
}
pub fn exited(code: i32) -> Self {
Self::new(FrameType::Exited, code.to_be_bytes().to_vec())
}
#[allow(dead_code)]
pub fn stdin(data: impl Into<Vec<u8>>) -> Self {
Self::new(FrameType::Stdin, data.into())
}
pub fn as_pid(&self) -> Option<u32> {
if self.frame_type == FrameType::Started && self.data.len() == 4 {
Some(u32::from_be_bytes(self.data[..].try_into().unwrap()))
} else {
None
}
}
pub fn as_exit_code(&self) -> Option<i32> {
if self.frame_type == FrameType::Exited && self.data.len() == 4 {
Some(i32::from_be_bytes(self.data[..].try_into().unwrap()))
} else {
None
}
}
}
pub struct FrameReader {
buf: Vec<u8>,
consumed: u64,
}
impl Default for FrameReader {
fn default() -> Self {
Self::new()
}
}
impl FrameReader {
pub fn new() -> Self {
Self {
buf: Vec::new(),
consumed: 0,
}
}
pub fn push(&mut self, data: &[u8]) {
self.buf.extend_from_slice(data);
}
pub fn consumed_bytes(&self) -> u64 {
self.consumed
}
#[allow(dead_code)]
pub fn remaining(&self) -> u64 {
self.buf.len() as u64
}
pub fn next_frame(&mut self) -> Option<Frame> {
loop {
if self.buf.len() < HEADER_LEN {
return None;
}
let len = u32::from_be_bytes([
self.buf[1],
self.buf[2],
self.buf[3],
self.buf[4],
]) as usize;
if len > 16 * 1024 * 1024 {
self.buf.clear();
return None;
}
let total = HEADER_LEN + len;
if self.buf.len() < total {
return None;
}
let frame_type_byte = self.buf[0];
let data = self.buf[HEADER_LEN..total].to_vec();
self.buf.drain(0..total);
self.consumed += total as u64;
match FrameType::from_byte(frame_type_byte) {
Some(ft) => {
return Some(Frame {
frame_type: ft,
data,
});
}
None => {
continue;
}
}
}
}
}
#[allow(dead_code)]
pub fn parse_frames(data: &[u8]) -> (Vec<Frame>, usize) {
let mut reader = FrameReader::new();
reader.push(data);
let mut frames = Vec::new();
while let Some(frame) = reader.next_frame() {
frames.push(frame);
}
let consumed = reader.consumed_bytes() as usize;
(frames, consumed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_roundtrip() {
let frame = Frame::stdout(b"hello world".to_vec());
let encoded = frame.encode();
assert_eq!(encoded.len(), HEADER_LEN + 11);
let mut reader = FrameReader::new();
reader.push(&encoded);
let decoded = reader.next_frame().unwrap();
assert_eq!(decoded.frame_type, FrameType::Stdout);
assert_eq!(decoded.data, b"hello world");
assert!(reader.next_frame().is_none());
}
#[test]
fn test_partial_frame() {
let frame = Frame::stdout(b"hello".to_vec());
let encoded = frame.encode();
let mut reader = FrameReader::new();
reader.push(&encoded[..3]); assert!(reader.next_frame().is_none());
reader.push(&encoded[3..]); let decoded = reader.next_frame().unwrap();
assert_eq!(decoded.data, b"hello");
}
#[test]
fn test_multiple_frames_in_one_push() {
let f1 = Frame::stdout(b"aaa".to_vec());
let f2 = Frame::stderr(b"bbb".to_vec());
let f3 = Frame::exited(0);
let mut combined = Vec::new();
combined.extend_from_slice(&f1.encode());
combined.extend_from_slice(&f2.encode());
combined.extend_from_slice(&f3.encode());
let mut reader = FrameReader::new();
reader.push(&combined);
let d1 = reader.next_frame().unwrap();
assert_eq!(d1.frame_type, FrameType::Stdout);
assert_eq!(d1.data, b"aaa");
let d2 = reader.next_frame().unwrap();
assert_eq!(d2.frame_type, FrameType::Stderr);
assert_eq!(d2.data, b"bbb");
let d3 = reader.next_frame().unwrap();
assert_eq!(d3.frame_type, FrameType::Exited);
assert_eq!(d3.as_exit_code(), Some(0));
assert!(reader.next_frame().is_none());
}
#[test]
fn test_parse_frames_consumed() {
let f1 = Frame::stdout(b"aaa".to_vec());
let f2 = Frame::exited(0);
let mut data = Vec::new();
data.extend_from_slice(&f1.encode());
data.extend_from_slice(&f2.encode());
let (frames, consumed) = parse_frames(&data);
assert_eq!(frames.len(), 2);
assert_eq!(consumed, data.len());
}
#[test]
fn test_started_pid_roundtrip() {
let frame = Frame::started(12345);
assert_eq!(frame.as_pid(), Some(12345));
}
#[test]
fn test_unknown_byte_rejected() {
let frame_type = FrameType::from_byte(0xFF);
assert!(frame_type.is_none());
}
#[test]
fn test_no_value_collision() {
let all_types = [
FrameType::Stdout, FrameType::Stderr,
FrameType::Started, FrameType::Exited,
FrameType::Stdin, FrameType::Resize,
FrameType::Signal, FrameType::Eof,
];
let mut seen = std::collections::HashSet::new();
for ft in &all_types {
assert!(seen.insert(*ft as u8), "duplicate byte value: {:#04x}", *ft as u8);
}
}
#[test]
fn test_unknown_frame_type_skipped() {
let mut unknown_encoded = vec![0x07];
unknown_encoded.extend_from_slice(&(7u32).to_be_bytes());
unknown_encoded.extend_from_slice(b"unknown");
let known_frame = Frame::stdout(b"after".to_vec());
let mut combined = Vec::new();
combined.extend_from_slice(&unknown_encoded);
combined.extend_from_slice(&known_frame.encode());
let mut reader = FrameReader::new();
reader.push(&combined);
let decoded = reader.next_frame().unwrap();
assert_eq!(decoded.frame_type, FrameType::Stdout);
assert_eq!(decoded.data, b"after");
assert!(reader.next_frame().is_none());
assert_eq!(reader.consumed_bytes(), combined.len() as u64);
}
#[test]
fn test_consumed_bytes_tracking() {
let f1 = Frame::stdout(b"aaa".to_vec()); let f2 = Frame::stderr(b"bb".to_vec());
let mut combined = Vec::new();
combined.extend_from_slice(&f1.encode());
combined.extend_from_slice(&f2.encode());
let mut reader = FrameReader::new();
reader.push(&combined);
reader.next_frame().unwrap();
assert_eq!(reader.consumed_bytes(), 8);
reader.next_frame().unwrap();
assert_eq!(reader.consumed_bytes(), 15);
assert!(reader.next_frame().is_none());
assert_eq!(reader.consumed_bytes(), 15); }
}