1use std::io::{self, Read, Write};
2
3pub const MAX_FRAME_SIZE: usize = 16 * 1024 * 1024; pub const VAR_PROTOCOL_VERSION: u16 = 2;
9
10#[repr(u8)]
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum MessageType {
14 SyncRequest = 1,
15 ShardInfo = 2,
16 EntryBatch = 3,
17 CaughtUp = 4,
18 Ack = 5,
19 Heartbeat = 6,
20 Error = 255,
21}
22
23impl MessageType {
24 fn from_u8(v: u8) -> Option<Self> {
25 match v {
26 1 => Some(Self::SyncRequest),
27 2 => Some(Self::ShardInfo),
28 3 => Some(Self::EntryBatch),
29 4 => Some(Self::CaughtUp),
30 5 => Some(Self::Ack),
31 6 => Some(Self::Heartbeat),
32 255 => Some(Self::Error),
33 _ => None,
34 }
35 }
36}
37
38impl crate::frame_reader::FrameKind for MessageType {
39 const MAX_PAYLOAD: usize = MAX_FRAME_SIZE;
40 fn from_u8(b: u8) -> Option<Self> {
41 MessageType::from_u8(b)
44 }
45}
46
47pub type Frame = crate::frame_reader::GenericFrame<MessageType>;
49
50pub fn write_frame(w: &mut impl Write, frame: &Frame) -> io::Result<()> {
51 w.write_all(&[frame.msg_type as u8])?;
52 w.write_all(&(frame.payload.len() as u32).to_le_bytes())?;
53 w.write_all(&frame.payload)?;
54 w.flush()
55}
56
57pub fn read_frame(r: &mut impl Read) -> io::Result<Frame> {
58 let mut type_buf = [0u8; 1];
59 r.read_exact(&mut type_buf)?;
60 let msg_type = MessageType::from_u8(type_buf[0]).ok_or_else(|| {
61 io::Error::new(
62 io::ErrorKind::InvalidData,
63 format!("unknown message type: {}", type_buf[0]),
64 )
65 })?;
66 let mut len_buf = [0u8; 4];
67 r.read_exact(&mut len_buf)?;
68 let len = u32::from_le_bytes(len_buf) as usize;
69 if len > MAX_FRAME_SIZE {
70 return Err(io::Error::new(
71 io::ErrorKind::InvalidData,
72 "frame too large",
73 ));
74 }
75 let mut payload = vec![0u8; len];
76 if len > 0 {
77 r.read_exact(&mut payload)?;
78 }
79 Ok(Frame { msg_type, payload })
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct SyncRequest {
88 pub protocol_version: u16,
89 pub shard_id: u8,
90 pub from_gsn: u64,
91 pub key_len: u16,
92}
93
94impl SyncRequest {
95 pub fn encode(&self) -> Frame {
96 let mut payload = Vec::with_capacity(13);
97 payload.extend_from_slice(&self.protocol_version.to_le_bytes());
98 payload.push(self.shard_id);
99 payload.extend_from_slice(&self.from_gsn.to_le_bytes());
100 payload.extend_from_slice(&self.key_len.to_le_bytes());
101 Frame {
102 msg_type: MessageType::SyncRequest,
103 payload,
104 }
105 }
106
107 pub fn decode(payload: &[u8]) -> io::Result<Self> {
108 if payload.len() != 13 {
109 return Err(io::Error::new(
110 io::ErrorKind::InvalidData,
111 "invalid SyncRequest length",
112 ));
113 }
114 let protocol_version =
115 u16::from_le_bytes(payload[0..2].try_into().expect("validated length"));
116 let shard_id = payload[2];
117 let from_gsn = u64::from_le_bytes(payload[3..11].try_into().expect("validated length"));
118 let key_len = u16::from_le_bytes(payload[11..13].try_into().expect("validated length"));
119 Ok(Self {
120 protocol_version,
121 shard_id,
122 from_gsn,
123 key_len,
124 })
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct ShardInfo {
132 pub protocol_version: u16,
133 pub shard_count: u8,
134 pub max_file_size: u64,
135}
136
137impl ShardInfo {
138 pub fn encode(&self) -> Frame {
139 let mut payload = Vec::with_capacity(11);
140 payload.extend_from_slice(&self.protocol_version.to_le_bytes());
141 payload.push(self.shard_count);
142 payload.extend_from_slice(&self.max_file_size.to_le_bytes());
143 Frame {
144 msg_type: MessageType::ShardInfo,
145 payload,
146 }
147 }
148
149 pub fn decode(payload: &[u8]) -> io::Result<Self> {
150 if payload.len() != 11 {
151 return Err(io::Error::new(
152 io::ErrorKind::InvalidData,
153 "invalid ShardInfo length",
154 ));
155 }
156 Ok(Self {
157 protocol_version: u16::from_le_bytes(
158 payload[0..2].try_into().expect("validated length"),
159 ),
160 shard_count: payload[2],
161 max_file_size: u64::from_le_bytes(payload[3..11].try_into().expect("validated length")),
162 })
163 }
164}
165
166pub struct WireEntry {
169 pub entry_len: u32,
170 pub key_len: u16,
171 pub gsn: u64,
172 pub data: Vec<u8>,
173}
174
175pub struct EntryBatch {
176 pub shard_id: u8,
177 pub entries: Vec<WireEntry>,
178}
179
180impl EntryBatch {
181 pub fn encode(&self) -> Frame {
182 let mut payload = Vec::with_capacity(5 + self.entries.len() * 64);
183 payload.push(self.shard_id);
184 payload.extend_from_slice(&(self.entries.len() as u32).to_le_bytes());
185 for e in &self.entries {
186 payload.extend_from_slice(&e.entry_len.to_le_bytes());
187 payload.extend_from_slice(&e.key_len.to_le_bytes());
188 payload.extend_from_slice(&e.gsn.to_le_bytes());
189 payload.extend_from_slice(&e.data);
190 }
191 Frame {
192 msg_type: MessageType::EntryBatch,
193 payload,
194 }
195 }
196
197 pub fn decode(payload: &[u8]) -> io::Result<Self> {
198 if payload.len() < 5 {
199 return Err(io::Error::new(
200 io::ErrorKind::InvalidData,
201 "EntryBatch too short",
202 ));
203 }
204 let shard_id = payload[0];
205 let count = u32::from_le_bytes(payload[1..5].try_into().expect("impossible")) as usize;
206 let mut entries = Vec::with_capacity(count);
207 let mut off = 5;
208 for _ in 0..count {
209 if off + 14 > payload.len() {
210 return Err(io::Error::new(
211 io::ErrorKind::InvalidData,
212 "EntryBatch truncated",
213 ));
214 }
215 let entry_len =
216 u32::from_le_bytes(payload[off..off + 4].try_into().expect("impossible"));
217 let key_len =
218 u16::from_le_bytes(payload[off + 4..off + 6].try_into().expect("impossible"));
219 let gsn =
220 u64::from_le_bytes(payload[off + 6..off + 14].try_into().expect("impossible"));
221 off += 14;
222 if off + entry_len as usize > payload.len() {
223 return Err(io::Error::new(
224 io::ErrorKind::InvalidData,
225 "EntryBatch data truncated",
226 ));
227 }
228 let data = payload[off..off + entry_len as usize].to_vec();
229 off += entry_len as usize;
230 entries.push(WireEntry {
231 entry_len,
232 key_len,
233 gsn,
234 data,
235 });
236 }
237 Ok(Self { shard_id, entries })
238 }
239}
240
241pub struct CaughtUp {
244 pub shard_id: u8,
245 pub leader_gsn: u64,
246}
247
248impl CaughtUp {
249 pub fn encode(&self) -> Frame {
250 let mut payload = Vec::with_capacity(9);
251 payload.push(self.shard_id);
252 payload.extend_from_slice(&self.leader_gsn.to_le_bytes());
253 Frame {
254 msg_type: MessageType::CaughtUp,
255 payload,
256 }
257 }
258
259 pub fn decode(payload: &[u8]) -> io::Result<Self> {
260 if payload.len() < 9 {
261 return Err(io::Error::new(
262 io::ErrorKind::InvalidData,
263 "CaughtUp too short",
264 ));
265 }
266 Ok(Self {
267 shard_id: payload[0],
268 leader_gsn: u64::from_le_bytes(payload[1..9].try_into().expect("impossible")),
269 })
270 }
271}
272
273pub struct AckMessage {
276 pub shard_id: u8,
277 pub last_gsn: u64,
278}
279
280impl AckMessage {
281 pub fn encode(&self) -> Frame {
282 let mut payload = Vec::with_capacity(9);
283 payload.push(self.shard_id);
284 payload.extend_from_slice(&self.last_gsn.to_le_bytes());
285 Frame {
286 msg_type: MessageType::Ack,
287 payload,
288 }
289 }
290
291 pub fn decode(payload: &[u8]) -> io::Result<Self> {
292 if payload.len() < 9 {
293 return Err(io::Error::new(io::ErrorKind::InvalidData, "Ack too short"));
294 }
295 Ok(Self {
296 shard_id: payload[0],
297 last_gsn: u64::from_le_bytes(payload[1..9].try_into().expect("impossible")),
298 })
299 }
300}
301
302pub fn encode_heartbeat() -> Frame {
305 Frame {
306 msg_type: MessageType::Heartbeat,
307 payload: Vec::new(),
308 }
309}
310
311#[repr(u8)]
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum ReplicationErrorCode {
317 RetryableIo = 1,
319 ResourceLimit = 2,
321 CorruptedLog = 3,
323 ProtocolMismatch = 4,
325 InvalidRequest = 5,
327}
328
329impl ReplicationErrorCode {
330 fn from_u8(value: u8) -> Option<Self> {
331 match value {
332 1 => Some(Self::RetryableIo),
333 2 => Some(Self::ResourceLimit),
334 3 => Some(Self::CorruptedLog),
335 4 => Some(Self::ProtocolMismatch),
336 5 => Some(Self::InvalidRequest),
337 _ => None,
338 }
339 }
340}
341
342#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct ReplicationWireError {
345 pub code: ReplicationErrorCode,
347 pub message: String,
349}
350
351pub fn encode_error(code: ReplicationErrorCode, message: &str) -> Frame {
355 let max = message.len().min(u16::MAX as usize);
356 let end = message.floor_char_boundary(max);
357 let message = &message.as_bytes()[..end];
358 let mut payload = Vec::with_capacity(3 + message.len());
359 payload.push(code as u8);
360 payload.extend_from_slice(&(message.len() as u16).to_le_bytes());
361 payload.extend_from_slice(message);
362 Frame {
363 msg_type: MessageType::Error,
364 payload,
365 }
366}
367
368pub fn decode_error(payload: &[u8]) -> io::Result<ReplicationWireError> {
375 if payload.len() < 3 {
376 return Err(io::Error::new(
377 io::ErrorKind::InvalidData,
378 "replication error payload too short",
379 ));
380 }
381
382 let code = ReplicationErrorCode::from_u8(payload[0]).ok_or_else(|| {
383 io::Error::new(
384 io::ErrorKind::InvalidData,
385 format!("unknown replication error code: {}", payload[0]),
386 )
387 })?;
388 let message_len = u16::from_le_bytes([payload[1], payload[2]]) as usize;
389 let expected_len = 3 + message_len;
390 if payload.len() != expected_len {
391 return Err(io::Error::new(
392 io::ErrorKind::InvalidData,
393 "invalid replication error payload length",
394 ));
395 }
396 let message = std::str::from_utf8(&payload[3..])
397 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?
398 .to_owned();
399
400 Ok(ReplicationWireError { code, message })
401}
402
403#[cfg(test)]
404mod max_frame_size_tests {
405 use super::*;
406 use std::io::Cursor;
407
408 #[test]
409 fn read_frame_rejects_oversized_length() {
410 let mut bytes = Vec::new();
412 bytes.push(MessageType::SyncRequest as u8);
413 bytes.extend_from_slice(&((MAX_FRAME_SIZE + 1) as u32).to_le_bytes());
414 let mut cursor = Cursor::new(bytes);
415 let err = read_frame(&mut cursor).unwrap_err();
416 let msg = format!("{err}");
417 assert!(msg.contains("frame too large"), "got: {msg}");
418 }
419
420 #[test]
421 fn frame_reader_rejects_garbage_type_byte_immediately() {
422 let mut fr = crate::frame_reader::FrameReader::<MessageType>::new();
427 let err = fr.read_frame(&mut Cursor::new([0u8])).unwrap_err();
428 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
429 assert!(err.to_string().contains("unknown message type"));
430 }
431
432 #[test]
433 fn frame_alias_keeps_debug_contract() {
434 let frame = Frame {
437 msg_type: MessageType::Heartbeat,
438 payload: Vec::new(),
439 };
440 assert!(format!("{frame:?}").contains("Heartbeat"));
441 }
442}
443
444#[cfg(test)]
445mod sync_request_tests {
446 use super::*;
447
448 #[test]
449 fn sync_request_v2_round_trip() {
450 let req = SyncRequest {
451 protocol_version: VAR_PROTOCOL_VERSION,
452 shard_id: 7,
453 from_gsn: 0xDEAD_BEEF_CAFE_BABE,
454 key_len: 32,
455 };
456 let frame = req.encode();
457 assert_eq!(frame.payload.len(), 13);
458 assert_eq!(SyncRequest::decode(&frame.payload).unwrap(), req);
459 }
460
461 #[test]
462 fn shard_info_v2_round_trip() {
463 let info = ShardInfo {
464 protocol_version: VAR_PROTOCOL_VERSION,
465 shard_count: 4,
466 max_file_size: 64 * 1024 * 1024,
467 };
468 let frame = info.encode();
469 assert_eq!(frame.payload.len(), 11);
470 assert_eq!(ShardInfo::decode(&frame.payload).unwrap(), info);
471 }
472
473 #[test]
474 fn typed_error_round_trips_every_code() {
475 for code in [
476 ReplicationErrorCode::RetryableIo,
477 ReplicationErrorCode::ResourceLimit,
478 ReplicationErrorCode::CorruptedLog,
479 ReplicationErrorCode::ProtocolMismatch,
480 ReplicationErrorCode::InvalidRequest,
481 ] {
482 let frame = encode_error(code, "detail");
483 assert_eq!(
484 decode_error(&frame.payload).unwrap(),
485 ReplicationWireError {
486 code,
487 message: "detail".to_owned(),
488 }
489 );
490 }
491 }
492
493 #[test]
494 fn typed_error_rejects_trailing_and_invalid_utf8() {
495 let mut trailing = encode_error(ReplicationErrorCode::InvalidRequest, "x").payload;
496 trailing.push(0);
497 assert_eq!(
498 decode_error(&trailing).unwrap_err().kind(),
499 io::ErrorKind::InvalidData
500 );
501
502 let invalid = [ReplicationErrorCode::InvalidRequest as u8, 1, 0, 0xFF];
503 assert_eq!(
504 decode_error(&invalid).unwrap_err().kind(),
505 io::ErrorKind::InvalidData
506 );
507 }
508
509 #[test]
510 fn typed_error_truncates_on_utf8_boundary() {
511 let message = format!("{}€", "a".repeat(u16::MAX as usize - 1));
512 let decoded =
513 decode_error(&encode_error(ReplicationErrorCode::RetryableIo, &message).payload)
514 .unwrap();
515 assert!(decoded.message.is_char_boundary(decoded.message.len()));
516 assert_eq!(decoded.message.len(), u16::MAX as usize - 1);
517 }
518}