Skip to main content

gbp_proto/
lib.rs

1//! Protobuf codec for the Group Protocol Stack (GBP/GTP/GAP/GSP).
2//!
3//! Alternative wire format to CBOR per gbp_rfc §12.2. All message types
4//! derive [`prost::Message`] so they can be encoded/decoded without protoc.
5//!
6//! # Modules
7//! - [`gbp`] — GbpFrame, ControlMessage, ErrorObject
8//! - [`gtp`] — GtpMessage, AttachmentManifest, AttachmentChunk
9//! - [`gap`] — GapPayload
10//! - [`gsp`] — GspSignal
11
12/// GBP (Group Base Protocol) messages.
13pub mod gbp {
14    /// A framed GBP envelope carrying an encrypted payload.
15    #[derive(Clone, PartialEq, prost::Message)]
16    pub struct GbpFrame {
17        /// Protocol version.
18        #[prost(uint32, tag = "1")]
19        pub version: u32,
20        /// 16-byte group identifier.
21        #[prost(bytes = "vec", tag = "2")]
22        pub group_id: Vec<u8>,
23        /// MLS epoch.
24        #[prost(uint64, tag = "3")]
25        pub epoch: u64,
26        /// Transition identifier.
27        #[prost(uint32, tag = "4")]
28        pub transition_id: u32,
29        /// Stream type discriminant.
30        #[prost(uint32, tag = "5")]
31        pub stream_type: u32,
32        /// Stream identifier.
33        #[prost(uint32, tag = "6")]
34        pub stream_id: u32,
35        /// Frame flags bitmask.
36        #[prost(uint32, tag = "7")]
37        pub flags: u32,
38        /// Monotonic sequence number.
39        #[prost(uint64, tag = "8")]
40        pub sequence_no: u64,
41        /// AEAD-encrypted payload bytes.
42        #[prost(bytes = "vec", tag = "9")]
43        pub encrypted_payload: Vec<u8>,
44    }
45
46    /// GBP control-plane message.
47    #[derive(Clone, PartialEq, prost::Message)]
48    pub struct ControlMessage {
49        /// Control opcode.
50        #[prost(uint32, tag = "1")]
51        pub opcode: u32,
52        /// Request/response correlation identifier.
53        #[prost(uint32, tag = "2")]
54        pub request_id: u32,
55        /// Sender member identifier.
56        #[prost(uint32, tag = "3")]
57        pub sender_id: u32,
58        /// Transition this message belongs to.
59        #[prost(uint32, tag = "4")]
60        pub transition_id: u32,
61        /// Opcode-specific CBOR-encoded arguments.
62        #[prost(bytes = "vec", tag = "5")]
63        pub args: Vec<u8>,
64    }
65
66    /// GBP structured error.
67    #[derive(Clone, PartialEq, prost::Message)]
68    pub struct ErrorObject {
69        /// Numeric error code.
70        #[prost(uint32, tag = "1")]
71        pub code: u32,
72        /// Error class (e.g. transport, crypto, protocol).
73        #[prost(uint32, tag = "2")]
74        pub class: u32,
75        /// Whether the sender may retry after a back-off.
76        #[prost(bool, tag = "3")]
77        pub retryable: bool,
78        /// Whether this error is unrecoverable for the session.
79        #[prost(bool, tag = "4")]
80        pub fatal: bool,
81        /// Human-readable reason string.
82        #[prost(string, tag = "5")]
83        pub reason: String,
84    }
85}
86
87/// GTP (Group Text Protocol) messages.
88pub mod gtp {
89    /// A GTP text message envelope.
90    #[derive(Clone, PartialEq, prost::Message)]
91    pub struct GtpMessage {
92        /// Globally unique message identifier.
93        #[prost(uint64, tag = "1")]
94        pub message_id: u64,
95        /// Sender member identifier.
96        #[prost(uint32, tag = "2")]
97        pub sender_id: u32,
98        /// Wall-clock send time in milliseconds since Unix epoch.
99        #[prost(uint64, tag = "3")]
100        pub timestamp_ms: u64,
101        /// Request/response correlation identifier.
102        #[prost(uint32, tag = "4")]
103        pub request_id: u32,
104        /// Message flags bitmask.
105        #[prost(uint32, tag = "5")]
106        pub flags: u32,
107        /// Content-type discriminant.
108        #[prost(uint32, tag = "6")]
109        pub content_type: u32,
110        /// Byte length of [`content`](Self::content).
111        #[prost(uint32, tag = "7")]
112        pub content_length: u32,
113        /// Message payload bytes (text, JSON, or attachment reference).
114        #[prost(bytes = "vec", tag = "8")]
115        pub content: Vec<u8>,
116    }
117
118    /// Attachment manifest sent before the chunk stream.
119    #[derive(Clone, PartialEq, prost::Message)]
120    pub struct AttachmentManifest {
121        /// Unique attachment identifier (sender-scoped).
122        #[prost(uint64, tag = "1")]
123        pub attachment_id: u64,
124        /// Original filename (UTF-8, no path components).
125        #[prost(string, tag = "2")]
126        pub filename: String,
127        /// MIME type string.
128        #[prost(string, tag = "3")]
129        pub mime_type: String,
130        /// Total byte length of the reassembled payload.
131        #[prost(uint64, tag = "4")]
132        pub total_size: u64,
133        /// Number of chunks.
134        #[prost(uint32, tag = "5")]
135        pub chunk_count: u32,
136        /// SHA-256 hash of the complete payload (32 bytes).
137        #[prost(bytes = "vec", tag = "6")]
138        pub sha256: Vec<u8>,
139    }
140
141    /// One chunk of an attachment payload.
142    #[derive(Clone, PartialEq, prost::Message)]
143    pub struct AttachmentChunk {
144        /// Attachment this chunk belongs to.
145        #[prost(uint64, tag = "1")]
146        pub attachment_id: u64,
147        /// Zero-based chunk index.
148        #[prost(uint32, tag = "2")]
149        pub chunk_index: u32,
150        /// Total number of chunks.
151        #[prost(uint32, tag = "3")]
152        pub chunk_count: u32,
153        /// Chunk payload bytes.
154        #[prost(bytes = "vec", tag = "4")]
155        pub data: Vec<u8>,
156    }
157}
158
159/// GAP (Group Audio Protocol) messages.
160pub mod gap {
161    /// An encrypted audio frame payload.
162    #[derive(Clone, PartialEq, prost::Message)]
163    pub struct GapPayload {
164        /// Audio source identifier.
165        #[prost(uint32, tag = "1")]
166        pub media_source_id: u32,
167        /// RTP sequence number (16-bit widened to u32).
168        #[prost(uint32, tag = "2")]
169        pub rtp_sequence: u32,
170        /// 48 kHz RTP timestamp.
171        #[prost(uint64, tag = "3")]
172        pub rtp_timestamp: u64,
173        /// Key phase (MLS epoch binding).
174        #[prost(uint32, tag = "4")]
175        pub key_phase: u32,
176        /// Opus-encoded frame bytes.
177        #[prost(bytes = "vec", tag = "5")]
178        pub opus_frame: Vec<u8>,
179    }
180}
181
182/// GSP (Group Signaling Protocol) messages.
183pub mod gsp {
184    /// A GSP signal envelope.
185    #[derive(Clone, PartialEq, prost::Message)]
186    pub struct GspSignal {
187        /// Signal type discriminant.
188        #[prost(uint32, tag = "1")]
189        pub signal_type: u32,
190        /// Request/response correlation identifier.
191        #[prost(uint32, tag = "2")]
192        pub request_id: u32,
193        /// Sender member identifier.
194        #[prost(uint32, tag = "3")]
195        pub sender_id: u32,
196        /// Role claim (used by ROLE_CHANGE signals).
197        #[prost(uint32, tag = "4")]
198        pub role_claim: u32,
199        /// Declared byte length of [`args`](Self::args).
200        #[prost(uint32, tag = "5")]
201        pub args_length: u32,
202        /// Opcode-specific CBOR-encoded arguments.
203        #[prost(bytes = "vec", tag = "6")]
204        pub args: Vec<u8>,
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use prost::Message;
211
212    #[test]
213    fn gbp_frame_round_trip() {
214        let frame = crate::gbp::GbpFrame {
215            version: 1,
216            group_id: vec![0u8; 16],
217            epoch: 42,
218            transition_id: 7,
219            stream_type: 1,
220            stream_id: 0,
221            flags: 0,
222            sequence_no: 100,
223            encrypted_payload: b"hello".to_vec(),
224        };
225        let encoded = frame.encode_to_vec();
226        let decoded = crate::gbp::GbpFrame::decode(encoded.as_slice()).unwrap();
227        assert_eq!(frame, decoded);
228    }
229
230    #[test]
231    fn control_message_round_trip() {
232        let msg = crate::gbp::ControlMessage {
233            opcode: 3,
234            request_id: 99,
235            sender_id: 1,
236            transition_id: 2,
237            args: vec![0xA0],
238        };
239        let encoded = msg.encode_to_vec();
240        let decoded = crate::gbp::ControlMessage::decode(encoded.as_slice()).unwrap();
241        assert_eq!(msg, decoded);
242    }
243
244    #[test]
245    fn error_object_round_trip() {
246        let err = crate::gbp::ErrorObject {
247            code: 404,
248            class: 1,
249            retryable: true,
250            fatal: false,
251            reason: "not found".to_string(),
252        };
253        let encoded = err.encode_to_vec();
254        let decoded = crate::gbp::ErrorObject::decode(encoded.as_slice()).unwrap();
255        assert_eq!(err, decoded);
256    }
257
258    #[test]
259    fn gtp_message_round_trip() {
260        let msg = crate::gtp::GtpMessage {
261            message_id: 12345,
262            sender_id: 1,
263            timestamp_ms: 1_700_000_000_000,
264            request_id: 0,
265            flags: 0,
266            content_type: 1,
267            content_length: 5,
268            content: b"hello".to_vec(),
269        };
270        let encoded = msg.encode_to_vec();
271        let decoded = crate::gtp::GtpMessage::decode(encoded.as_slice()).unwrap();
272        assert_eq!(msg, decoded);
273    }
274
275    #[test]
276    fn attachment_manifest_round_trip() {
277        let m = crate::gtp::AttachmentManifest {
278            attachment_id: 1,
279            filename: "test.png".to_string(),
280            mime_type: "image/png".to_string(),
281            total_size: 1024,
282            chunk_count: 1,
283            sha256: vec![0u8; 32],
284        };
285        let encoded = m.encode_to_vec();
286        let decoded = crate::gtp::AttachmentManifest::decode(encoded.as_slice()).unwrap();
287        assert_eq!(m, decoded);
288    }
289
290    #[test]
291    fn attachment_chunk_round_trip() {
292        let c = crate::gtp::AttachmentChunk {
293            attachment_id: 1,
294            chunk_index: 0,
295            chunk_count: 1,
296            data: vec![1, 2, 3, 4],
297        };
298        let encoded = c.encode_to_vec();
299        let decoded = crate::gtp::AttachmentChunk::decode(encoded.as_slice()).unwrap();
300        assert_eq!(c, decoded);
301    }
302
303    #[test]
304    fn gap_payload_round_trip() {
305        let p = crate::gap::GapPayload {
306            media_source_id: 5,
307            rtp_sequence: 100,
308            rtp_timestamp: 960,
309            key_phase: 2,
310            opus_frame: vec![0xAB, 0xCD],
311        };
312        let encoded = p.encode_to_vec();
313        let decoded = crate::gap::GapPayload::decode(encoded.as_slice()).unwrap();
314        assert_eq!(p, decoded);
315    }
316
317    #[test]
318    fn gsp_signal_round_trip() {
319        let s = crate::gsp::GspSignal {
320            signal_type: 1,
321            request_id: 42,
322            sender_id: 3,
323            role_claim: 0,
324            args_length: 0,
325            args: vec![],
326        };
327        let encoded = s.encode_to_vec();
328        let decoded = crate::gsp::GspSignal::decode(encoded.as_slice()).unwrap();
329        assert_eq!(s, decoded);
330    }
331
332    #[test]
333    fn empty_frame_decodes_to_defaults() {
334        let decoded = crate::gbp::GbpFrame::decode(&[][..]).unwrap();
335        assert_eq!(decoded.version, 0);
336        assert!(decoded.encrypted_payload.is_empty());
337    }
338
339    #[test]
340    fn encoded_size_is_nonzero_for_nonempty_frame() {
341        let frame = crate::gbp::GbpFrame {
342            version: 1,
343            sequence_no: 1,
344            ..Default::default()
345        };
346        assert!(!frame.encode_to_vec().is_empty());
347    }
348}