Skip to main content

gsp/
signal.rs

1//! GSP signal codec. Six CBOR keys.
2
3use gbp::CodecError;
4use gbp_core::PayloadCodec;
5use serde::{Deserialize, Serialize};
6use serde_bytes::ByteBuf;
7
8/// GSP signal envelope. `args` carries opcode-specific CBOR bytes.
9#[derive(Clone, Debug, Serialize, Deserialize)]
10pub struct GspSignal {
11    /// `SignalType` widened to `u32` for CBOR uint compatibility.
12    #[serde(rename = "t")]
13    pub signal_type: u32,
14    /// Request identifier (echoed in ACK / NACK and used for deduplication).
15    #[serde(rename = "rid")]
16    pub request_id: u32,
17    /// Sender member id.
18    #[serde(rename = "sid")]
19    pub sender_id: u32,
20    /// Role claim (used by `ROLE_CHANGE`).
21    #[serde(rename = "rc")]
22    pub role_claim: u32,
23    /// Declared length of [`args`](Self::args).
24    #[serde(rename = "alen")]
25    pub args_length: u32,
26    /// Opcode-specific CBOR-encoded arguments.
27    #[serde(rename = "args")]
28    pub args: ByteBuf,
29}
30
31impl GspSignal {
32    /// Builds a signal with no arguments.
33    pub fn bare(signal_type: u32, request_id: u32, sender_id: u32) -> Self {
34        Self {
35            signal_type,
36            request_id,
37            sender_id,
38            role_claim: 0,
39            args_length: 0,
40            args: ByteBuf::new(),
41        }
42    }
43
44    /// CBOR-encodes the signal.
45    pub fn to_cbor(&self) -> Vec<u8> {
46        let mut buf = Vec::new();
47        ciborium::into_writer(self, &mut buf).expect("cbor encode");
48        buf
49    }
50
51    /// Decodes a CBOR-encoded signal and validates `args_length`.
52    pub fn from_cbor(data: &[u8]) -> Result<Self, CodecError> {
53        let s: Self = ciborium::from_reader(data).map_err(|e| CodecError::Decode(e.to_string()))?;
54        if s.args_length as usize != s.args.len() {
55            return Err(CodecError::PayloadSizeMismatch);
56        }
57        Ok(s)
58    }
59
60    /// Encodes using the given codec.
61    pub fn to_bytes(&self, codec: PayloadCodec) -> Vec<u8> {
62        match codec {
63            PayloadCodec::Cbor => self.to_cbor(),
64            PayloadCodec::Protobuf => {
65                use prost::Message as _;
66                gbp_proto::gsp::GspSignal::from(self).encode_to_vec()
67            }
68            PayloadCodec::FlatBuffers => {
69                let mut b = gbp_flat::planus::Builder::new();
70                b.finish(gbp_flat::gsp::GspSignal::from(self), None)
71                    .to_vec()
72            }
73        }
74    }
75
76    /// Decodes from the given codec.
77    pub fn from_bytes(data: &[u8], codec: PayloadCodec) -> Result<Self, CodecError> {
78        match codec {
79            PayloadCodec::Cbor => Self::from_cbor(data),
80            PayloadCodec::Protobuf => {
81                use prost::Message as _;
82                let p = gbp_proto::gsp::GspSignal::decode(data)
83                    .map_err(|e| CodecError::Decode(e.to_string()))?;
84                Self::try_from(p).map_err(|_| CodecError::PayloadSizeMismatch)
85            }
86            PayloadCodec::FlatBuffers => {
87                use gbp_flat::planus::ReadAsRoot as _;
88                let r = gbp_flat::gsp::GspSignalRef::read_as_root(data)
89                    .map_err(|e| CodecError::Decode(e.to_string()))?;
90                Self::try_from(r).map_err(|_| CodecError::PayloadSizeMismatch)
91            }
92        }
93    }
94}
95
96// ── Proto conversions ─────────────────────────────────────────────────────────
97
98impl From<&GspSignal> for gbp_proto::gsp::GspSignal {
99    fn from(s: &GspSignal) -> Self {
100        Self {
101            signal_type: s.signal_type,
102            request_id: s.request_id,
103            sender_id: s.sender_id,
104            role_claim: s.role_claim,
105            args_length: s.args_length,
106            args: s.args.to_vec(),
107        }
108    }
109}
110
111impl TryFrom<gbp_proto::gsp::GspSignal> for GspSignal {
112    type Error = ();
113    fn try_from(p: gbp_proto::gsp::GspSignal) -> Result<Self, ()> {
114        if p.args_length as usize != p.args.len() {
115            return Err(());
116        }
117        Ok(Self {
118            signal_type: p.signal_type,
119            request_id: p.request_id,
120            sender_id: p.sender_id,
121            role_claim: p.role_claim,
122            args_length: p.args_length,
123            args: ByteBuf::from(p.args),
124        })
125    }
126}
127
128// ── FlatBuffers conversions ───────────────────────────────────────────────────
129
130impl From<&GspSignal> for gbp_flat::gsp::GspSignal {
131    fn from(s: &GspSignal) -> Self {
132        Self {
133            signal_type: s.signal_type,
134            request_id: s.request_id,
135            sender_id: s.sender_id,
136            role_claim: s.role_claim,
137            args_length: s.args_length,
138            args: if s.args.is_empty() {
139                None
140            } else {
141                Some(s.args.to_vec())
142            },
143        }
144    }
145}
146
147impl<'a> TryFrom<gbp_flat::gsp::GspSignalRef<'a>> for GspSignal {
148    type Error = ();
149    fn try_from(r: gbp_flat::gsp::GspSignalRef<'a>) -> Result<Self, ()> {
150        let args = r.args().map_err(|_| ())?.unwrap_or(&[]).to_vec();
151        let args_length = r.args_length().map_err(|_| ())?;
152        if args_length as usize != args.len() {
153            return Err(());
154        }
155        Ok(Self {
156            signal_type: r.signal_type().map_err(|_| ())?,
157            request_id: r.request_id().map_err(|_| ())?,
158            sender_id: r.sender_id().map_err(|_| ())?,
159            role_claim: r.role_claim().map_err(|_| ())?,
160            args_length,
161            args: ByteBuf::from(args),
162        })
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    fn sample() -> GspSignal {
171        GspSignal::bare(1, 99, 5)
172    }
173
174    #[test]
175    fn cbor_roundtrip() {
176        let orig = sample();
177        let bytes = orig.to_bytes(PayloadCodec::Cbor);
178        let decoded = GspSignal::from_bytes(&bytes, PayloadCodec::Cbor).unwrap();
179        assert_eq!(decoded.signal_type, orig.signal_type);
180        assert_eq!(decoded.request_id, orig.request_id);
181        assert_eq!(decoded.sender_id, orig.sender_id);
182    }
183
184    #[test]
185    fn protobuf_roundtrip() {
186        let orig = sample();
187        let bytes = orig.to_bytes(PayloadCodec::Protobuf);
188        let decoded = GspSignal::from_bytes(&bytes, PayloadCodec::Protobuf).unwrap();
189        assert_eq!(decoded.signal_type, orig.signal_type);
190        assert_eq!(decoded.request_id, orig.request_id);
191        assert_eq!(decoded.sender_id, orig.sender_id);
192    }
193
194    #[test]
195    fn flatbuffers_roundtrip() {
196        let orig = sample();
197        let bytes = orig.to_bytes(PayloadCodec::FlatBuffers);
198        let decoded = GspSignal::from_bytes(&bytes, PayloadCodec::FlatBuffers).unwrap();
199        assert_eq!(decoded.signal_type, orig.signal_type);
200        assert_eq!(decoded.request_id, orig.request_id);
201        assert_eq!(decoded.sender_id, orig.sender_id);
202    }
203
204    #[test]
205    fn codec_bytes_differ() {
206        let sig = sample();
207        let cbor = sig.to_bytes(PayloadCodec::Cbor);
208        let proto = sig.to_bytes(PayloadCodec::Protobuf);
209        let flat = sig.to_bytes(PayloadCodec::FlatBuffers);
210        assert_ne!(cbor, proto);
211        assert_ne!(cbor, flat);
212        assert_ne!(proto, flat);
213    }
214}