use crate::CodecError;
use gbp_core::{GroupId, StreamType};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GbpFrame {
#[serde(rename = "v")]
pub version: u8,
#[serde(rename = "gid")]
pub group_id: ByteBuf,
#[serde(rename = "ep")]
pub epoch: u64,
#[serde(rename = "tid")]
pub transition_id: u32,
#[serde(rename = "st")]
pub stream_type: u8,
#[serde(rename = "sid")]
pub stream_id: u32,
#[serde(rename = "fl")]
pub flags: u16,
#[serde(rename = "seq")]
pub sequence_no: u32,
#[serde(rename = "psz")]
pub payload_size: u32,
#[serde(rename = "pl")]
pub encrypted_payload: ByteBuf,
}
impl GbpFrame {
pub fn new(
group_id: GroupId,
epoch: u64,
transition_id: u32,
stream_type: StreamType,
stream_id: u32,
flags: u16,
sequence_no: u32,
encrypted_payload: Vec<u8>,
) -> Self {
Self {
version: 1,
group_id: ByteBuf::from(group_id.to_vec()),
epoch,
transition_id,
stream_type: stream_type as u8,
stream_id,
flags,
sequence_no,
payload_size: encrypted_payload.len() as u32,
encrypted_payload: ByteBuf::from(encrypted_payload),
}
}
pub fn to_cbor(&self) -> Vec<u8> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf).expect("cbor encode is infallible on Vec");
buf
}
pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
let f = Self::decode(data)?;
f.validate_payload_size()?;
Ok(f)
}
pub fn decode(data: &[u8]) -> Result<Self, CodecError> {
ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
}
pub fn validate_payload_size(&self) -> Result<(), CodecError> {
if self.payload_size as usize != self.encrypted_payload.len() {
return Err(CodecError::PayloadSizeMismatch);
}
Ok(())
}
pub fn stream_type_typed(&self) -> Result<StreamType, CodecError> {
StreamType::try_from(self.stream_type as u32).map_err(CodecError::UnknownEnumValue)
}
pub fn group_id_array(&self) -> GroupId {
let mut out = [0u8; 16];
let n = self.group_id.len().min(16);
out[..n].copy_from_slice(&self.group_id[..n]);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use gbp_core::GbpFlags;
#[test]
fn frame_roundtrip() {
let f = GbpFrame::new(
[0xAA; 16],
42,
7,
StreamType::Text,
201,
GbpFlags::ORDERED | GbpFlags::RELIABLE,
1,
vec![1, 2, 3, 4, 5],
);
let bytes = f.to_cbor();
let back = GbpFrame::from_cbor(&bytes).unwrap();
assert_eq!(back.epoch, 42);
assert_eq!(back.transition_id, 7);
assert_eq!(back.stream_type_typed().unwrap(), StreamType::Text);
assert_eq!(back.encrypted_payload.as_slice(), &[1, 2, 3, 4, 5]);
}
#[test]
fn frame_rejects_bad_payload_size() {
let mut f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![1, 2, 3]);
f.payload_size = 99;
let mut bytes = Vec::new();
ciborium::into_writer(&f, &mut bytes).unwrap();
assert!(matches!(
GbpFrame::from_cbor(&bytes),
Err(CodecError::PayloadSizeMismatch)
));
}
}