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    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    /// Returns the `payload_format` field as a [`PayloadCodec`], falling back
92    /// to [`PayloadCodec::Cbor`] for unknown discriminants.
93    pub fn payload_codec(&self) -> PayloadCodec {
94        PayloadCodec::from_u8(self.payload_format).unwrap_or(PayloadCodec::Cbor)
95    }
96
97    /// Serialises the frame into a freshly allocated CBOR byte vector.
98    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    /// Decodes a CBOR-encoded frame **and** validates `payload_size`.
105    ///
106    /// The order of all §6.2 checks (`version`, `group_id`, `epoch`,
107    /// `payload_size`, `transition_id`, `sequence_no`) is what governs which
108    /// error a malformed frame produces. Most callers should use
109    /// [`GroupNode::on_wire`](https://docs.rs/gbp-node) which decodes via
110    /// [`GbpFrame::decode`] and runs the full pipeline. This convenience
111    /// wrapper exists for tests and ad-hoc tooling that want both decode
112    /// and the length check in one shot.
113    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    /// Decodes a CBOR-encoded frame **without** running any §6.2 checks.
120    ///
121    /// Use [`GbpFrame::validate_payload_size`] (and the higher-priority
122    /// version / group_id / epoch checks at the calling layer) before
123    /// trusting the result.
124    pub fn decode(data: &[u8]) -> Result<Self, CodecError> {
125        ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
126    }
127
128    /// Returns `Ok(())` if `payload_size` equals the actual payload length,
129    /// `Err(CodecError::PayloadSizeMismatch)` otherwise.
130    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    /// Returns the typed `StreamType`, or `CodecError::UnknownEnumValue` for
138    /// unknown stream classes.
139    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    /// Returns `group_id` as a 16-byte array, padding with zeros or
144    /// truncating if necessary.
145    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        // CBOR map should NOT contain the "pf" key when codec is 0.
206        // Decode and confirm pf defaults to 0.
207        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}