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 #[allow(clippy::too_many_arguments)]
68 pub fn new(
69 group_id: GroupId,
70 epoch: u64,
71 transition_id: u32,
72 stream_type: StreamType,
73 stream_id: u32,
74 flags: u16,
75 sequence_no: u32,
76 encrypted_payload: Vec<u8>,
77 payload_format: u8,
78 ) -> Self {
79 Self {
80 version: 1,
81 group_id: ByteBuf::from(group_id.to_vec()),
82 epoch,
83 transition_id,
84 stream_type: stream_type as u8,
85 stream_id,
86 flags,
87 sequence_no,
88 payload_size: encrypted_payload.len() as u32,
89 encrypted_payload: ByteBuf::from(encrypted_payload),
90 payload_format,
91 }
92 }
93
94 pub fn payload_codec(&self) -> PayloadCodec {
97 PayloadCodec::from_u8(self.payload_format).unwrap_or(PayloadCodec::Cbor)
98 }
99
100 pub fn to_cbor(&self) -> Vec<u8> {
102 let mut buf = Vec::new();
103 ciborium::into_writer(self, &mut buf).expect("cbor encode is infallible on Vec");
104 buf
105 }
106
107 pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
117 let f = Self::decode(data)?;
118 f.validate_payload_size()?;
119 Ok(f)
120 }
121
122 pub fn decode(data: &[u8]) -> Result<Self, CodecError> {
128 ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
129 }
130
131 pub fn validate_payload_size(&self) -> Result<(), CodecError> {
134 if self.payload_size as usize != self.encrypted_payload.len() {
135 return Err(CodecError::PayloadSizeMismatch);
136 }
137 Ok(())
138 }
139
140 pub fn stream_type_typed(&self) -> Result<StreamType, CodecError> {
143 StreamType::try_from(self.stream_type as u32).map_err(CodecError::UnknownEnumValue)
144 }
145
146 pub fn group_id_array(&self) -> GroupId {
149 let mut out = [0u8; 16];
150 let n = self.group_id.len().min(16);
151 out[..n].copy_from_slice(&self.group_id[..n]);
152 out
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use gbp_core::GbpFlags;
160
161 #[test]
162 fn frame_roundtrip() {
163 let f = GbpFrame::new(
164 [0xAA; 16],
165 42,
166 7,
167 StreamType::Text,
168 201,
169 GbpFlags::ORDERED | GbpFlags::RELIABLE,
170 1,
171 vec![1, 2, 3, 4, 5],
172 0,
173 );
174 let bytes = f.to_cbor();
175 let back = GbpFrame::from_cbor(&bytes).unwrap();
176 assert_eq!(back.epoch, 42);
177 assert_eq!(back.transition_id, 7);
178 assert_eq!(back.stream_type_typed().unwrap(), StreamType::Text);
179 assert_eq!(back.encrypted_payload.as_slice(), &[1, 2, 3, 4, 5]);
180 assert_eq!(back.payload_format, 0);
181 }
182
183 #[test]
184 fn frame_roundtrip_with_codec() {
185 use gbp_core::PayloadCodec;
186 let f = GbpFrame::new(
187 [0xBB; 16],
188 1,
189 0,
190 StreamType::Audio,
191 1,
192 0,
193 1,
194 vec![0xDE, 0xAD],
195 PayloadCodec::FlatBuffers.as_u8(),
196 );
197 assert_eq!(f.payload_codec(), PayloadCodec::FlatBuffers);
198 let bytes = f.to_cbor();
199 let back = GbpFrame::from_cbor(&bytes).unwrap();
200 assert_eq!(back.payload_format, PayloadCodec::FlatBuffers.as_u8());
201 assert_eq!(back.payload_codec(), PayloadCodec::FlatBuffers);
202 }
203
204 #[test]
205 fn cbor_codec_field_omitted_from_wire() {
206 let f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![0], 0);
207 let bytes = f.to_cbor();
208 let back = GbpFrame::from_cbor(&bytes).unwrap();
211 assert_eq!(back.payload_format, 0);
212 }
213
214 #[test]
215 fn frame_rejects_bad_payload_size() {
216 let mut f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![1, 2, 3], 0);
217 f.payload_size = 99;
218 let mut bytes = Vec::new();
219 ciborium::into_writer(&f, &mut bytes).unwrap();
220 assert!(matches!(
221 GbpFrame::from_cbor(&bytes),
222 Err(CodecError::PayloadSizeMismatch)
223 ));
224 }
225}