use crate::CodecError;
use gbp_core::{GroupId, PayloadCodec, StreamType};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
fn is_zero_u8(v: &u8) -> bool {
*v == 0
}
#[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,
#[serde(rename = "pf", default, skip_serializing_if = "is_zero_u8")]
pub payload_format: u8,
}
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>,
payload_format: 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),
payload_format,
}
}
pub fn payload_codec(&self) -> PayloadCodec {
PayloadCodec::from_u8(self.payload_format).unwrap_or(PayloadCodec::Cbor)
}
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],
0,
);
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]);
assert_eq!(back.payload_format, 0);
}
#[test]
fn frame_roundtrip_with_codec() {
use gbp_core::PayloadCodec;
let f = GbpFrame::new(
[0xBB; 16],
1,
0,
StreamType::Audio,
1,
0,
1,
vec![0xDE, 0xAD],
PayloadCodec::FlatBuffers.as_u8(),
);
assert_eq!(f.payload_codec(), PayloadCodec::FlatBuffers);
let bytes = f.to_cbor();
let back = GbpFrame::from_cbor(&bytes).unwrap();
assert_eq!(back.payload_format, PayloadCodec::FlatBuffers.as_u8());
assert_eq!(back.payload_codec(), PayloadCodec::FlatBuffers);
}
#[test]
fn cbor_codec_field_omitted_from_wire() {
let f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![0], 0);
let bytes = f.to_cbor();
let back = GbpFrame::from_cbor(&bytes).unwrap();
assert_eq!(back.payload_format, 0);
}
#[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], 0);
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)
));
}
}