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:
4//! `v, gid, ep, tid, st, sid, fl, seq, psz, pl`. Field `psz` MUST equal the
5//! actual length of `pl`; this is checked on decode.
6
7use crate::CodecError;
8use gbp_core::{GroupId, StreamType};
9use serde::{Deserialize, Serialize};
10use serde_bytes::ByteBuf;
11
12/// CBOR-encoded GBP frame.
13#[derive(Clone, Debug, Serialize, Deserialize)]
14pub struct GbpFrame {
15    /// Protocol version (currently `1`).
16    #[serde(rename = "v")]
17    pub version: u8,
18    /// 16-byte group identifier.
19    #[serde(rename = "gid")]
20    pub group_id: ByteBuf,
21    /// Sender's current epoch.
22    #[serde(rename = "ep")]
23    pub epoch: u64,
24    /// Last applied `transition_id`.
25    #[serde(rename = "tid")]
26    pub transition_id: u32,
27    /// StreamType as a `u8` (see `gbp_core::StreamType`).
28    #[serde(rename = "st")]
29    pub stream_type: u8,
30    /// Logical stream identifier within the session.
31    #[serde(rename = "sid")]
32    pub stream_id: u32,
33    /// Delivery flags (see `gbp_core::GbpFlags`).
34    #[serde(rename = "fl")]
35    pub flags: u16,
36    /// Per-stream sequence number (replay window key).
37    #[serde(rename = "seq")]
38    pub sequence_no: u32,
39    /// Declared payload length; MUST equal `encrypted_payload.len()`.
40    #[serde(rename = "psz")]
41    pub payload_size: u32,
42    /// Encrypted payload (an opaque byte string).
43    #[serde(rename = "pl")]
44    pub encrypted_payload: ByteBuf,
45}
46
47impl GbpFrame {
48    /// Builds a frame from already-encrypted payload bytes.
49    ///
50    /// `payload_size` is set to `encrypted_payload.len()` automatically.
51    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    /// Serialises the frame into a freshly allocated CBOR byte vector.
76    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    /// Decodes a CBOR-encoded frame and validates `payload_size`.
83    pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
84        let f: Self = ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))?;
85        if f.payload_size as usize != f.encrypted_payload.len() {
86            return Err(CodecError::PayloadSizeMismatch);
87        }
88        Ok(f)
89    }
90
91    /// Returns the typed `StreamType`, or `CodecError::UnknownEnumValue` for
92    /// unknown stream classes.
93    pub fn stream_type_typed(&self) -> Result<StreamType, CodecError> {
94        StreamType::try_from(self.stream_type as u32).map_err(CodecError::UnknownEnumValue)
95    }
96
97    /// Returns `group_id` as a 16-byte array, padding with zeros or
98    /// truncating if necessary.
99    pub fn group_id_array(&self) -> GroupId {
100        let mut out = [0u8; 16];
101        let n = self.group_id.len().min(16);
102        out[..n].copy_from_slice(&self.group_id[..n]);
103        out
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use gbp_core::GbpFlags;
111
112    #[test]
113    fn frame_roundtrip() {
114        let f = GbpFrame::new(
115            [0xAA; 16],
116            42,
117            7,
118            StreamType::Text,
119            201,
120            GbpFlags::ORDERED | GbpFlags::RELIABLE,
121            1,
122            vec![1, 2, 3, 4, 5],
123        );
124        let bytes = f.to_cbor();
125        let back = GbpFrame::from_cbor(&bytes).unwrap();
126        assert_eq!(back.epoch, 42);
127        assert_eq!(back.transition_id, 7);
128        assert_eq!(back.stream_type_typed().unwrap(), StreamType::Text);
129        assert_eq!(back.encrypted_payload.as_slice(), &[1, 2, 3, 4, 5]);
130    }
131
132    #[test]
133    fn frame_rejects_bad_payload_size() {
134        let mut f = GbpFrame::new([0; 16], 1, 0, StreamType::Text, 1, 0, 1, vec![1, 2, 3]);
135        f.payload_size = 99;
136        let mut bytes = Vec::new();
137        ciborium::into_writer(&f, &mut bytes).unwrap();
138        assert!(matches!(
139            GbpFrame::from_cbor(&bytes),
140            Err(CodecError::PayloadSizeMismatch)
141        ));
142    }
143}