Skip to main content

gbp/
frame.rs

1//! GBP transport frame.
2//!
3//! On the wire the frame is a deterministic CBOR map of ten keys (eleven when
4//! a non-CBOR payload codec is in use):
5//! `v, gid, ep, tid, st, sid, fl, seq, psz, pl[, pf]`.
6//! Field `psz` MUST equal the actual length of `pl`; this is checked on
7//! decode. Field `pf` is omitted when its value is 0 (CBOR) for
8//! backward-compatibility.
9
10use 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/// CBOR-encoded GBP frame.
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct GbpFrame {
22    /// Protocol version (currently `1`).
23    #[serde(rename = "v")]
24    pub version: u8,
25    /// 16-byte group identifier.
26    #[serde(rename = "gid")]
27    pub group_id: ByteBuf,
28    /// Sender's current epoch.
29    #[serde(rename = "ep")]
30    pub epoch: u64,
31    /// Last applied `transition_id`.
32    #[serde(rename = "tid")]
33    pub transition_id: u32,
34    /// StreamType as a `u8` (see `gbp_core::StreamType`).
35    #[serde(rename = "st")]
36    pub stream_type: u8,
37    /// Logical stream identifier within the session.
38    #[serde(rename = "sid")]
39    pub stream_id: u32,
40    /// Delivery flags (see `gbp_core::GbpFlags`).
41    #[serde(rename = "fl")]
42    pub flags: u16,
43    /// Per-stream sequence number (replay window key).
44    #[serde(rename = "seq")]
45    pub sequence_no: u32,
46    /// Declared payload length; MUST equal `encrypted_payload.len()`.
47    #[serde(rename = "psz")]
48    pub payload_size: u32,
49    /// Encrypted payload (an opaque byte string).
50    #[serde(rename = "pl")]
51    pub encrypted_payload: ByteBuf,
52    /// Payload codec discriminant (see [`gbp_core::PayloadCodec`]).
53    /// Omitted when 0 (CBOR) for backward-compatibility with pre-1.5 peers.
54    #[serde(rename = "pf", default, skip_serializing_if = "is_zero_u8")]
55    pub payload_format: u8,
56}
57
58impl GbpFrame {
59    /// Builds a frame from already-encrypted payload bytes.
60    ///
61    /// `payload_size` is set to `encrypted_payload.len()` automatically.
62    /// Pass `PayloadCodec::Cbor` (or `0`) for the default CBOR encoding; the
63    /// `pf` field is omitted from the wire when the codec is CBOR so older
64    /// peers continue to decode the frame correctly.
65    // One-to-one with the wire frame's own fields (see the struct above); a
66    // builder would just shuffle the same 9 fields without cutting complexity.
67    #[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    /// Returns the `payload_format` field as a [`PayloadCodec`], falling back
95    /// to [`PayloadCodec::Cbor`] for unknown discriminants.
96    pub fn payload_codec(&self) -> PayloadCodec {
97        PayloadCodec::from_u8(self.payload_format).unwrap_or(PayloadCodec::Cbor)
98    }
99
100    /// Serialises the frame into a freshly allocated CBOR byte vector.
101    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    /// Decodes a CBOR-encoded frame **and** validates `payload_size`.
108    ///
109    /// The order of all §6.2 checks (`version`, `group_id`, `epoch`,
110    /// `payload_size`, `transition_id`, `sequence_no`) is what governs which
111    /// error a malformed frame produces. Most callers should use
112    /// [`GroupNode::on_wire`](https://docs.rs/gbp-node) which decodes via
113    /// [`GbpFrame::decode`] and runs the full pipeline. This convenience
114    /// wrapper exists for tests and ad-hoc tooling that want both decode
115    /// and the length check in one shot.
116    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    /// Decodes a CBOR-encoded frame **without** running any §6.2 checks.
123    ///
124    /// Use [`GbpFrame::validate_payload_size`] (and the higher-priority
125    /// version / group_id / epoch checks at the calling layer) before
126    /// trusting the result.
127    pub fn decode(data: &[u8]) -> Result<Self, CodecError> {
128        ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
129    }
130
131    /// Returns `Ok(())` if `payload_size` equals the actual payload length,
132    /// `Err(CodecError::PayloadSizeMismatch)` otherwise.
133    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    /// Returns the typed `StreamType`, or `CodecError::UnknownEnumValue` for
141    /// unknown stream classes.
142    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    /// Returns `group_id` as a 16-byte array, padding with zeros or
147    /// truncating if necessary.
148    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        // CBOR map should NOT contain the "pf" key when codec is 0.
209        // Decode and confirm pf defaults to 0.
210        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}