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> {
92 let f = Self::decode(data)?;
93 f.validate_payload_size()?;
94 Ok(f)
95 }
96
97 pub fn decode(data: &[u8]) -> Result<Self, CodecError> {
103 ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
104 }
105
106 pub fn validate_payload_size(&self) -> Result<(), CodecError> {
109 if self.payload_size as usize != self.encrypted_payload.len() {
110 return Err(CodecError::PayloadSizeMismatch);
111 }
112 Ok(())
113 }
114
115 pub fn stream_type_typed(&self) -> Result<StreamType, CodecError> {
118 StreamType::try_from(self.stream_type as u32).map_err(CodecError::UnknownEnumValue)
119 }
120
121 pub fn group_id_array(&self) -> GroupId {
124 let mut out = [0u8; 16];
125 let n = self.group_id.len().min(16);
126 out[..n].copy_from_slice(&self.group_id[..n]);
127 out
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use gbp_core::GbpFlags;
135
136 #[test]
137 fn frame_roundtrip() {
138 let f = GbpFrame::new(
139 [0xAA; 16],
140 42,
141 7,
142 StreamType::Text,
143 201,
144 GbpFlags::ORDERED | GbpFlags::RELIABLE,
145 1,
146 vec![1, 2, 3, 4, 5],
147 );
148 let bytes = f.to_cbor();
149 let back = GbpFrame::from_cbor(&bytes).unwrap();
150 assert_eq!(back.epoch, 42);
151 assert_eq!(back.transition_id, 7);
152 assert_eq!(back.stream_type_typed().unwrap(), StreamType::Text);
153 assert_eq!(back.encrypted_payload.as_slice(), &[1, 2, 3, 4, 5]);
154 }
155
156 #[test]
157 fn frame_rejects_bad_payload_size() {
158 let mut f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![1, 2, 3]);
159 f.payload_size = 99;
160 let mut bytes = Vec::new();
161 ciborium::into_writer(&f, &mut bytes).unwrap();
162 assert!(matches!(
163 GbpFrame::from_cbor(&bytes),
164 Err(CodecError::PayloadSizeMismatch)
165 ));
166 }
167}