Skip to main content

gbp/
control.rs

1//! GBP control plane message envelope. Six CBOR keys.
2
3use crate::CodecError;
4use serde::{Deserialize, Serialize};
5use serde_bytes::ByteBuf;
6
7/// Control plane message that travels on Stream 0.
8#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct ControlMessage {
10    /// Opcode (see `gbp_core::ControlOpcode`).
11    #[serde(rename = "op")]
12    pub opcode: u16,
13    /// Request identifier (echoed in ACK / NACK).
14    #[serde(rename = "rid")]
15    pub request_id: u32,
16    /// Sender's member id.
17    #[serde(rename = "sid")]
18    pub sender_id: u32,
19    /// Related `transition_id`.
20    #[serde(rename = "tid")]
21    pub transition_id: u32,
22    /// Declared length of [`args`](Self::args).
23    #[serde(rename = "alen")]
24    pub args_length: u32,
25    /// Opcode-specific CBOR arguments.
26    #[serde(rename = "args")]
27    pub args: ByteBuf,
28}
29
30impl ControlMessage {
31    /// Builds an argument-less message.
32    pub fn bare(opcode: u16, request_id: u32, sender_id: u32, transition_id: u32) -> Self {
33        Self {
34            opcode,
35            request_id,
36            sender_id,
37            transition_id,
38            args_length: 0,
39            args: ByteBuf::new(),
40        }
41    }
42
43    /// Builds a message with raw CBOR arguments.
44    pub fn with_args(
45        opcode: u16,
46        request_id: u32,
47        sender_id: u32,
48        transition_id: u32,
49        args: Vec<u8>,
50    ) -> Self {
51        Self {
52            opcode,
53            request_id,
54            sender_id,
55            transition_id,
56            args_length: args.len() as u32,
57            args: ByteBuf::from(args),
58        }
59    }
60
61    /// CBOR-encodes the message.
62    pub fn to_cbor(&self) -> Vec<u8> {
63        let mut buf = Vec::new();
64        ciborium::into_writer(self, &mut buf).expect("cbor encode");
65        buf
66    }
67
68    /// Decodes a CBOR-encoded message and validates `args_length`.
69    pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
70        let m: Self = ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))?;
71        if m.args_length as usize != m.args.len() {
72            return Err(CodecError::PayloadSizeMismatch);
73        }
74        Ok(m)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn bare_message_round_trip() {
84        let msg = ControlMessage::bare(3, 99, 1, 2);
85        let bytes = msg.to_cbor();
86        let decoded = ControlMessage::from_cbor(&bytes).unwrap();
87        assert_eq!(decoded.opcode, 3);
88        assert_eq!(decoded.request_id, 99);
89        assert_eq!(decoded.sender_id, 1);
90        assert_eq!(decoded.transition_id, 2);
91        assert_eq!(decoded.args_length, 0);
92        assert!(decoded.args.is_empty());
93    }
94
95    #[test]
96    fn with_args_round_trip() {
97        let args = vec![0xA1u8, 0x00, 0xF5];
98        let msg = ControlMessage::with_args(7, 1, 2, 3, args.clone());
99        assert_eq!(msg.args_length, 3);
100        let decoded = ControlMessage::from_cbor(&msg.to_cbor()).unwrap();
101        assert_eq!(decoded.args.as_ref(), args.as_slice());
102        assert_eq!(decoded.args_length, 3);
103    }
104
105    #[test]
106    fn args_length_mismatch_rejected() {
107        let mut msg = ControlMessage::bare(1, 0, 0, 0);
108        msg.args = serde_bytes::ByteBuf::from(vec![0xFFu8; 5]);
109        // args_length still 0, args has 5 bytes → mismatch
110        let bytes = {
111            let mut buf = Vec::new();
112            ciborium::into_writer(&msg, &mut buf).unwrap();
113            buf
114        };
115        assert!(matches!(
116            ControlMessage::from_cbor(&bytes),
117            Err(CodecError::PayloadSizeMismatch)
118        ));
119    }
120
121    #[test]
122    fn invalid_cbor_returns_decode_error() {
123        assert!(matches!(
124            ControlMessage::from_cbor(b"\xFF\xFF"),
125            Err(CodecError::Decode(_))
126        ));
127    }
128}