1use serde::{Deserialize, Serialize};
7use std::fmt;
8
9pub const MULTIPLEX_VERSION: u8 = 1;
11
12pub const MAX_DATA_PAYLOAD: usize = 1024 * 1024;
14
15pub const FRAME_HEADER_LEN: usize = 1 + 1 + 4 + 8 + 4;
17
18
19pub fn grow_read_buf(buf: &mut Vec<u8>, filled: usize) -> Result<(), FrameError> {
23 const CAP: usize = MAX_DATA_PAYLOAD + FRAME_HEADER_LEN;
24 if filled < buf.len() {
25 return Ok(());
26 }
27 if buf.len() >= CAP {
28 return Err(FrameError::PayloadTooLarge(filled));
29 }
30 let next = (buf.len().saturating_mul(2)).clamp(filled + 64 * 1024, CAP);
31 buf.resize(next, 0);
32 Ok(())
33}
34
35
36#[repr(u8)]
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum FrameType {
39 Open = 1,
40 Data = 2,
41 Close = 3,
42 Window = 4,
43 Ping = 5,
44 Pong = 6,
45 GoAway = 7,
46 Ack = 8,
47 Resume = 9,
48 JournalReq = 10,
50 JournalReply = 11,
52}
53
54impl FrameType {
55 pub fn from_u8(v: u8) -> Option<Self> {
56 Some(match v {
57 1 => Self::Open,
58 2 => Self::Data,
59 3 => Self::Close,
60 4 => Self::Window,
61 5 => Self::Ping,
62 6 => Self::Pong,
63 7 => Self::GoAway,
64 8 => Self::Ack,
65 9 => Self::Resume,
66 10 => Self::JournalReq,
67 11 => Self::JournalReply,
68 _ => return None,
69 })
70 }
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "snake_case")]
76pub enum ChannelKind {
77 G4aControl,
78 G4aPty,
79 G4aEvents,
80 M4a,
81 Vault,
82 Observation,
83}
84
85impl ChannelKind {
86 pub fn as_str(self) -> &'static str {
87 match self {
88 Self::G4aControl => "g4a.control",
89 Self::G4aPty => "g4a.pty",
90 Self::G4aEvents => "g4a.events",
91 Self::M4a => "m4a",
92 Self::Vault => "vault",
93 Self::Observation => "observation",
94 }
95 }
96
97 pub fn parse(s: &str) -> Option<Self> {
98 Some(match s {
99 "g4a.control" => Self::G4aControl,
100 "g4a.pty" => Self::G4aPty,
101 "g4a.events" => Self::G4aEvents,
102 "m4a" => Self::M4a,
103 "vault" => Self::Vault,
104 "observation" => Self::Observation,
105 _ => return None,
106 })
107 }
108}
109
110impl fmt::Display for ChannelKind {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(self.as_str())
113 }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
118pub struct OpenPayload {
119 pub channel_kind: ChannelKind,
120 pub node_id: String,
122}
123
124#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct Frame {
127 pub version: u8,
128 pub frame_type: FrameType,
129 pub channel_id: u32,
130 pub seq: u64,
132 pub payload: Vec<u8>,
133}
134
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub enum FrameError {
137 Truncated,
138 BadVersion(u8),
139 BadType(u8),
140 PayloadTooLarge(usize),
141 Io(String),
142}
143
144impl fmt::Display for FrameError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 Self::Truncated => write!(f, "truncated frame"),
148 Self::BadVersion(v) => write!(f, "unsupported multiplex version {v}"),
149 Self::BadType(t) => write!(f, "unknown frame type {t}"),
150 Self::PayloadTooLarge(n) => write!(f, "payload length {n} exceeds limit"),
151 Self::Io(msg) => write!(f, "{msg}"),
152 }
153 }
154}
155
156impl std::error::Error for FrameError {}
157
158impl Frame {
159 pub fn open(channel_id: u32, payload: &OpenPayload) -> Result<Self, FrameError> {
160 let body = serde_json::to_vec(payload).map_err(|e| FrameError::Io(e.to_string()))?;
161 Ok(Self {
162 version: MULTIPLEX_VERSION,
163 frame_type: FrameType::Open,
164 channel_id,
165 seq: 0,
166 payload: body,
167 })
168 }
169
170 pub fn data(channel_id: u32, seq: u64, payload: Vec<u8>) -> Result<Self, FrameError> {
171 if payload.len() > MAX_DATA_PAYLOAD {
172 return Err(FrameError::PayloadTooLarge(payload.len()));
173 }
174 Ok(Self {
175 version: MULTIPLEX_VERSION,
176 frame_type: FrameType::Data,
177 channel_id,
178 seq,
179 payload,
180 })
181 }
182
183 pub fn ack(seq: u64) -> Self {
184 Self {
185 version: MULTIPLEX_VERSION,
186 frame_type: FrameType::Ack,
187 channel_id: 0,
188 seq,
189 payload: Vec::new(),
190 }
191 }
192
193 pub fn resume(after: u64) -> Self {
194 Self {
195 version: MULTIPLEX_VERSION,
196 frame_type: FrameType::Resume,
197 channel_id: 0,
198 seq: after,
199 payload: Vec::new(),
200 }
201 }
202
203 pub fn window(channel_id: u32, credit: u32) -> Self {
204 Self {
205 version: MULTIPLEX_VERSION,
206 frame_type: FrameType::Window,
207 channel_id,
208 seq: u64::from(credit),
209 payload: Vec::new(),
210 }
211 }
212
213 pub fn ping(nonce: u64) -> Self {
214 Self {
215 version: MULTIPLEX_VERSION,
216 frame_type: FrameType::Ping,
217 channel_id: 0,
218 seq: nonce,
219 payload: Vec::new(),
220 }
221 }
222
223 pub fn pong(nonce: u64) -> Self {
224 Self {
225 version: MULTIPLEX_VERSION,
226 frame_type: FrameType::Pong,
227 channel_id: 0,
228 seq: nonce,
229 payload: Vec::new(),
230 }
231 }
232
233 pub fn goaway(code: u32) -> Self {
234 Self {
235 version: MULTIPLEX_VERSION,
236 frame_type: FrameType::GoAway,
237 channel_id: 0,
238 seq: u64::from(code),
239 payload: Vec::new(),
240 }
241 }
242
243 pub fn close(channel_id: u32) -> Self {
244 Self {
245 version: MULTIPLEX_VERSION,
246 frame_type: FrameType::Close,
247 channel_id,
248 seq: 0,
249 payload: Vec::new(),
250 }
251 }
252
253 pub fn encode(&self) -> Result<Vec<u8>, FrameError> {
255 if self.payload.len() > MAX_DATA_PAYLOAD {
256 return Err(FrameError::PayloadTooLarge(self.payload.len()));
257 }
258 let mut out = Vec::with_capacity(FRAME_HEADER_LEN + self.payload.len());
259 out.push(self.version);
260 out.push(self.frame_type as u8);
261 out.extend_from_slice(&self.channel_id.to_be_bytes());
262 out.extend_from_slice(&self.seq.to_be_bytes());
263 out.extend_from_slice(&(self.payload.len() as u32).to_be_bytes());
264 out.extend_from_slice(&self.payload);
265 Ok(out)
266 }
267
268 pub fn decode(buf: &[u8]) -> Result<(Self, usize), FrameError> {
270 if buf.len() < FRAME_HEADER_LEN {
271 return Err(FrameError::Truncated);
272 }
273 let version = buf[0];
274 if version != MULTIPLEX_VERSION {
275 return Err(FrameError::BadVersion(version));
276 }
277 let frame_type = FrameType::from_u8(buf[1]).ok_or(FrameError::BadType(buf[1]))?;
278 let channel_id = u32::from_be_bytes([buf[2], buf[3], buf[4], buf[5]]);
279 let seq = u64::from_be_bytes([
280 buf[6], buf[7], buf[8], buf[9], buf[10], buf[11], buf[12], buf[13],
281 ]);
282 let length = u32::from_be_bytes([buf[14], buf[15], buf[16], buf[17]]) as usize;
283 if length > MAX_DATA_PAYLOAD {
284 return Err(FrameError::PayloadTooLarge(length));
285 }
286 let total = FRAME_HEADER_LEN + length;
287 if buf.len() < total {
288 return Err(FrameError::Truncated);
289 }
290 Ok((
291 Self {
292 version,
293 frame_type,
294 channel_id,
295 seq,
296 payload: buf[FRAME_HEADER_LEN..total].to_vec(),
297 },
298 total,
299 ))
300 }
301
302 pub fn parse_open(&self) -> Result<OpenPayload, FrameError> {
303 if self.frame_type != FrameType::Open {
304 return Err(FrameError::Io("not an OPEN frame".into()));
305 }
306 serde_json::from_slice(&self.payload).map_err(|e| FrameError::Io(e.to_string()))
307 }
308
309 pub fn journal_req(channel_id: u32, after: u64, limit: u32) -> Self {
311 Self {
312 version: MULTIPLEX_VERSION,
313 frame_type: FrameType::JournalReq,
314 channel_id,
315 seq: after,
316 payload: limit.to_be_bytes().to_vec(),
317 }
318 }
319
320 pub fn journal_req_limit(&self) -> Option<u32> {
321 if self.frame_type != FrameType::JournalReq || self.payload.len() != 4 {
322 return None;
323 }
324 Some(u32::from_be_bytes([
325 self.payload[0],
326 self.payload[1],
327 self.payload[2],
328 self.payload[3],
329 ]))
330 }
331
332 pub fn journal_reply(channel_id: u32, body: &JournalReplyBody) -> Result<Self, FrameError> {
333 let payload = body.encode()?;
334 if payload.len() > MAX_DATA_PAYLOAD {
335 return Err(FrameError::PayloadTooLarge(payload.len()));
336 }
337 Ok(Self {
338 version: MULTIPLEX_VERSION,
339 frame_type: FrameType::JournalReply,
340 channel_id,
341 seq: body.after,
342 payload,
343 })
344 }
345}
346
347#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
349pub struct JournalWireEntry {
350 pub seq: u64,
351 pub payload: Vec<u8>,
352}
353
354#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
356pub struct JournalReplyBody {
357 pub after: u64,
358 pub entries: Vec<JournalWireEntry>,
359}
360
361impl JournalReplyBody {
362 pub fn encode(&self) -> Result<Vec<u8>, FrameError> {
363 serde_json::to_vec(self).map_err(|e| FrameError::Io(e.to_string()))
364 }
365
366 pub fn decode(bytes: &[u8]) -> Result<Self, FrameError> {
367 serde_json::from_slice(bytes).map_err(|e| FrameError::Io(e.to_string()))
368 }
369}
370
371#[derive(Clone, Debug, PartialEq, Eq)]
373pub struct DrainEnvelope {
374 pub node_id: String,
376 pub frame: Frame,
377}
378
379impl DrainEnvelope {
380 pub fn encode(&self) -> Result<Vec<u8>, FrameError> {
381 let id = self.node_id.as_bytes();
382 if id.len() > 255 {
383 return Err(FrameError::Io("node_id too long".into()));
384 }
385 let frame_bytes = self.frame.encode()?;
386 let mut out = Vec::with_capacity(1 + id.len() + frame_bytes.len());
387 out.push(id.len() as u8);
388 out.extend_from_slice(id);
389 out.extend_from_slice(&frame_bytes);
390 Ok(out)
391 }
392
393 pub fn decode(buf: &[u8]) -> Result<(Self, usize), FrameError> {
394 if buf.is_empty() {
395 return Err(FrameError::Truncated);
396 }
397 let id_len = buf[0] as usize;
398 if buf.len() < 1 + id_len {
399 return Err(FrameError::Truncated);
400 }
401 let node_id = String::from_utf8(buf[1..1 + id_len].to_vec())
402 .map_err(|_| FrameError::Io("node_id is not utf-8".into()))?;
403 let (frame, n) = Frame::decode(&buf[1 + id_len..])?;
404 Ok((Self { node_id, frame }, 1 + id_len + n))
405 }
406}
407
408
409pub const UDP_DATA_MAGIC: &[u8; 4] = b"H4UD";
411
412pub fn encode_udp_data(node_id: &str, frame: &Frame) -> Result<Vec<u8>, FrameError> {
415 if !matches!(frame.frame_type, FrameType::Data) {
416 return Err(FrameError::Io("udp data plane accepts DATA frames only".into()));
417 }
418 let nid = node_id.as_bytes();
419 if nid.is_empty() || nid.len() > 255 {
420 return Err(FrameError::Io("udp node_id length".into()));
421 }
422 let body = frame.encode()?;
423 let mut out = Vec::with_capacity(4 + 1 + nid.len() + body.len());
424 out.extend_from_slice(UDP_DATA_MAGIC);
425 out.push(nid.len() as u8);
426 out.extend_from_slice(nid);
427 out.extend_from_slice(&body);
428 Ok(out)
429}
430
431pub fn decode_udp_data(buf: &[u8]) -> Result<(String, Frame), FrameError> {
433 if buf.len() < 5 + FRAME_HEADER_LEN {
434 return Err(FrameError::Truncated);
435 }
436 if &buf[..4] != UDP_DATA_MAGIC {
437 return Err(FrameError::Io("udp magic".into()));
438 }
439 let nlen = buf[4] as usize;
440 if nlen == 0 || buf.len() < 5 + nlen + FRAME_HEADER_LEN {
441 return Err(FrameError::Truncated);
442 }
443 let node_id = std::str::from_utf8(&buf[5..5 + nlen])
444 .map_err(|_| FrameError::Io("udp node_id utf8".into()))?
445 .to_owned();
446 let (frame, consumed) = Frame::decode(&buf[5 + nlen..])?;
447 if 5 + nlen + consumed != buf.len() {
448 return Err(FrameError::Io("udp trailing bytes".into()));
449 }
450 if !matches!(frame.frame_type, FrameType::Data) {
451 return Err(FrameError::Io("udp data plane accepts DATA frames only".into()));
452 }
453 Ok((node_id, frame))
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn roundtrip_data_ack_resume() {
462 let data = Frame::data(7, 42, b"opaque-ciphertext".to_vec()).unwrap();
463 let bytes = data.encode().unwrap();
464 let (decoded, n) = Frame::decode(&bytes).unwrap();
465 assert_eq!(n, bytes.len());
466 assert_eq!(decoded, data);
467
468 let ack = Frame::ack(42);
469 let (d, _) = Frame::decode(&ack.encode().unwrap()).unwrap();
470 assert_eq!(d.frame_type, FrameType::Ack);
471 assert_eq!(d.seq, 42);
472
473 let resume = Frame::resume(41);
474 let (d, _) = Frame::decode(&resume.encode().unwrap()).unwrap();
475 assert_eq!(d.frame_type, FrameType::Resume);
476 assert_eq!(d.seq, 41);
477 }
478
479 #[test]
480 fn open_payload_roundtrip() {
481 let open = Frame::open(
482 1,
483 &OpenPayload {
484 channel_kind: ChannelKind::M4a,
485 node_id: "node-a".into(),
486 },
487 )
488 .unwrap();
489 let (decoded, _) = Frame::decode(&open.encode().unwrap()).unwrap();
490 let payload = decoded.parse_open().unwrap();
491 assert_eq!(payload.channel_kind, ChannelKind::M4a);
492 assert_eq!(payload.node_id, "node-a");
493 }
494
495
496 #[test]
497 fn grow_read_buf_accepts_frames_between_64kib_and_1mib() {
498 let payload = vec![0xABu8; 100 * 1024];
500 let frame = Frame::data(1, 7, payload.clone()).unwrap();
501 let wire = frame.encode().unwrap();
502 assert!(wire.len() > 64 * 1024);
503
504 let mut buf = vec![0u8; 64 * 1024];
505 let mut filled = 0usize;
506 let mut offset = 0usize;
507 loop {
508 if filled == buf.len() {
509 grow_read_buf(&mut buf, filled).expect("must grow for 100 KiB frame");
510 }
511 let space = buf.len() - filled;
512 let take = space.min(wire.len() - offset);
513 buf[filled..filled + take].copy_from_slice(&wire[offset..offset + take]);
514 filled += take;
515 offset += take;
516 match Frame::decode(&buf[..filled]) {
517 Ok((decoded, consumed)) => {
518 assert_eq!(decoded.payload, payload);
519 assert_eq!(consumed, wire.len());
520 break;
521 }
522 Err(FrameError::Truncated) => {
523 assert!(offset < wire.len(), "still truncated after full wire");
524 continue;
525 }
526 Err(e) => panic!("unexpected {e}"),
527 }
528 }
529 }
530
531 #[test]
532 fn rejects_oversized_payload() {
533 let big = vec![0u8; MAX_DATA_PAYLOAD + 1];
534 assert!(matches!(
535 Frame::data(1, 1, big),
536 Err(FrameError::PayloadTooLarge(_))
537 ));
538 }
539
540 #[test]
541 fn drain_envelope_prefixes_opaque_node_id() {
542 let env = DrainEnvelope {
543 node_id: "node-b".into(),
544 frame: Frame::data(3, 9, b"inner".to_vec()).unwrap(),
545 };
546 let bytes = env.encode().unwrap();
547 let (decoded, n) = DrainEnvelope::decode(&bytes).unwrap();
548 assert_eq!(n, bytes.len());
549 assert_eq!(decoded.node_id, "node-b");
550 assert_eq!(decoded.frame.seq, 9);
551 assert_eq!(decoded.frame.payload, b"inner");
552 }
553
554 #[test]
555 fn journal_req_roundtrip() {
556 let f = Frame::journal_req(3, 10, 64);
557 let (d, _) = Frame::decode(&f.encode().unwrap()).unwrap();
558 assert_eq!(d.frame_type, FrameType::JournalReq);
559 assert_eq!(d.seq, 10);
560 assert_eq!(d.journal_req_limit(), Some(64));
561 let body = JournalReplyBody {
562 after: 10,
563 entries: vec![JournalWireEntry {
564 seq: 11,
565 payload: b"x".to_vec(),
566 }],
567 };
568 let reply = Frame::journal_reply(3, &body).unwrap();
569 let (d, _) = Frame::decode(&reply.encode().unwrap()).unwrap();
570 let decoded = JournalReplyBody::decode(&d.payload).unwrap();
571 assert_eq!(decoded.entries[0].seq, 11);
572 }
573
574 #[test]
575 fn truncated_frame_errors() {
576 assert!(matches!(Frame::decode(&[1, 2, 3]), Err(FrameError::Truncated)));
577 let mut partial = Frame::data(1, 1, b"hello".to_vec()).unwrap().encode().unwrap();
578 partial.pop();
579 assert!(matches!(Frame::decode(&partial), Err(FrameError::Truncated)));
580 }
581
582 #[test]
583 fn udp_data_roundtrip() {
584 let frame = Frame::data(3, 0, vec![1, 2, 3, 4]).unwrap();
585 let dg = encode_udp_data("hatchery-box", &frame).unwrap();
586 let (nid, f) = decode_udp_data(&dg).unwrap();
587 assert_eq!(nid, "hatchery-box");
588 assert_eq!(f.channel_id, 3);
589 assert_eq!(f.payload, vec![1, 2, 3, 4]);
590 }
591}