1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[repr(u8)]
25#[non_exhaustive]
26pub enum SyncMessageType {
27 Handshake = 0x01,
28 HandshakeAck = 0x02,
29 DeltaPush = 0x10,
30 DeltaAck = 0x11,
31 DeltaReject = 0x12,
32 CollectionSchema = 0x13,
36 CollectionPurged = 0x14,
42 RowPush = 0x15,
50 RowPushReject = 0x16,
54 ShapeSubscribe = 0x20,
55 ShapeSnapshot = 0x21,
56 ShapeDelta = 0x22,
57 ShapeUnsubscribe = 0x23,
58 VectorClockSync = 0x30,
59 TimeseriesPush = 0x40,
61 TimeseriesAck = 0x41,
63 ResyncRequest = 0x50,
66 Throttle = 0x52,
69 TokenRefresh = 0x60,
71 TokenRefreshAck = 0x61,
73 DefinitionSync = 0x70,
76 PresenceUpdate = 0x80,
78 PresenceBroadcast = 0x81,
80 PresenceLeave = 0x82,
82 ArrayDelta = 0x90,
84 ArrayDeltaBatch = 0x91,
86 ArraySnapshot = 0x92,
88 ArraySnapshotChunk = 0x93,
90 ArraySchema = 0x94,
92 ArrayAck = 0x95,
94 ArrayReject = 0x96,
96 ArrayCatchupRequest = 0x97,
98 ArrayOpResults = 0x98,
101 ArrayAdvisoryRefused = 0x99,
104 ColumnarInsert = 0xA0,
106 ColumnarInsertAck = 0xA1,
108 VectorInsert = 0xA2,
110 VectorInsertAck = 0xA3,
112 VectorDelete = 0xA4,
114 VectorDeleteAck = 0xA5,
116 FtsIndex = 0xA6,
118 FtsIndexAck = 0xA7,
120 FtsDelete = 0xA8,
122 FtsDeleteAck = 0xA9,
124 SpatialInsert = 0xAA,
126 SpatialInsertAck = 0xAB,
128 SpatialDelete = 0xAC,
130 SpatialDeleteAck = 0xAD,
132 KvPush = 0xAE,
134 KvPushAck = 0xAF,
136 PingPong = 0xFF,
137}
138
139impl SyncMessageType {
140 pub fn from_u8(v: u8) -> Option<Self> {
141 match v {
142 0x01 => Some(Self::Handshake),
143 0x02 => Some(Self::HandshakeAck),
144 0x10 => Some(Self::DeltaPush),
145 0x11 => Some(Self::DeltaAck),
146 0x12 => Some(Self::DeltaReject),
147 0x13 => Some(Self::CollectionSchema),
148 0x14 => Some(Self::CollectionPurged),
149 0x15 => Some(Self::RowPush),
150 0x16 => Some(Self::RowPushReject),
151 0x20 => Some(Self::ShapeSubscribe),
152 0x21 => Some(Self::ShapeSnapshot),
153 0x22 => Some(Self::ShapeDelta),
154 0x23 => Some(Self::ShapeUnsubscribe),
155 0x30 => Some(Self::VectorClockSync),
156 0x40 => Some(Self::TimeseriesPush),
157 0x41 => Some(Self::TimeseriesAck),
158 0x50 => Some(Self::ResyncRequest),
159 0x52 => Some(Self::Throttle),
160 0x60 => Some(Self::TokenRefresh),
161 0x61 => Some(Self::TokenRefreshAck),
162 0x70 => Some(Self::DefinitionSync),
163 0x80 => Some(Self::PresenceUpdate),
164 0x81 => Some(Self::PresenceBroadcast),
165 0x82 => Some(Self::PresenceLeave),
166 0x90 => Some(Self::ArrayDelta),
167 0x91 => Some(Self::ArrayDeltaBatch),
168 0x92 => Some(Self::ArraySnapshot),
169 0x93 => Some(Self::ArraySnapshotChunk),
170 0x94 => Some(Self::ArraySchema),
171 0x95 => Some(Self::ArrayAck),
172 0x96 => Some(Self::ArrayReject),
173 0x97 => Some(Self::ArrayCatchupRequest),
174 0x98 => Some(Self::ArrayOpResults),
175 0x99 => Some(Self::ArrayAdvisoryRefused),
176 0xA0 => Some(Self::ColumnarInsert),
177 0xA1 => Some(Self::ColumnarInsertAck),
178 0xA2 => Some(Self::VectorInsert),
179 0xA3 => Some(Self::VectorInsertAck),
180 0xA4 => Some(Self::VectorDelete),
181 0xA5 => Some(Self::VectorDeleteAck),
182 0xA6 => Some(Self::FtsIndex),
183 0xA7 => Some(Self::FtsIndexAck),
184 0xA8 => Some(Self::FtsDelete),
185 0xA9 => Some(Self::FtsDeleteAck),
186 0xAA => Some(Self::SpatialInsert),
187 0xAB => Some(Self::SpatialInsertAck),
188 0xAC => Some(Self::SpatialDelete),
189 0xAD => Some(Self::SpatialDeleteAck),
190 0xAE => Some(Self::KvPush),
191 0xAF => Some(Self::KvPushAck),
192 0xFF => Some(Self::PingPong),
193 _ => None,
194 }
195 }
196}
197
198#[non_exhaustive]
212#[derive(Clone)]
213pub struct SyncFrame {
214 pub msg_type: SyncMessageType,
215 pub body: Vec<u8>,
216}
217
218impl SyncFrame {
219 pub const FORMAT_VERSION: u8 = 1;
221
222 pub const HEADER_SIZE: usize = 10;
226
227 pub fn to_bytes(&self) -> Vec<u8> {
231 let len = self.body.len() as u32;
232 let crc = crc32c::crc32c(&self.body);
233 let mut buf = Vec::with_capacity(Self::HEADER_SIZE + self.body.len());
234 buf.push(Self::FORMAT_VERSION);
235 buf.push(self.msg_type as u8);
236 buf.extend_from_slice(&len.to_le_bytes());
237 buf.extend_from_slice(&crc.to_le_bytes());
238 buf.extend_from_slice(&self.body);
239 buf
240 }
241
242 pub fn from_bytes(data: &[u8]) -> Option<Self> {
251 if data.len() < Self::HEADER_SIZE {
252 return None;
253 }
254 let version = data[0];
255 if version != Self::FORMAT_VERSION {
256 return None;
257 }
258 let msg_type = SyncMessageType::from_u8(data[1])?;
259 let len = u32::from_le_bytes(data[2..6].try_into().ok()?) as usize;
260 let expected_crc = u32::from_le_bytes(data[6..10].try_into().ok()?);
261 if data.len() < Self::HEADER_SIZE + len {
262 return None;
263 }
264 let body = data[Self::HEADER_SIZE..Self::HEADER_SIZE + len].to_vec();
265 let actual_crc = crc32c::crc32c(&body);
266 if actual_crc != expected_crc {
267 tracing::warn!(
268 msg_type = data[1],
269 expected_crc,
270 actual_crc,
271 "sync frame CRC32C mismatch; dropping corrupt frame"
272 );
273 return None;
274 }
275 Some(Self { msg_type, body })
276 }
277
278 pub fn new_msgpack<T: zerompk::ToMessagePack>(
280 msg_type: SyncMessageType,
281 value: &T,
282 ) -> Option<Self> {
283 let body = zerompk::to_msgpack_vec(value).ok()?;
284 Some(Self { msg_type, body })
285 }
286
287 pub fn try_encode<T: zerompk::ToMessagePack>(
293 msg_type: SyncMessageType,
294 value: &T,
295 ) -> Option<Self> {
296 match zerompk::to_msgpack_vec(value) {
297 Ok(body) => Some(Self { msg_type, body }),
298 Err(e) => {
299 tracing::error!(
300 msg_type = msg_type as u8,
301 error = %e,
302 "failed to encode sync frame body; dropping response"
303 );
304 None
305 }
306 }
307 }
308
309 pub fn decode_body<T: zerompk::FromMessagePackOwned>(&self) -> Option<T> {
311 zerompk::from_msgpack(&self.body).ok()
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 fn make_frame(msg_type: SyncMessageType, body: Vec<u8>) -> SyncFrame {
320 SyncFrame { msg_type, body }
321 }
322
323 #[test]
324 fn roundtrip_preserves_msg_type_and_body() {
325 let body = b"hello sync world".to_vec();
326 let frame = make_frame(SyncMessageType::PingPong, body.clone());
327 let bytes = frame.to_bytes();
328 let decoded = SyncFrame::from_bytes(&bytes).unwrap();
329 assert_eq!(decoded.msg_type, SyncMessageType::PingPong);
330 assert_eq!(decoded.body, body);
331 }
332
333 #[test]
334 fn flipped_body_byte_returns_none() {
335 let body = b"integrity check".to_vec();
336 let frame = make_frame(SyncMessageType::DeltaPush, body);
337 let mut bytes = frame.to_bytes();
338 bytes[SyncFrame::HEADER_SIZE] ^= 0xFF;
340 assert!(SyncFrame::from_bytes(&bytes).is_none());
341 }
342
343 #[test]
344 fn truncated_buffer_returns_none() {
345 assert!(SyncFrame::from_bytes(&[]).is_none());
347 assert!(SyncFrame::from_bytes(&[1u8; SyncFrame::HEADER_SIZE - 1]).is_none());
348
349 let frame = make_frame(SyncMessageType::PingPong, b"abcdef".to_vec());
351 let bytes = frame.to_bytes();
352 let truncated = &bytes[..bytes.len() - 1];
353 assert!(SyncFrame::from_bytes(truncated).is_none());
354 }
355
356 #[test]
357 fn wrong_version_returns_none() {
358 let frame = make_frame(SyncMessageType::PingPong, b"version test".to_vec());
359 let mut bytes = frame.to_bytes();
360 bytes[0] = SyncFrame::FORMAT_VERSION.wrapping_add(1);
362 assert!(SyncFrame::from_bytes(&bytes).is_none());
363 }
364
365 #[test]
366 fn header_size_is_ten_and_total_length_is_correct() {
367 assert_eq!(SyncFrame::HEADER_SIZE, 10);
368 let body = b"nodedb".to_vec();
369 let frame = make_frame(SyncMessageType::PingPong, body.clone());
370 let bytes = frame.to_bytes();
371 assert_eq!(bytes.len(), SyncFrame::HEADER_SIZE + body.len());
372 }
373
374 #[test]
375 fn crc32c_field_matches_crate_output() {
376 let body = b"crc check".to_vec();
377 let frame = make_frame(SyncMessageType::Handshake, body.clone());
378 let bytes = frame.to_bytes();
379 let stored = u32::from_le_bytes(bytes[6..10].try_into().unwrap());
381 let expected = crc32c::crc32c(&body);
382 assert_eq!(stored, expected);
383 }
384
385 #[test]
386 fn frame_roundtrip() {
387 let ping = crate::sync::wire::PingPongMsg {
388 timestamp_ms: 12345,
389 is_pong: false,
390 };
391 let frame = SyncFrame::new_msgpack(SyncMessageType::PingPong, &ping).unwrap();
392 let bytes = frame.to_bytes();
393 let decoded = SyncFrame::from_bytes(&bytes).unwrap();
394 assert_eq!(decoded.msg_type, SyncMessageType::PingPong);
395 let decoded_ping: crate::sync::wire::PingPongMsg = decoded.decode_body().unwrap();
396 assert_eq!(decoded_ping.timestamp_ms, 12345);
397 assert!(!decoded_ping.is_pong);
398 }
399
400 #[test]
401 fn message_type_roundtrip() {
402 for v in [
403 0x01, 0x02, 0x10, 0x11, 0x12, 0x14, 0x20, 0x21, 0x22, 0x23, 0x30, 0x40, 0x41, 0x50,
404 0x52, 0x60, 0x61, 0x70, 0x80, 0x81, 0x82, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96,
405 0x97, 0x98, 0x99, 0xFF,
406 ] {
407 let mt = SyncMessageType::from_u8(v).unwrap();
408 assert_eq!(mt as u8, v);
409 }
410 assert!(SyncMessageType::from_u8(0x9A).is_none());
411 }
412}