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    ///
84    /// The order of all §6.2 checks (`version`, `group_id`, `epoch`,
85    /// `payload_size`, `transition_id`, `sequence_no`) is what governs which
86    /// error a malformed frame produces. Most callers should use
87    /// [`GroupNode::on_wire`](https://docs.rs/gbp-node) which decodes via
88    /// [`GbpFrame::decode`] and runs the full pipeline. This convenience
89    /// wrapper exists for tests and ad-hoc tooling that want both decode
90    /// and the length check in one shot.
91    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    /// Decodes a CBOR-encoded frame **without** running any §6.2 checks.
98    ///
99    /// Use [`GbpFrame::validate_payload_size`] (and the higher-priority
100    /// version / group_id / epoch checks at the calling layer) before
101    /// trusting the result.
102    pub fn decode(data: &[u8]) -> Result<Self, CodecError> {
103        ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))
104    }
105
106    /// Returns `Ok(())` if `payload_size` equals the actual payload length,
107    /// `Err(CodecError::PayloadSizeMismatch)` otherwise.
108    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    /// Returns the typed `StreamType`, or `CodecError::UnknownEnumValue` for
116    /// unknown stream classes.
117    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    /// Returns `group_id` as a 16-byte array, padding with zeros or
122    /// truncating if necessary.
123    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}