1use crate::CodecError;
11use gbp_core::{GroupId, PayloadCodec, StreamType};
12use serde::{Deserialize, Serialize};
13use serde_bytes::ByteBuf;
14
15fn is_zero_u8(v: &u8) -> bool {
16 *v == 0
17}
18
19#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct GbpFrame {
22 #[serde(rename = "v")]
24 pub version: u8,
25 #[serde(rename = "gid")]
27 pub group_id: ByteBuf,
28 #[serde(rename = "ep")]
30 pub epoch: u64,
31 #[serde(rename = "tid")]
33 pub transition_id: u32,
34 #[serde(rename = "st")]
36 pub stream_type: u8,
37 #[serde(rename = "sid")]
39 pub stream_id: u32,
40 #[serde(rename = "fl")]
42 pub flags: u16,
43 #[serde(rename = "seq")]
45 pub sequence_no: u32,
46 #[serde(rename = "psz")]
48 pub payload_size: u32,
49 #[serde(rename = "pl")]
51 pub encrypted_payload: ByteBuf,
52 #[serde(rename = "pf", default, skip_serializing_if = "is_zero_u8")]
55 pub payload_format: u8,
56}
57
58impl GbpFrame {
59 pub fn new(
66 group_id: GroupId,
67 epoch: u64,
68 transition_id: u32,
69 stream_type: StreamType,
70 stream_id: u32,
71 flags: u16,
72 sequence_no: u32,
73 encrypted_payload: Vec<u8>,
74 payload_format: u8,
75 ) -> Self {
76 Self {
77 version: 1,
78 group_id: ByteBuf::from(group_id.to_vec()),
79 epoch,
80 transition_id,
81 stream_type: stream_type as u8,
82 stream_id,
83 flags,
84 sequence_no,
85 payload_size: encrypted_payload.len() as u32,
86 encrypted_payload: ByteBuf::from(encrypted_payload),
87 payload_format,
88 }
89 }
90
91 pub fn payload_codec(&self) -> PayloadCodec {
94 PayloadCodec::from_u8(self.payload_format).unwrap_or(PayloadCodec::Cbor)
95 }
96
97 pub fn to_cbor(&self) -> Vec<u8> {
99 let mut buf = Vec::new();
100 ciborium::into_writer(self, &mut buf).expect("cbor encode is infallible on Vec");
101 buf
102 }
103
104 pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
114 let f = Self::decode(data)?;
115 f.validate_payload_size()?;
116 Ok(f)
117 }
118
119 pub fn decode(data: &[u8]) -> Result<Self, CodecError> {
125 ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
126 }
127
128 pub fn validate_payload_size(&self) -> Result<(), CodecError> {
131 if self.payload_size as usize != self.encrypted_payload.len() {
132 return Err(CodecError::PayloadSizeMismatch);
133 }
134 Ok(())
135 }
136
137 pub fn stream_type_typed(&self) -> Result<StreamType, CodecError> {
140 StreamType::try_from(self.stream_type as u32).map_err(CodecError::UnknownEnumValue)
141 }
142
143 pub fn group_id_array(&self) -> GroupId {
146 let mut out = [0u8; 16];
147 let n = self.group_id.len().min(16);
148 out[..n].copy_from_slice(&self.group_id[..n]);
149 out
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use gbp_core::GbpFlags;
157
158 #[test]
159 fn frame_roundtrip() {
160 let f = GbpFrame::new(
161 [0xAA; 16],
162 42,
163 7,
164 StreamType::Text,
165 201,
166 GbpFlags::ORDERED | GbpFlags::RELIABLE,
167 1,
168 vec![1, 2, 3, 4, 5],
169 0,
170 );
171 let bytes = f.to_cbor();
172 let back = GbpFrame::from_cbor(&bytes).unwrap();
173 assert_eq!(back.epoch, 42);
174 assert_eq!(back.transition_id, 7);
175 assert_eq!(back.stream_type_typed().unwrap(), StreamType::Text);
176 assert_eq!(back.encrypted_payload.as_slice(), &[1, 2, 3, 4, 5]);
177 assert_eq!(back.payload_format, 0);
178 }
179
180 #[test]
181 fn frame_roundtrip_with_codec() {
182 use gbp_core::PayloadCodec;
183 let f = GbpFrame::new(
184 [0xBB; 16],
185 1,
186 0,
187 StreamType::Audio,
188 1,
189 0,
190 1,
191 vec![0xDE, 0xAD],
192 PayloadCodec::FlatBuffers.as_u8(),
193 );
194 assert_eq!(f.payload_codec(), PayloadCodec::FlatBuffers);
195 let bytes = f.to_cbor();
196 let back = GbpFrame::from_cbor(&bytes).unwrap();
197 assert_eq!(back.payload_format, PayloadCodec::FlatBuffers.as_u8());
198 assert_eq!(back.payload_codec(), PayloadCodec::FlatBuffers);
199 }
200
201 #[test]
202 fn cbor_codec_field_omitted_from_wire() {
203 let f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![0], 0);
204 let bytes = f.to_cbor();
205 let back = GbpFrame::from_cbor(&bytes).unwrap();
208 assert_eq!(back.payload_format, 0);
209 }
210
211 #[test]
212 fn frame_rejects_bad_payload_size() {
213 let mut f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![1, 2, 3], 0);
214 f.payload_size = 99;
215 let mut bytes = Vec::new();
216 ciborium::into_writer(&f, &mut bytes).unwrap();
217 assert!(matches!(
218 GbpFrame::from_cbor(&bytes),
219 Err(CodecError::PayloadSizeMismatch)
220 ));
221 }
222}