use crate::error::{PwrError, Result};
use crate::protocol::MessageType;
pub const FRAME_MAGIC: [u8; 4] = [0x50, 0x57, 0x52, 0x46];
pub const PROTOCOL_VERSION: u8 = 0x01;
pub const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024;
pub const HEADER_SIZE: usize = 10;
pub fn encode_frame<T: serde::Serialize>(
msg: &T,
msg_type: MessageType,
) -> Result<Vec<u8>> {
let body = serde_json::to_vec(msg)
.map_err(|e| PwrError::Framing(format!("encode error: {}", e)))?;
if body.len() > MAX_FRAME_SIZE {
return Err(PwrError::Framing(format!(
"payload size {} exceeds maximum {}",
body.len(),
MAX_FRAME_SIZE
)));
}
let payload_len = body.len() as u32;
let mut frame = Vec::with_capacity(HEADER_SIZE + body.len());
frame.extend_from_slice(&FRAME_MAGIC);
frame.extend_from_slice(&payload_len.to_be_bytes());
frame.push(PROTOCOL_VERSION);
frame.push(msg_type as u8);
frame.extend_from_slice(&body);
Ok(frame)
}
#[derive(Debug)]
pub struct FrameHeader {
pub payload_len: u32,
pub version: u8,
pub msg_type: MessageType,
}
pub fn decode_frame(data: &[u8]) -> Result<Option<(FrameHeader, Vec<u8>)>> {
if data.len() < HEADER_SIZE {
return Ok(None);
}
if data[0..4] != FRAME_MAGIC {
return Err(PwrError::Framing(format!(
"invalid magic bytes: expected {:02X?}, got {:02X?}",
&FRAME_MAGIC[..],
&data[0..4]
)));
}
let payload_len = u32::from_be_bytes([data[4], data[5], data[6], data[7]]) as usize;
if payload_len > MAX_FRAME_SIZE {
return Err(PwrError::Framing(format!(
"frame payload size {} exceeds maximum {}",
payload_len, MAX_FRAME_SIZE
)));
}
let total_frame_size = HEADER_SIZE + payload_len;
if data.len() < total_frame_size {
return Ok(None);
}
let version = data[8];
if version != PROTOCOL_VERSION {
return Err(PwrError::Framing(format!(
"unsupported protocol version: got {}, expected {}",
version, PROTOCOL_VERSION
)));
}
let msg_type_byte = data[9];
let msg_type = MessageType::from_byte(msg_type_byte).ok_or_else(|| {
PwrError::Framing(format!("unknown message type: 0x{:02X}", msg_type_byte))
})?;
let payload = data[HEADER_SIZE..total_frame_size].to_vec();
Ok(Some((
FrameHeader {
payload_len: payload_len as u32,
version,
msg_type,
},
payload,
)))
}
#[derive(Debug, Default)]
pub struct FrameDecoder {
buffer: Vec<u8>,
}
impl FrameDecoder {
pub fn new() -> Self {
Self { buffer: Vec::new() }
}
pub fn push_bytes(&mut self, data: &[u8]) {
self.buffer.extend_from_slice(data);
}
pub fn try_decode(&mut self) -> Result<Option<(FrameHeader, Vec<u8>)>> {
match decode_frame(&self.buffer)? {
Some((header, payload)) => {
let consumed = HEADER_SIZE + header.payload_len as usize;
self.buffer.drain(..consumed);
Ok(Some((header, payload)))
}
None => Ok(None),
}
}
pub fn buffered_len(&self) -> usize {
self.buffer.len()
}
pub fn drain_bytes(&mut self) -> Vec<u8> {
std::mem::take(&mut self.buffer)
}
}
pub fn encode_file_chunk(data: &[u8]) -> Vec<u8> {
let len = data.len() as u32;
let mut chunk = Vec::with_capacity(4 + data.len());
chunk.extend_from_slice(&len.to_be_bytes());
chunk.extend_from_slice(data);
chunk
}
pub fn encode_file_eof() -> Vec<u8> {
vec![0u8, 0, 0, 0]
}
pub fn decode_file_chunk(data: &[u8]) -> Result<Option<&[u8]>> {
if data.len() < 4 {
return Err(PwrError::Framing(
"incomplete chunk header: need 4 bytes".into(),
));
}
let chunk_len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
if chunk_len == 0 {
return Ok(None); }
if data.len() < 4 + chunk_len {
return Err(PwrError::Framing(format!(
"incomplete chunk data: need {} bytes, have {}",
4 + chunk_len,
data.len()
)));
}
Ok(Some(&data[4..4 + chunk_len]))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::{Handshake, StatusRequest};
#[test]
fn test_encode_decode_round_trip() -> Result<()> {
let msg = Handshake {
version: 1,
client_id: "test-client".into(),
nonce: [0x42; 32],
proof: [0x99; 32],
};
let frame = encode_frame(&msg, MessageType::Handshake)?;
assert_eq!(&frame[0..4], &FRAME_MAGIC);
let (header, payload) = decode_frame(&frame)?.unwrap();
assert_eq!(header.version, PROTOCOL_VERSION);
assert_eq!(header.msg_type, MessageType::Handshake);
let decoded: Handshake = serde_json::from_slice(&payload).unwrap();
assert_eq!(decoded.client_id, "test-client");
Ok(())
}
#[test]
fn test_decode_needs_more_data() -> Result<()> {
assert!(decode_frame(&[0x50, 0x57, 0x52, 0x46, 0x00])?.is_none());
Ok(())
}
#[test]
fn test_decode_rejects_bad_magic() {
let mut bad = vec![0x00; 10];
bad[0] = 0xDE;
bad[1] = 0xAD;
assert!(decode_frame(&bad).is_err());
}
#[test]
fn test_decode_rejects_oversized_frame() {
let mut header = Vec::new();
header.extend_from_slice(&FRAME_MAGIC);
header.extend_from_slice(&((MAX_FRAME_SIZE as u32 + 1).to_be_bytes()));
header.push(PROTOCOL_VERSION);
header.push(MessageType::Error as u8);
assert!(decode_frame(&header).is_err());
}
#[test]
fn test_frame_decoder_buffering() -> Result<()> {
let mut decoder = FrameDecoder::new();
let msg = StatusRequest { project_uuid: None };
let frame = encode_frame(&msg, MessageType::StatusRequest)?;
let mid = frame.len() / 2;
decoder.push_bytes(&frame[..mid]);
assert!(decoder.try_decode()?.is_none());
decoder.push_bytes(&frame[mid..]);
let result = decoder.try_decode()?;
assert!(result.is_some());
let (header, _payload) = result.unwrap();
assert_eq!(header.msg_type, MessageType::StatusRequest);
assert_eq!(decoder.buffered_len(), 0);
Ok(())
}
#[test]
fn test_file_chunk_round_trip() {
let data = b"Hello, file streaming!";
let chunk = encode_file_chunk(data);
let decoded = decode_file_chunk(&chunk).unwrap();
assert_eq!(decoded, Some(&data[..]));
}
#[test]
fn test_file_eof() {
let eof = encode_file_eof();
assert_eq!(decode_file_chunk(&eof).unwrap(), None);
}
}