1use crate::CodecError;
8use gbp_core::{GroupId, StreamType};
9use serde::{Deserialize, Serialize};
10use serde_bytes::ByteBuf;
11
12#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct GbpFrame {
15 #[serde(rename = "v")]
17 pub version: u8,
18 #[serde(rename = "gid")]
20 pub group_id: ByteBuf,
21 #[serde(rename = "ep")]
23 pub epoch: u64,
24 #[serde(rename = "tid")]
26 pub transition_id: u32,
27 #[serde(rename = "st")]
29 pub stream_type: u8,
30 #[serde(rename = "sid")]
32 pub stream_id: u32,
33 #[serde(rename = "fl")]
35 pub flags: u16,
36 #[serde(rename = "seq")]
38 pub sequence_no: u32,
39 #[serde(rename = "psz")]
41 pub payload_size: u32,
42 #[serde(rename = "pl")]
44 pub encrypted_payload: ByteBuf,
45}
46
47impl GbpFrame {
48 pub fn new(
52 group_id: GroupId,
53 epoch: u64,
54 transition_id: u32,
55 stream_type: StreamType,
56 stream_id: u32,
57 flags: u16,
58 sequence_no: u32,
59 encrypted_payload: Vec<u8>,
60 ) -> Self {
61 Self {
62 version: 1,
63 group_id: ByteBuf::from(group_id.to_vec()),
64 epoch,
65 transition_id,
66 stream_type: stream_type as u8,
67 stream_id,
68 flags,
69 sequence_no,
70 payload_size: encrypted_payload.len() as u32,
71 encrypted_payload: ByteBuf::from(encrypted_payload),
72 }
73 }
74
75 pub fn to_cbor(&self) -> Vec<u8> {
77 let mut buf = Vec::new();
78 ciborium::into_writer(self, &mut buf).expect("cbor encode is infallible on Vec");
79 buf
80 }
81
82 pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
84 let f: Self = ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))?;
85 if f.payload_size as usize != f.encrypted_payload.len() {
86 return Err(CodecError::PayloadSizeMismatch);
87 }
88 Ok(f)
89 }
90
91 pub fn stream_type_typed(&self) -> Result<StreamType, CodecError> {
94 StreamType::try_from(self.stream_type as u32).map_err(CodecError::UnknownEnumValue)
95 }
96
97 pub fn group_id_array(&self) -> GroupId {
100 let mut out = [0u8; 16];
101 let n = self.group_id.len().min(16);
102 out[..n].copy_from_slice(&self.group_id[..n]);
103 out
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use gbp_core::GbpFlags;
111
112 #[test]
113 fn frame_roundtrip() {
114 let f = GbpFrame::new(
115 [0xAA; 16],
116 42,
117 7,
118 StreamType::Text,
119 201,
120 GbpFlags::ORDERED | GbpFlags::RELIABLE,
121 1,
122 vec![1, 2, 3, 4, 5],
123 );
124 let bytes = f.to_cbor();
125 let back = GbpFrame::from_cbor(&bytes).unwrap();
126 assert_eq!(back.epoch, 42);
127 assert_eq!(back.transition_id, 7);
128 assert_eq!(back.stream_type_typed().unwrap(), StreamType::Text);
129 assert_eq!(back.encrypted_payload.as_slice(), &[1, 2, 3, 4, 5]);
130 }
131
132 #[test]
133 fn frame_rejects_bad_payload_size() {
134 let mut f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![1, 2, 3]);
135 f.payload_size = 99;
136 let mut bytes = Vec::new();
137 ciborium::into_writer(&f, &mut bytes).unwrap();
138 assert!(matches!(
139 GbpFrame::from_cbor(&bytes),
140 Err(CodecError::PayloadSizeMismatch)
141 ));
142 }
143}