Skip to main content

synapse_proto/
lib.rs

1//! Synapse Protocol Buffer Definitions
2//!
3//! This crate contains the core protobuf definitions for the Synapse RPC protocol.
4//! All messages use the `SynapseMessage` envelope with a discriminator for message type.
5
6pub use bytes::Bytes;
7
8/// Serde helpers for protobuf types
9pub mod serde_helpers {
10    use bytes::Bytes;
11    use serde::{Deserialize, Deserializer, Serializer};
12
13    /// Serialize request_id (16 bytes) as UUID string
14    pub mod uuid_string {
15        use super::*;
16        use serde::Serialize;
17
18        pub fn serialize<S>(bytes: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
19        where
20            S: Serializer,
21        {
22            if bytes.len() == 16 {
23                let mut uuid_bytes = [0u8; 16];
24                uuid_bytes.copy_from_slice(&bytes[..]);
25
26                // Format as UUID string: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
27                let uuid_str = format!(
28                    "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
29                    uuid_bytes[0],
30                    uuid_bytes[1],
31                    uuid_bytes[2],
32                    uuid_bytes[3],
33                    uuid_bytes[4],
34                    uuid_bytes[5],
35                    uuid_bytes[6],
36                    uuid_bytes[7],
37                    uuid_bytes[8],
38                    uuid_bytes[9],
39                    uuid_bytes[10],
40                    uuid_bytes[11],
41                    uuid_bytes[12],
42                    uuid_bytes[13],
43                    uuid_bytes[14],
44                    uuid_bytes[15]
45                );
46                uuid_str.serialize(serializer)
47            } else {
48                // Fallback to base64 for non-UUID bytes
49                base64::Engine::encode(&base64::engine::general_purpose::STANDARD, bytes)
50                    .serialize(serializer)
51            }
52        }
53
54        pub fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
55        where
56            D: Deserializer<'de>,
57        {
58            use serde::de::Error;
59            let s = String::deserialize(deserializer)?;
60
61            // Try to parse as UUID first (with or without hyphens)
62            let cleaned = s.replace("-", "");
63            if cleaned.len() == 32 {
64                // Parse hex string as UUID
65                let mut bytes = Vec::with_capacity(16);
66                for i in 0..16 {
67                    let byte_str = &cleaned[i * 2..i * 2 + 2];
68                    if let Ok(byte) = u8::from_str_radix(byte_str, 16) {
69                        bytes.push(byte);
70                    } else {
71                        return Err(D::Error::custom("Invalid UUID hex"));
72                    }
73                }
74                Ok(Bytes::from(bytes))
75            } else {
76                // Fallback to base64 decode
77                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.as_bytes())
78                    .map(Bytes::from)
79                    .map_err(D::Error::custom)
80            }
81        }
82    }
83
84    /// Serialize payload - supports both plain text/JSON and base64
85    pub mod payload_flexible {
86        use super::*;
87        use serde::Serialize;
88
89        pub fn serialize<S>(bytes: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
90        where
91            S: Serializer,
92        {
93            // Try to interpret as UTF-8 string first
94            if let Ok(s) = std::str::from_utf8(bytes) {
95                // If it's valid UTF-8, serialize as string
96                s.serialize(serializer)
97            } else {
98                // Otherwise use base64
99                base64::Engine::encode(&base64::engine::general_purpose::STANDARD, bytes)
100                    .serialize(serializer)
101            }
102        }
103
104        pub fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
105        where
106            D: Deserializer<'de>,
107        {
108            let s = String::deserialize(deserializer)?;
109
110            // Try base64 decode first
111            if let Ok(decoded) =
112                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.as_bytes())
113            {
114                // If it decodes successfully and is different from the original, use it
115                if decoded != s.as_bytes() {
116                    return Ok(Bytes::from(decoded));
117                }
118            }
119
120            // Otherwise treat as plain text/JSON
121            Ok(Bytes::from(s.into_bytes()))
122        }
123    }
124
125    /// Serialize Bytes as base64 string (for other byte fields)
126    pub mod bytes_base64 {
127        use super::*;
128
129        pub fn serialize<S>(bytes: &Bytes, serializer: S) -> Result<S::Ok, S::Error>
130        where
131            S: Serializer,
132        {
133            use serde::Serialize;
134            base64::Engine::encode(&base64::engine::general_purpose::STANDARD, bytes)
135                .serialize(serializer)
136        }
137
138        pub fn deserialize<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
139        where
140            D: Deserializer<'de>,
141        {
142            use serde::de::Error;
143            let s = String::deserialize(deserializer)?;
144            base64::Engine::decode(&base64::engine::general_purpose::STANDARD, s.as_bytes())
145                .map(Bytes::from)
146                .map_err(D::Error::custom)
147        }
148    }
149}
150
151// Include generated code
152include!(concat!(env!("OUT_DIR"), "/synapse.rs"));
153
154#[cfg(test)]
155mod tests {
156    use super::serde_helpers::*;
157    use bytes::Bytes;
158
159    // Helper: roundtrip through JSON using a wrapper struct
160    #[derive(serde::Serialize, serde::Deserialize, Debug)]
161    struct UuidWrapper {
162        #[serde(with = "uuid_string")]
163        id: Bytes,
164    }
165
166    #[derive(serde::Serialize, serde::Deserialize, Debug)]
167    struct PayloadWrapper {
168        #[serde(with = "payload_flexible")]
169        data: Bytes,
170    }
171
172    #[derive(serde::Serialize, serde::Deserialize, Debug)]
173    struct Base64Wrapper {
174        #[serde(with = "bytes_base64")]
175        data: Bytes,
176    }
177
178    // ========== uuid_string ==========
179
180    #[test]
181    fn test_uuid_serialize_16_bytes() {
182        let bytes = Bytes::from(vec![
183            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
184            0x00, 0x00,
185        ]);
186        let wrapper = UuidWrapper { id: bytes };
187        let json = serde_json::to_string(&wrapper).unwrap();
188        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
189    }
190
191    #[test]
192    fn test_uuid_roundtrip() {
193        let original = Bytes::from(vec![
194            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
195            0x0f, 0x10,
196        ]);
197        let wrapper = UuidWrapper {
198            id: original.clone(),
199        };
200        let json = serde_json::to_string(&wrapper).unwrap();
201        let decoded: UuidWrapper = serde_json::from_str(&json).unwrap();
202        assert_eq!(original, decoded.id);
203    }
204
205    #[test]
206    fn test_uuid_non_16_bytes_falls_back_to_base64() {
207        let bytes = Bytes::from(vec![0x01, 0x02, 0x03]); // Not 16 bytes
208        let wrapper = UuidWrapper { id: bytes };
209        let json = serde_json::to_string(&wrapper).unwrap();
210        // Should not contain UUID format hyphens in the expected pattern
211        assert!(!json.contains('-'));
212    }
213
214    #[test]
215    fn test_uuid_deserialize_with_hyphens() {
216        let json = r#"{"id":"550e8400-e29b-41d4-a716-446655440000"}"#;
217        let wrapper: UuidWrapper = serde_json::from_str(json).unwrap();
218        assert_eq!(wrapper.id.len(), 16);
219        assert_eq!(wrapper.id[0], 0x55);
220        assert_eq!(wrapper.id[15], 0x00);
221    }
222
223    #[test]
224    fn test_uuid_deserialize_without_hyphens() {
225        let json = r#"{"id":"550e8400e29b41d4a716446655440000"}"#;
226        let wrapper: UuidWrapper = serde_json::from_str(json).unwrap();
227        assert_eq!(wrapper.id.len(), 16);
228        assert_eq!(wrapper.id[0], 0x55);
229    }
230
231    // ========== payload_flexible ==========
232
233    #[test]
234    fn test_payload_utf8_serializes_as_string() {
235        let wrapper = PayloadWrapper {
236            data: Bytes::from("hello world"),
237        };
238        let json = serde_json::to_string(&wrapper).unwrap();
239        assert!(json.contains("hello world"));
240    }
241
242    #[test]
243    fn test_payload_binary_serializes_as_base64() {
244        let wrapper = PayloadWrapper {
245            data: Bytes::from(vec![0xFF, 0xFE, 0x00, 0x01]),
246        };
247        let json = serde_json::to_string(&wrapper).unwrap();
248        // Should not contain raw binary, should be base64 encoded
249        assert!(!json.contains('\u{ffff}'));
250    }
251
252    #[test]
253    fn test_payload_plain_text_roundtrip() {
254        // Plain text that is NOT valid base64 should survive roundtrip
255        let wrapper = PayloadWrapper {
256            data: Bytes::from("{\"key\":\"value\"}"),
257        };
258        let json = serde_json::to_string(&wrapper).unwrap();
259        let decoded: PayloadWrapper = serde_json::from_str(&json).unwrap();
260        assert_eq!(decoded.data, Bytes::from("{\"key\":\"value\"}"));
261    }
262
263    // ========== bytes_base64 ==========
264
265    #[test]
266    fn test_base64_roundtrip() {
267        let original = Bytes::from(vec![0x00, 0xFF, 0x42, 0x13, 0x37]);
268        let wrapper = Base64Wrapper {
269            data: original.clone(),
270        };
271        let json = serde_json::to_string(&wrapper).unwrap();
272        let decoded: Base64Wrapper = serde_json::from_str(&json).unwrap();
273        assert_eq!(original, decoded.data);
274    }
275
276    #[test]
277    fn test_base64_invalid_input() {
278        let json = r#"{"data":"not valid base64!!@@"}"#;
279        let result: Result<Base64Wrapper, _> = serde_json::from_str(json);
280        assert!(result.is_err());
281    }
282}