1use std::io::{self, Read, Write};
14
15const MAX_FRAME_SIZE: u32 = 64 * 1024 * 1024;
17
18const MSG_AUTHENTICATE: u8 = 0x01;
20const MSG_CREATE_SESSION: u8 = 0x02;
21const MSG_DESTROY_SESSION: u8 = 0x03;
22const MSG_INJECT_GLOBALS: u8 = 0x04;
23const MSG_EXECUTE: u8 = 0x05;
24const MSG_BRIDGE_RESPONSE: u8 = 0x06;
25const MSG_STREAM_EVENT: u8 = 0x07;
26const MSG_TERMINATE_EXECUTION: u8 = 0x08;
27const MSG_WARM_SNAPSHOT: u8 = 0x09;
28
29const MSG_BRIDGE_CALL: u8 = 0x81;
31const MSG_EXECUTION_RESULT: u8 = 0x82;
32const MSG_LOG: u8 = 0x83;
33const MSG_STREAM_CALLBACK: u8 = 0x84;
34
35const FLAG_HAS_EXPORTS: u8 = 0x01;
37const FLAG_HAS_ERROR: u8 = 0x02;
38const FLAG_LIMIT_EXCEEDED: u8 = 0x04;
39
40#[derive(Debug, Clone, PartialEq)]
42pub enum BinaryFrame {
43 Authenticate {
45 token: String,
46 },
47 CreateSession {
48 session_id: String,
49 heap_limit_mb: u32,
50 cpu_time_limit_ms: u32,
51 wall_clock_limit_ms: u32,
52 },
53 DestroySession {
54 session_id: String,
55 },
56 InjectGlobals {
57 session_id: String,
58 payload: Vec<u8>, },
60 Execute {
61 session_id: String,
62 mode: u8, file_path: String,
64 bridge_code: String,
65 post_restore_script: String,
66 userland_code: String,
70 high_resolution_time: bool,
71 user_code: String,
72 },
73 BridgeResponse {
74 session_id: String,
75 call_id: u64,
76 status: u8, payload: Vec<u8>, },
79 StreamEvent {
80 session_id: String,
81 event_type: String,
82 payload: Vec<u8>, },
84 TerminateExecution {
85 session_id: String,
86 },
87 WarmSnapshot {
88 bridge_code: String,
89 userland_code: String,
91 },
92
93 BridgeCall {
95 session_id: String,
96 call_id: u64,
97 method: String,
98 payload: Vec<u8>, },
100 ExecutionResult {
101 session_id: String,
102 exit_code: i32,
103 exports: Option<Vec<u8>>,
104 error: Option<ExecutionErrorBin>,
105 limit_exceeded: Option<ExecutionLimit>,
106 },
107 Log {
108 session_id: String,
109 channel: u8, message: String,
111 },
112 StreamCallback {
113 session_id: String,
114 callback_type: String,
115 payload: Vec<u8>, },
117}
118
119#[derive(Debug, Clone, PartialEq)]
121pub struct ExecutionErrorBin {
122 pub error_type: String,
123 pub message: String,
124 pub stack: String,
125 pub code: String, }
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum ExecutionLimit {
132 V8Heap,
133 CpuTime,
134 WallClock,
135}
136
137impl ExecutionLimit {
138 pub fn to_wire(self) -> u8 {
139 match self {
140 Self::V8Heap => 1,
141 Self::CpuTime => 2,
142 Self::WallClock => 3,
143 }
144 }
145
146 pub fn from_wire(value: u8) -> io::Result<Self> {
147 match value {
148 1 => Ok(Self::V8Heap),
149 2 => Ok(Self::CpuTime),
150 3 => Ok(Self::WallClock),
151 other => Err(io::Error::new(
152 io::ErrorKind::InvalidData,
153 format!("unknown ExecutionResult limit: {other}"),
154 )),
155 }
156 }
157}
158
159pub fn encode_frame_into(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
163 buf.clear();
164 buf.extend_from_slice(&[0, 0, 0, 0]);
166 encode_body(buf, frame)?;
167
168 let total_len = buf.len() - 4;
169 if total_len > MAX_FRAME_SIZE as usize {
170 return Err(io::Error::new(
171 io::ErrorKind::InvalidData,
172 format!("frame size {total_len} exceeds maximum {MAX_FRAME_SIZE}"),
173 ));
174 }
175 buf[..4].copy_from_slice(&(total_len as u32).to_be_bytes());
176 Ok(())
177}
178
179pub fn frame_to_bytes(frame: &BinaryFrame) -> io::Result<Vec<u8>> {
182 let mut buf = Vec::new();
183 encode_frame_into(&mut buf, frame)?;
184 Ok(buf)
185}
186
187pub fn write_frame<W: Write>(writer: &mut W, frame: &BinaryFrame) -> io::Result<()> {
189 let bytes = frame_to_bytes(frame)?;
190 writer.write_all(&bytes)?;
191 Ok(())
192}
193
194pub fn read_frame<R: Read>(reader: &mut R) -> io::Result<BinaryFrame> {
196 let mut len_buf = [0u8; 4];
197 reader.read_exact(&mut len_buf)?;
198 let total_len = u32::from_be_bytes(len_buf);
199
200 if total_len > MAX_FRAME_SIZE {
201 return Err(io::Error::new(
202 io::ErrorKind::InvalidData,
203 format!("frame size {total_len} exceeds maximum {MAX_FRAME_SIZE}"),
204 ));
205 }
206
207 let mut buf = vec![0u8; total_len as usize];
208 reader.read_exact(&mut buf)?;
209 decode_body(&buf)
210}
211
212#[allow(dead_code)]
216pub fn extract_session_id(raw: &[u8]) -> io::Result<Option<&str>> {
217 if raw.len() < 2 {
218 return Err(io::Error::new(
219 io::ErrorKind::InvalidData,
220 "frame too short",
221 ));
222 }
223 let msg_type = raw[0];
224 if msg_type == MSG_AUTHENTICATE || msg_type == MSG_WARM_SNAPSHOT {
225 return Ok(None);
226 }
227 let sid_len = raw[1] as usize;
228 if raw.len() < 2 + sid_len {
229 return Err(io::Error::new(
230 io::ErrorKind::InvalidData,
231 "frame too short for session_id",
232 ));
233 }
234 let sid = std::str::from_utf8(&raw[2..2 + sid_len])
235 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
236 Ok(Some(sid))
237}
238
239fn encode_body(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
242 match frame {
243 BinaryFrame::Authenticate { token } => {
244 buf.push(MSG_AUTHENTICATE);
245 buf.push(0);
247 buf.extend_from_slice(token.as_bytes());
248 }
249 BinaryFrame::CreateSession {
250 session_id,
251 heap_limit_mb,
252 cpu_time_limit_ms,
253 wall_clock_limit_ms,
254 } => {
255 buf.push(MSG_CREATE_SESSION);
256 write_session_id(buf, session_id)?;
257 buf.extend_from_slice(&heap_limit_mb.to_be_bytes());
258 buf.extend_from_slice(&cpu_time_limit_ms.to_be_bytes());
259 buf.extend_from_slice(&wall_clock_limit_ms.to_be_bytes());
260 }
261 BinaryFrame::DestroySession { session_id } => {
262 buf.push(MSG_DESTROY_SESSION);
263 write_session_id(buf, session_id)?;
264 }
265 BinaryFrame::InjectGlobals {
266 session_id,
267 payload,
268 } => {
269 buf.push(MSG_INJECT_GLOBALS);
270 write_session_id(buf, session_id)?;
271 buf.extend_from_slice(payload);
272 }
273 BinaryFrame::Execute {
274 session_id,
275 mode,
276 file_path,
277 bridge_code,
278 post_restore_script,
279 userland_code,
280 high_resolution_time,
281 user_code,
282 } => {
283 buf.push(MSG_EXECUTE);
284 write_session_id(buf, session_id)?;
285 buf.push(*mode);
286 write_len_prefixed_u16(buf, file_path)?;
288 let bc_bytes = bridge_code.as_bytes();
290 buf.extend_from_slice(&(bc_bytes.len() as u32).to_be_bytes());
291 buf.extend_from_slice(bc_bytes);
292 let prs_bytes = post_restore_script.as_bytes();
294 buf.extend_from_slice(&(prs_bytes.len() as u32).to_be_bytes());
295 buf.extend_from_slice(prs_bytes);
296 let ul_bytes = userland_code.as_bytes();
298 buf.extend_from_slice(&(ul_bytes.len() as u32).to_be_bytes());
299 buf.extend_from_slice(ul_bytes);
300 buf.push(u8::from(*high_resolution_time));
301 buf.extend_from_slice(user_code.as_bytes());
303 }
304 BinaryFrame::BridgeResponse {
305 session_id,
306 call_id,
307 status,
308 payload,
309 } => {
310 buf.push(MSG_BRIDGE_RESPONSE);
311 write_session_id(buf, session_id)?;
312 buf.extend_from_slice(&call_id.to_be_bytes());
313 buf.push(*status);
314 buf.extend_from_slice(payload);
315 }
316 BinaryFrame::StreamEvent {
317 session_id,
318 event_type,
319 payload,
320 } => {
321 buf.push(MSG_STREAM_EVENT);
322 write_session_id(buf, session_id)?;
323 write_len_prefixed_u16(buf, event_type)?;
324 buf.extend_from_slice(payload);
325 }
326 BinaryFrame::TerminateExecution { session_id } => {
327 buf.push(MSG_TERMINATE_EXECUTION);
328 write_session_id(buf, session_id)?;
329 }
330 BinaryFrame::WarmSnapshot {
331 bridge_code,
332 userland_code,
333 } => {
334 buf.push(MSG_WARM_SNAPSHOT);
335 buf.push(0); let bc_bytes = bridge_code.as_bytes();
337 buf.extend_from_slice(&(bc_bytes.len() as u32).to_be_bytes());
338 buf.extend_from_slice(bc_bytes);
339 let ul_bytes = userland_code.as_bytes();
341 buf.extend_from_slice(&(ul_bytes.len() as u32).to_be_bytes());
342 buf.extend_from_slice(ul_bytes);
343 }
344 BinaryFrame::BridgeCall {
345 session_id,
346 call_id,
347 method,
348 payload,
349 } => {
350 buf.push(MSG_BRIDGE_CALL);
351 write_session_id(buf, session_id)?;
352 buf.extend_from_slice(&call_id.to_be_bytes());
353 write_len_prefixed_u16(buf, method)?;
354 buf.extend_from_slice(payload);
355 }
356 BinaryFrame::ExecutionResult {
357 session_id,
358 exit_code,
359 exports,
360 error,
361 limit_exceeded,
362 } => {
363 buf.push(MSG_EXECUTION_RESULT);
364 write_session_id(buf, session_id)?;
365 buf.extend_from_slice(&exit_code.to_be_bytes());
366 let mut flags: u8 = 0;
367 if exports.is_some() {
368 flags |= FLAG_HAS_EXPORTS;
369 }
370 if error.is_some() {
371 flags |= FLAG_HAS_ERROR;
372 }
373 if limit_exceeded.is_some() {
374 flags |= FLAG_LIMIT_EXCEEDED;
375 }
376 buf.push(flags);
377 if let Some(exp) = exports {
378 buf.extend_from_slice(&(exp.len() as u32).to_be_bytes());
379 buf.extend_from_slice(exp);
380 }
381 if let Some(err) = error {
382 write_len_prefixed_u16(buf, &err.error_type)?;
383 write_len_prefixed_u16(buf, &err.message)?;
384 write_len_prefixed_u16(buf, &err.stack)?;
385 write_len_prefixed_u16(buf, &err.code)?;
386 }
387 if let Some(limit) = limit_exceeded {
388 buf.push(limit.to_wire());
389 }
390 }
391 BinaryFrame::Log {
392 session_id,
393 channel,
394 message,
395 } => {
396 buf.push(MSG_LOG);
397 write_session_id(buf, session_id)?;
398 buf.push(*channel);
399 buf.extend_from_slice(message.as_bytes());
400 }
401 BinaryFrame::StreamCallback {
402 session_id,
403 callback_type,
404 payload,
405 } => {
406 buf.push(MSG_STREAM_CALLBACK);
407 write_session_id(buf, session_id)?;
408 write_len_prefixed_u16(buf, callback_type)?;
409 buf.extend_from_slice(payload);
410 }
411 }
412 Ok(())
413}
414
415fn decode_body(buf: &[u8]) -> io::Result<BinaryFrame> {
416 if buf.is_empty() {
417 return Err(io::Error::new(io::ErrorKind::InvalidData, "empty frame"));
418 }
419
420 let msg_type = buf[0];
421 let mut pos = 1;
422
423 let sid_len = read_u8(buf, &mut pos)? as usize;
426 let session_id = read_utf8(buf, &mut pos, sid_len)?;
427
428 match msg_type {
429 MSG_AUTHENTICATE => {
430 ensure_no_session_id(&session_id, "Authenticate")?;
431 let remaining = buf.len() - pos;
433 let token = read_utf8(buf, &mut pos, remaining)?;
434 Ok(BinaryFrame::Authenticate { token })
435 }
436 MSG_CREATE_SESSION => {
437 let heap_limit_mb = read_u32(buf, &mut pos)?;
438 let cpu_time_limit_ms = read_u32(buf, &mut pos)?;
439 let wall_clock_limit_ms = read_u32(buf, &mut pos)?;
440 ensure_frame_consumed(buf, pos)?;
441 Ok(BinaryFrame::CreateSession {
442 session_id,
443 heap_limit_mb,
444 cpu_time_limit_ms,
445 wall_clock_limit_ms,
446 })
447 }
448 MSG_DESTROY_SESSION => {
449 ensure_frame_consumed(buf, pos)?;
450 Ok(BinaryFrame::DestroySession { session_id })
451 }
452 MSG_INJECT_GLOBALS => {
453 let payload = buf[pos..].to_vec();
454 Ok(BinaryFrame::InjectGlobals {
455 session_id,
456 payload,
457 })
458 }
459 MSG_EXECUTE => {
460 let mode = read_u8(buf, &mut pos)?;
461 let fp_len = read_u16(buf, &mut pos)? as usize;
462 let file_path = read_utf8(buf, &mut pos, fp_len)?;
463 let bc_len = read_u32(buf, &mut pos)? as usize;
464 let bridge_code = read_utf8(buf, &mut pos, bc_len)?;
465 let prs_len = read_u32(buf, &mut pos)? as usize;
466 let post_restore_script = read_utf8(buf, &mut pos, prs_len)?;
467 let ul_len = read_u32(buf, &mut pos)? as usize;
468 let userland_code = read_utf8(buf, &mut pos, ul_len)?;
469 let high_resolution_time = read_u8(buf, &mut pos)? != 0;
470 let remaining = buf.len() - pos;
471 let user_code = read_utf8(buf, &mut pos, remaining)?;
472 Ok(BinaryFrame::Execute {
473 session_id,
474 mode,
475 file_path,
476 bridge_code,
477 post_restore_script,
478 userland_code,
479 high_resolution_time,
480 user_code,
481 })
482 }
483 MSG_BRIDGE_RESPONSE => {
484 let call_id = read_u64(buf, &mut pos)?;
485 let status = read_u8(buf, &mut pos)?;
486 let payload = buf[pos..].to_vec();
487 Ok(BinaryFrame::BridgeResponse {
488 session_id,
489 call_id,
490 status,
491 payload,
492 })
493 }
494 MSG_STREAM_EVENT => {
495 let et_len = read_u16(buf, &mut pos)? as usize;
496 let event_type = read_utf8(buf, &mut pos, et_len)?;
497 let payload = buf[pos..].to_vec();
498 Ok(BinaryFrame::StreamEvent {
499 session_id,
500 event_type,
501 payload,
502 })
503 }
504 MSG_TERMINATE_EXECUTION => {
505 ensure_frame_consumed(buf, pos)?;
506 Ok(BinaryFrame::TerminateExecution { session_id })
507 }
508 MSG_WARM_SNAPSHOT => {
509 ensure_no_session_id(&session_id, "WarmSnapshot")?;
510 let bc_len = read_u32(buf, &mut pos)? as usize;
511 let bridge_code = read_utf8(buf, &mut pos, bc_len)?;
512 let ul_len = read_u32(buf, &mut pos)? as usize;
513 let userland_code = read_utf8(buf, &mut pos, ul_len)?;
514 ensure_frame_consumed(buf, pos)?;
515 Ok(BinaryFrame::WarmSnapshot {
516 bridge_code,
517 userland_code,
518 })
519 }
520 MSG_BRIDGE_CALL => {
521 let call_id = read_u64(buf, &mut pos)?;
522 let m_len = read_u16(buf, &mut pos)? as usize;
523 let method = read_utf8(buf, &mut pos, m_len)?;
524 let payload = buf[pos..].to_vec();
525 Ok(BinaryFrame::BridgeCall {
526 session_id,
527 call_id,
528 method,
529 payload,
530 })
531 }
532 MSG_EXECUTION_RESULT => {
533 let exit_code = read_i32(buf, &mut pos)?;
534 let flags = read_u8(buf, &mut pos)?;
535 if flags & !(FLAG_HAS_EXPORTS | FLAG_HAS_ERROR | FLAG_LIMIT_EXCEEDED) != 0 {
536 return Err(io::Error::new(
537 io::ErrorKind::InvalidData,
538 format!("unknown ExecutionResult flags: 0x{flags:02x}"),
539 ));
540 }
541 let exports = if flags & FLAG_HAS_EXPORTS != 0 {
542 let exp_len = read_u32(buf, &mut pos)? as usize;
543 let data = read_bytes(buf, &mut pos, exp_len)?;
544 Some(data)
545 } else {
546 None
547 };
548 let error = if flags & FLAG_HAS_ERROR != 0 {
549 let error_type = read_len_prefixed_u16(buf, &mut pos)?;
550 let message = read_len_prefixed_u16(buf, &mut pos)?;
551 let stack = read_len_prefixed_u16(buf, &mut pos)?;
552 let code = read_len_prefixed_u16(buf, &mut pos)?;
553 Some(ExecutionErrorBin {
554 error_type,
555 message,
556 stack,
557 code,
558 })
559 } else {
560 None
561 };
562 let limit_exceeded = if flags & FLAG_LIMIT_EXCEEDED != 0 {
563 Some(ExecutionLimit::from_wire(read_u8(buf, &mut pos)?)?)
564 } else {
565 None
566 };
567 ensure_frame_consumed(buf, pos)?;
568 Ok(BinaryFrame::ExecutionResult {
569 session_id,
570 exit_code,
571 exports,
572 error,
573 limit_exceeded,
574 })
575 }
576 MSG_LOG => {
577 let channel = read_u8(buf, &mut pos)?;
578 let remaining = buf.len() - pos;
579 let message = read_utf8(buf, &mut pos, remaining)?;
580 Ok(BinaryFrame::Log {
581 session_id,
582 channel,
583 message,
584 })
585 }
586 MSG_STREAM_CALLBACK => {
587 let ct_len = read_u16(buf, &mut pos)? as usize;
588 let callback_type = read_utf8(buf, &mut pos, ct_len)?;
589 let payload = buf[pos..].to_vec();
590 Ok(BinaryFrame::StreamCallback {
591 session_id,
592 callback_type,
593 payload,
594 })
595 }
596 _ => Err(io::Error::new(
597 io::ErrorKind::InvalidData,
598 format!("unknown message type: 0x{msg_type:02x}"),
599 )),
600 }
601}
602
603fn write_session_id(buf: &mut Vec<u8>, sid: &str) -> io::Result<()> {
606 let bytes = sid.as_bytes();
607 if bytes.len() > 255 {
608 return Err(io::Error::new(
609 io::ErrorKind::InvalidInput,
610 format!("session ID byte length {} exceeds u8 max 255", bytes.len()),
611 ));
612 }
613 buf.push(bytes.len() as u8);
614 buf.extend_from_slice(bytes);
615 Ok(())
616}
617
618fn write_len_prefixed_u16(buf: &mut Vec<u8>, s: &str) -> io::Result<()> {
619 let bytes = s.as_bytes();
620 if bytes.len() > 0xFFFF {
621 return Err(io::Error::new(
622 io::ErrorKind::InvalidInput,
623 format!("string byte length {} exceeds u16 max 65535", bytes.len()),
624 ));
625 }
626 buf.extend_from_slice(&(bytes.len() as u16).to_be_bytes());
627 buf.extend_from_slice(bytes);
628 Ok(())
629}
630
631fn ensure_no_session_id(session_id: &str, frame_name: &str) -> io::Result<()> {
632 if session_id.is_empty() {
633 return Ok(());
634 }
635 Err(io::Error::new(
636 io::ErrorKind::InvalidData,
637 format!("{frame_name} frame must not include a session_id"),
638 ))
639}
640
641fn ensure_frame_consumed(buf: &[u8], pos: usize) -> io::Result<()> {
642 if pos == buf.len() {
643 return Ok(());
644 }
645 Err(io::Error::new(
646 io::ErrorKind::InvalidData,
647 format!("frame has {} trailing byte(s)", buf.len() - pos),
648 ))
649}
650
651fn read_u8(buf: &[u8], pos: &mut usize) -> io::Result<u8> {
652 if *pos >= buf.len() {
653 return Err(io::Error::new(
654 io::ErrorKind::UnexpectedEof,
655 "unexpected end of frame",
656 ));
657 }
658 let v = buf[*pos];
659 *pos += 1;
660 Ok(v)
661}
662
663fn read_u16(buf: &[u8], pos: &mut usize) -> io::Result<u16> {
664 if *pos + 2 > buf.len() {
665 return Err(io::Error::new(
666 io::ErrorKind::UnexpectedEof,
667 "unexpected end of frame",
668 ));
669 }
670 let v = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]);
671 *pos += 2;
672 Ok(v)
673}
674
675fn read_u32(buf: &[u8], pos: &mut usize) -> io::Result<u32> {
676 if *pos + 4 > buf.len() {
677 return Err(io::Error::new(
678 io::ErrorKind::UnexpectedEof,
679 "unexpected end of frame",
680 ));
681 }
682 let v = u32::from_be_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
683 *pos += 4;
684 Ok(v)
685}
686
687fn read_u64(buf: &[u8], pos: &mut usize) -> io::Result<u64> {
688 if *pos + 8 > buf.len() {
689 return Err(io::Error::new(
690 io::ErrorKind::UnexpectedEof,
691 "unexpected end of frame",
692 ));
693 }
694 let v = u64::from_be_bytes([
695 buf[*pos],
696 buf[*pos + 1],
697 buf[*pos + 2],
698 buf[*pos + 3],
699 buf[*pos + 4],
700 buf[*pos + 5],
701 buf[*pos + 6],
702 buf[*pos + 7],
703 ]);
704 *pos += 8;
705 Ok(v)
706}
707
708fn read_i32(buf: &[u8], pos: &mut usize) -> io::Result<i32> {
709 if *pos + 4 > buf.len() {
710 return Err(io::Error::new(
711 io::ErrorKind::UnexpectedEof,
712 "unexpected end of frame",
713 ));
714 }
715 let v = i32::from_be_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
716 *pos += 4;
717 Ok(v)
718}
719
720fn read_bytes(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<Vec<u8>> {
721 if *pos + len > buf.len() {
722 return Err(io::Error::new(
723 io::ErrorKind::UnexpectedEof,
724 "unexpected end of frame",
725 ));
726 }
727 let v = buf[*pos..*pos + len].to_vec();
728 *pos += len;
729 Ok(v)
730}
731
732fn read_utf8(buf: &[u8], pos: &mut usize, len: usize) -> io::Result<String> {
733 let bytes = read_bytes(buf, pos, len)?;
734 String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
735}
736
737fn read_len_prefixed_u16(buf: &[u8], pos: &mut usize) -> io::Result<String> {
738 let len = read_u16(buf, pos)? as usize;
739 read_utf8(buf, pos, len)
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745
746 fn roundtrip(frame: &BinaryFrame) {
747 let mut buf = Vec::new();
748 write_frame(&mut buf, frame).expect("write_frame");
749 let mut cursor = std::io::Cursor::new(&buf);
750 let decoded = read_frame(&mut cursor).expect("read_frame");
751 assert_eq!(&decoded, frame);
752 }
753
754 fn read_raw_body(body: Vec<u8>) -> io::Result<BinaryFrame> {
755 let mut buf = Vec::new();
756 buf.extend_from_slice(&(body.len() as u32).to_be_bytes());
757 buf.extend_from_slice(&body);
758 read_frame(&mut std::io::Cursor::new(buf))
759 }
760
761 #[test]
764 fn roundtrip_authenticate() {
765 roundtrip(&BinaryFrame::Authenticate {
766 token: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4".into(),
767 });
768 }
769
770 #[test]
771 fn roundtrip_create_session() {
772 roundtrip(&BinaryFrame::CreateSession {
773 session_id: "sess-abc-123".into(),
774 heap_limit_mb: 128,
775 cpu_time_limit_ms: 5000,
776 wall_clock_limit_ms: 9000,
777 });
778 }
779
780 #[test]
781 fn roundtrip_create_session_no_limits() {
782 roundtrip(&BinaryFrame::CreateSession {
783 session_id: "sess-1".into(),
784 heap_limit_mb: 0,
785 cpu_time_limit_ms: 0,
786 wall_clock_limit_ms: 0,
787 });
788 }
789
790 #[test]
791 fn roundtrip_destroy_session() {
792 roundtrip(&BinaryFrame::DestroySession {
793 session_id: "sess-7".into(),
794 });
795 }
796
797 #[test]
798 fn roundtrip_inject_globals() {
799 roundtrip(&BinaryFrame::InjectGlobals {
800 session_id: "sess-3".into(),
801 payload: vec![0x01, 0x02, 0x03, 0x04, 0x05],
802 });
803 }
804
805 #[test]
806 fn roundtrip_execute_exec_mode() {
807 roundtrip(&BinaryFrame::Execute {
808 session_id: "sess-1".into(),
809 mode: 0,
810 file_path: "".into(),
811 bridge_code: "(function(){ /* bridge */ })()".into(),
812 post_restore_script: "".into(),
813 userland_code: String::new(),
814 high_resolution_time: false,
815 user_code: "console.log('hello')".into(),
816 });
817 }
818
819 #[test]
820 fn roundtrip_execute_run_mode() {
821 roundtrip(&BinaryFrame::Execute {
822 session_id: "sess-2".into(),
823 mode: 1,
824 file_path: "/app/index.mjs".into(),
825 bridge_code: "(function(){ /* bridge */ })()".into(),
826 post_restore_script: "__runtimeApplyConfig({})".into(),
827 userland_code: String::new(),
828 high_resolution_time: false,
829 user_code: "export default 42".into(),
830 });
831 }
832
833 #[test]
834 fn roundtrip_bridge_response_success() {
835 roundtrip(&BinaryFrame::BridgeResponse {
836 session_id: "sess-4".into(),
837 call_id: 100,
838 status: 0,
839 payload: vec![0x93, 0x01, 0x02, 0x03],
840 });
841 }
842
843 #[test]
844 fn roundtrip_bridge_response_error() {
845 roundtrip(&BinaryFrame::BridgeResponse {
846 session_id: "sess-5".into(),
847 call_id: 101,
848 status: 1,
849 payload: b"ENOENT: no such file".to_vec(),
850 });
851 }
852
853 #[test]
854 fn roundtrip_stream_event() {
855 roundtrip(&BinaryFrame::StreamEvent {
856 session_id: "sess-5".into(),
857 event_type: "child_stdout".into(),
858 payload: vec![0x48, 0x65, 0x6c, 0x6c, 0x6f],
859 });
860 }
861
862 #[test]
863 fn roundtrip_terminate_execution() {
864 roundtrip(&BinaryFrame::TerminateExecution {
865 session_id: "sess-6".into(),
866 });
867 }
868
869 #[test]
872 fn roundtrip_bridge_call() {
873 roundtrip(&BinaryFrame::BridgeCall {
874 session_id: "sess-1".into(),
875 call_id: 200,
876 method: "_fsReadFile".into(),
877 payload: vec![0x91, 0xa5, 0x2f, 0x74, 0x6d, 0x70],
878 });
879 }
880
881 #[test]
882 fn roundtrip_execution_result_success() {
883 roundtrip(&BinaryFrame::ExecutionResult {
884 session_id: "sess-1".into(),
885 exit_code: 0,
886 exports: Some(vec![0xc0]),
887 error: None,
888 limit_exceeded: None,
889 });
890 }
891
892 #[test]
893 fn roundtrip_execution_result_error() {
894 roundtrip(&BinaryFrame::ExecutionResult {
895 session_id: "sess-2".into(),
896 exit_code: 1,
897 exports: None,
898 error: Some(ExecutionErrorBin {
899 error_type: "TypeError".into(),
900 message: "Cannot read properties of undefined".into(),
901 stack: "TypeError: Cannot read properties of undefined\n at main.js:1:5".into(),
902 code: "".into(),
903 }),
904 limit_exceeded: None,
905 });
906 }
907
908 #[test]
909 fn roundtrip_execution_result_error_with_code() {
910 roundtrip(&BinaryFrame::ExecutionResult {
911 session_id: "sess-3".into(),
912 exit_code: 1,
913 exports: None,
914 error: Some(ExecutionErrorBin {
915 error_type: "Error".into(),
916 message: "Cannot find module './missing'".into(),
917 stack: "Error: Cannot find module './missing'\n at resolve (node:internal)"
918 .into(),
919 code: "ERR_MODULE_NOT_FOUND".into(),
920 }),
921 limit_exceeded: None,
922 });
923 }
924
925 #[test]
926 fn roundtrip_execution_result_exports_and_error() {
927 roundtrip(&BinaryFrame::ExecutionResult {
928 session_id: "sess-4".into(),
929 exit_code: 1,
930 exports: Some(vec![0x01, 0x02]),
931 error: Some(ExecutionErrorBin {
932 error_type: "Error".into(),
933 message: "partial failure".into(),
934 stack: "".into(),
935 code: "".into(),
936 }),
937 limit_exceeded: None,
938 });
939 }
940
941 #[test]
942 fn roundtrip_execution_result_no_exports_no_error() {
943 roundtrip(&BinaryFrame::ExecutionResult {
944 session_id: "sess-5".into(),
945 exit_code: 0,
946 exports: None,
947 error: None,
948 limit_exceeded: None,
949 });
950 }
951
952 #[test]
953 fn roundtrip_log_stdout() {
954 roundtrip(&BinaryFrame::Log {
955 session_id: "sess-1".into(),
956 channel: 0,
957 message: "hello world\n".into(),
958 });
959 }
960
961 #[test]
962 fn roundtrip_log_stderr() {
963 roundtrip(&BinaryFrame::Log {
964 session_id: "sess-1".into(),
965 channel: 1,
966 message: "warning: deprecated API\n".into(),
967 });
968 }
969
970 #[test]
971 fn roundtrip_stream_callback() {
972 roundtrip(&BinaryFrame::StreamCallback {
973 session_id: "sess-1".into(),
974 callback_type: "child_dispatch".into(),
975 payload: vec![0x92, 0x01, 0xa3, 0x66, 0x6f, 0x6f],
976 });
977 }
978
979 #[test]
982 fn roundtrip_warm_snapshot() {
983 roundtrip(&BinaryFrame::WarmSnapshot {
984 bridge_code: "(function(){ /* bridge IIFE */ })()".into(),
985 userland_code: String::new(),
986 });
987 }
988
989 #[test]
990 fn roundtrip_warm_snapshot_empty_bridge_code() {
991 roundtrip(&BinaryFrame::WarmSnapshot {
992 bridge_code: "".into(),
993 userland_code: String::new(),
994 });
995 }
996
997 #[test]
998 fn roundtrip_warm_snapshot_large_bridge_code() {
999 roundtrip(&BinaryFrame::WarmSnapshot {
1000 bridge_code: "x".repeat(100_000),
1001 userland_code: String::new(),
1002 });
1003 }
1004
1005 #[test]
1006 fn extract_session_id_warm_snapshot_returns_none() {
1007 let frame = BinaryFrame::WarmSnapshot {
1008 bridge_code: "bridge()".into(),
1009 userland_code: String::new(),
1010 };
1011 let mut buf = Vec::new();
1012 write_frame(&mut buf, &frame).expect("write");
1013 let raw = &buf[4..];
1014 let result = extract_session_id(raw).expect("extract");
1015 assert_eq!(result, None);
1016 }
1017
1018 #[test]
1021 fn roundtrip_empty_payloads() {
1022 roundtrip(&BinaryFrame::BridgeResponse {
1023 session_id: "s".into(),
1024 call_id: 0,
1025 status: 0,
1026 payload: vec![],
1027 });
1028 roundtrip(&BinaryFrame::StreamEvent {
1029 session_id: "s".into(),
1030 event_type: "".into(),
1031 payload: vec![],
1032 });
1033 roundtrip(&BinaryFrame::BridgeCall {
1034 session_id: "s".into(),
1035 call_id: 0,
1036 method: "".into(),
1037 payload: vec![],
1038 });
1039 roundtrip(&BinaryFrame::InjectGlobals {
1040 session_id: "s".into(),
1041 payload: vec![],
1042 });
1043 }
1044
1045 #[test]
1046 fn roundtrip_empty_session_id() {
1047 roundtrip(&BinaryFrame::DestroySession {
1048 session_id: "".into(),
1049 });
1050 }
1051
1052 #[test]
1053 fn roundtrip_large_binary_payload() {
1054 roundtrip(&BinaryFrame::BridgeResponse {
1055 session_id: "sess-big".into(),
1056 call_id: 42,
1057 status: 0,
1058 payload: vec![0xAA; 1024],
1059 });
1060 }
1061
1062 #[test]
1065 fn frame_length_prefix_is_big_endian() {
1066 let frame = BinaryFrame::DestroySession {
1067 session_id: "x".into(),
1068 };
1069 let mut buf = Vec::new();
1070 write_frame(&mut buf, &frame).expect("write");
1071 let len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
1072 assert_eq!(len as usize, buf.len() - 4);
1073 }
1074
1075 #[test]
1076 fn multiple_frames_in_stream() {
1077 let frames = vec![
1078 BinaryFrame::CreateSession {
1079 session_id: "a".into(),
1080 heap_limit_mb: 64,
1081 cpu_time_limit_ms: 1000,
1082 wall_clock_limit_ms: 0,
1083 },
1084 BinaryFrame::Execute {
1085 session_id: "a".into(),
1086 mode: 0,
1087 file_path: "".into(),
1088 bridge_code: "bridge()".into(),
1089 post_restore_script: "".into(),
1090 userland_code: String::new(),
1091 high_resolution_time: false,
1092 user_code: "1+1".into(),
1093 },
1094 BinaryFrame::DestroySession {
1095 session_id: "a".into(),
1096 },
1097 ];
1098 let mut buf = Vec::new();
1099 for f in &frames {
1100 write_frame(&mut buf, f).expect("write");
1101 }
1102 let mut cursor = std::io::Cursor::new(&buf);
1103 for f in &frames {
1104 let decoded = read_frame(&mut cursor).expect("read");
1105 assert_eq!(&decoded, f);
1106 }
1107 }
1108
1109 #[test]
1110 fn reject_oversized_frame() {
1111 let oversized_len: u32 = 64 * 1024 * 1024 + 1;
1112 let mut buf = Vec::new();
1113 buf.extend_from_slice(&oversized_len.to_be_bytes());
1114 buf.extend_from_slice(&[0u8; 16]);
1115 let mut cursor = std::io::Cursor::new(&buf);
1116 let result = read_frame(&mut cursor);
1117 assert!(result.is_err());
1118 let err = result.unwrap_err();
1119 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1120 assert!(err.to_string().contains("exceeds maximum"));
1121 }
1122
1123 #[test]
1124 fn reject_unknown_message_type() {
1125 let body = vec![0xFF, 0x00]; let result = read_raw_body(body);
1128 assert!(result.is_err());
1129 assert!(result
1130 .unwrap_err()
1131 .to_string()
1132 .contains("unknown message type"));
1133 }
1134
1135 #[test]
1136 fn reject_session_id_on_sessionless_frames() {
1137 let authenticate = read_raw_body(vec![MSG_AUTHENTICATE, 1, b's', b't']);
1138 assert!(authenticate.is_err());
1139 assert!(authenticate
1140 .unwrap_err()
1141 .to_string()
1142 .contains("must not include a session_id"));
1143
1144 let warm_snapshot = read_raw_body(vec![MSG_WARM_SNAPSHOT, 1, b's', 0, 0, 0, 0]);
1145 assert!(warm_snapshot.is_err());
1146 assert!(warm_snapshot
1147 .unwrap_err()
1148 .to_string()
1149 .contains("must not include a session_id"));
1150 }
1151
1152 #[test]
1153 fn reject_trailing_bytes_on_fixed_shape_frames() {
1154 let mut create_session = vec![MSG_CREATE_SESSION, 1, b's'];
1155 create_session.extend_from_slice(&0u32.to_be_bytes());
1156 create_session.extend_from_slice(&0u32.to_be_bytes());
1157 create_session.extend_from_slice(&0u32.to_be_bytes());
1158 create_session.push(0xAA);
1159
1160 let destroy_session = vec![MSG_DESTROY_SESSION, 1, b's', 0xAA];
1161 let terminate_execution = vec![MSG_TERMINATE_EXECUTION, 1, b's', 0xAA];
1162 let warm_snapshot = vec![MSG_WARM_SNAPSHOT, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xAA];
1165
1166 for body in [
1167 create_session,
1168 destroy_session,
1169 terminate_execution,
1170 warm_snapshot,
1171 ] {
1172 let result = read_raw_body(body);
1173 assert!(result.is_err());
1174 assert!(result.unwrap_err().to_string().contains("trailing byte"));
1175 }
1176 }
1177
1178 #[test]
1179 fn reject_unknown_execution_result_flags() {
1180 let mut body = vec![MSG_EXECUTION_RESULT, 1, b's'];
1181 body.extend_from_slice(&0i32.to_be_bytes());
1182 body.push(0x80);
1183
1184 let result = read_raw_body(body);
1185 assert!(result.is_err());
1186 assert!(result
1187 .unwrap_err()
1188 .to_string()
1189 .contains("unknown ExecutionResult flags"));
1190 }
1191
1192 #[test]
1193 fn empty_input_returns_eof() {
1194 let buf: Vec<u8> = Vec::new();
1195 let mut cursor = std::io::Cursor::new(&buf);
1196 let result = read_frame(&mut cursor);
1197 assert!(result.is_err());
1198 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::UnexpectedEof);
1199 }
1200
1201 #[test]
1204 fn extract_session_id_from_raw_bytes() {
1205 let frame = BinaryFrame::BridgeCall {
1207 session_id: "my-session-42".into(),
1208 call_id: 7,
1209 method: "_fsReadFile".into(),
1210 payload: vec![0x01, 0x02],
1211 };
1212 let mut buf = Vec::new();
1213 write_frame(&mut buf, &frame).expect("write");
1214
1215 let raw = &buf[4..];
1217 let sid = extract_session_id(raw)
1218 .expect("extract")
1219 .expect("should have sid");
1220 assert_eq!(sid, "my-session-42");
1221 }
1222
1223 #[test]
1224 fn extract_session_id_from_various_types() {
1225 let test_cases: Vec<BinaryFrame> = vec![
1226 BinaryFrame::CreateSession {
1227 session_id: "sess-create".into(),
1228 heap_limit_mb: 0,
1229 cpu_time_limit_ms: 0,
1230 wall_clock_limit_ms: 0,
1231 },
1232 BinaryFrame::DestroySession {
1233 session_id: "sess-destroy".into(),
1234 },
1235 BinaryFrame::Execute {
1236 session_id: "sess-exec".into(),
1237 mode: 0,
1238 file_path: "".into(),
1239 bridge_code: "".into(),
1240 post_restore_script: "".into(),
1241 userland_code: String::new(),
1242 high_resolution_time: false,
1243 user_code: "".into(),
1244 },
1245 BinaryFrame::BridgeResponse {
1246 session_id: "sess-resp".into(),
1247 call_id: 1,
1248 status: 0,
1249 payload: vec![],
1250 },
1251 BinaryFrame::ExecutionResult {
1252 session_id: "sess-result".into(),
1253 exit_code: 0,
1254 exports: None,
1255 error: None,
1256 limit_exceeded: None,
1257 },
1258 BinaryFrame::Log {
1259 session_id: "sess-log".into(),
1260 channel: 0,
1261 message: "hi".into(),
1262 },
1263 ];
1264
1265 for frame in &test_cases {
1266 let mut buf = Vec::new();
1267 write_frame(&mut buf, frame).expect("write");
1268 let raw = &buf[4..];
1269 let sid = extract_session_id(raw)
1270 .expect("extract")
1271 .expect("should have sid");
1272 let expected = match frame {
1274 BinaryFrame::CreateSession { session_id, .. }
1275 | BinaryFrame::DestroySession { session_id }
1276 | BinaryFrame::Execute { session_id, .. }
1277 | BinaryFrame::BridgeResponse { session_id, .. }
1278 | BinaryFrame::ExecutionResult { session_id, .. }
1279 | BinaryFrame::Log { session_id, .. } => session_id.as_str(),
1280 _ => unreachable!(),
1281 };
1282 assert_eq!(sid, expected, "session_id mismatch for frame: {:?}", frame);
1283 }
1284 }
1285
1286 #[test]
1287 fn extract_session_id_authenticate_returns_none() {
1288 let frame = BinaryFrame::Authenticate {
1289 token: "secret-token".into(),
1290 };
1291 let mut buf = Vec::new();
1292 write_frame(&mut buf, &frame).expect("write");
1293 let raw = &buf[4..];
1294 let result = extract_session_id(raw).expect("extract");
1295 assert_eq!(result, None);
1296 }
1297
1298 #[test]
1299 fn extract_session_id_too_short() {
1300 let result = extract_session_id(&[0x02]); assert!(result.is_err());
1302 }
1303
1304 #[test]
1307 fn wire_format_message_type_bytes() {
1308 let cases: Vec<(BinaryFrame, u8)> = vec![
1309 (BinaryFrame::Authenticate { token: "t".into() }, 0x01),
1310 (
1311 BinaryFrame::CreateSession {
1312 session_id: "s".into(),
1313 heap_limit_mb: 0,
1314 cpu_time_limit_ms: 0,
1315 wall_clock_limit_ms: 0,
1316 },
1317 0x02,
1318 ),
1319 (
1320 BinaryFrame::DestroySession {
1321 session_id: "s".into(),
1322 },
1323 0x03,
1324 ),
1325 (
1326 BinaryFrame::InjectGlobals {
1327 session_id: "s".into(),
1328 payload: vec![],
1329 },
1330 0x04,
1331 ),
1332 (
1333 BinaryFrame::Execute {
1334 session_id: "s".into(),
1335 mode: 0,
1336 file_path: "".into(),
1337 bridge_code: "".into(),
1338 post_restore_script: "".into(),
1339 userland_code: String::new(),
1340 high_resolution_time: false,
1341 user_code: "".into(),
1342 },
1343 0x05,
1344 ),
1345 (
1346 BinaryFrame::BridgeResponse {
1347 session_id: "s".into(),
1348 call_id: 0,
1349 status: 0,
1350 payload: vec![],
1351 },
1352 0x06,
1353 ),
1354 (
1355 BinaryFrame::StreamEvent {
1356 session_id: "s".into(),
1357 event_type: "".into(),
1358 payload: vec![],
1359 },
1360 0x07,
1361 ),
1362 (
1363 BinaryFrame::TerminateExecution {
1364 session_id: "s".into(),
1365 },
1366 0x08,
1367 ),
1368 (
1369 BinaryFrame::WarmSnapshot {
1370 bridge_code: "bridge()".into(),
1371 userland_code: String::new(),
1372 },
1373 0x09,
1374 ),
1375 (
1376 BinaryFrame::BridgeCall {
1377 session_id: "s".into(),
1378 call_id: 0,
1379 method: "".into(),
1380 payload: vec![],
1381 },
1382 0x81,
1383 ),
1384 (
1385 BinaryFrame::ExecutionResult {
1386 session_id: "s".into(),
1387 exit_code: 0,
1388 exports: None,
1389 error: None,
1390 limit_exceeded: None,
1391 },
1392 0x82,
1393 ),
1394 (
1395 BinaryFrame::Log {
1396 session_id: "s".into(),
1397 channel: 0,
1398 message: "".into(),
1399 },
1400 0x83,
1401 ),
1402 (
1403 BinaryFrame::StreamCallback {
1404 session_id: "s".into(),
1405 callback_type: "".into(),
1406 payload: vec![],
1407 },
1408 0x84,
1409 ),
1410 ];
1411 for (frame, expected_type) in &cases {
1412 let mut buf = Vec::new();
1413 write_frame(&mut buf, frame).expect("write");
1414 assert_eq!(buf[4], *expected_type, "type mismatch for: {:?}", frame);
1416 }
1417 }
1418
1419 #[test]
1422 fn frame_to_bytes_matches_write_frame() {
1423 let frame = BinaryFrame::BridgeCall {
1424 session_id: "sess-42".into(),
1425 call_id: 123,
1426 method: "_fsReadFile".into(),
1427 payload: vec![0x01, 0x02, 0x03],
1428 };
1429 let bytes = frame_to_bytes(&frame).expect("frame_to_bytes");
1430 let mut buf = Vec::new();
1431 write_frame(&mut buf, &frame).expect("write_frame");
1432 assert_eq!(bytes, buf);
1433 }
1434
1435 #[test]
1436 fn frame_to_bytes_roundtrip() {
1437 let frame = BinaryFrame::ExecutionResult {
1438 session_id: "sess-1".into(),
1439 exit_code: 0,
1440 exports: Some(vec![0xAA, 0xBB]),
1441 error: None,
1442 limit_exceeded: None,
1443 };
1444 let bytes = frame_to_bytes(&frame).expect("frame_to_bytes");
1445 let mut cursor = std::io::Cursor::new(&bytes);
1446 let decoded = read_frame(&mut cursor).expect("read_frame");
1447 assert_eq!(decoded, frame);
1448 }
1449
1450 #[test]
1451 fn frame_to_bytes_atomic_no_interleaving() {
1452 let frame = BinaryFrame::BridgeCall {
1455 session_id: "s".into(),
1456 call_id: 1,
1457 method: "_fn".into(),
1458 payload: vec![0xFF; 1024],
1459 };
1460 let bytes = frame_to_bytes(&frame).expect("frame_to_bytes");
1461 let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
1463 assert_eq!(len, bytes.len() - 4);
1464 }
1465
1466 #[test]
1467 fn encode_frame_into_reuses_buffer_capacity() {
1468 let mut buf = Vec::new();
1469 let frame = BinaryFrame::BridgeCall {
1470 session_id: "s1".into(),
1471 call_id: 1,
1472 method: "_fn".into(),
1473 payload: vec![0xAA; 512],
1474 };
1475
1476 encode_frame_into(&mut buf, &frame).expect("encode");
1478 let first_bytes = buf.clone();
1479 let cap_after_first = buf.capacity();
1480 assert!(cap_after_first >= buf.len());
1481
1482 let frame2 = BinaryFrame::BridgeCall {
1484 session_id: "s1".into(),
1485 call_id: 2,
1486 method: "_fn".into(),
1487 payload: vec![0xBB; 256],
1488 };
1489 encode_frame_into(&mut buf, &frame2).expect("encode");
1490 assert!(
1491 buf.capacity() >= cap_after_first,
1492 "capacity should not shrink"
1493 );
1494
1495 let decoded = read_frame(&mut std::io::Cursor::new(&first_bytes)).expect("decode");
1497 assert_eq!(decoded, frame);
1498 let decoded2 = read_frame(&mut std::io::Cursor::new(&buf)).expect("decode");
1499 assert_eq!(decoded2, frame2);
1500 }
1501
1502 #[test]
1503 fn encode_frame_into_matches_frame_to_bytes() {
1504 let frame = BinaryFrame::ExecutionResult {
1505 session_id: "sess-1".into(),
1506 exit_code: 0,
1507 exports: Some(vec![0x01, 0x02]),
1508 error: None,
1509 limit_exceeded: None,
1510 };
1511 let expected = frame_to_bytes(&frame).expect("frame_to_bytes");
1512 let mut buf = Vec::new();
1513 encode_frame_into(&mut buf, &frame).expect("encode_frame_into");
1514 assert_eq!(buf, expected);
1515 }
1516
1517 #[test]
1518 fn encode_frame_into_grows_to_high_water_mark() {
1519 let mut buf = Vec::new();
1520
1521 let small = BinaryFrame::Log {
1523 session_id: "s".into(),
1524 channel: 0,
1525 message: "hi".into(),
1526 };
1527 encode_frame_into(&mut buf, &small).expect("encode");
1528 let small_cap = buf.capacity();
1529
1530 let large = BinaryFrame::BridgeCall {
1532 session_id: "s".into(),
1533 call_id: 1,
1534 method: "_fn".into(),
1535 payload: vec![0xFF; 4096],
1536 };
1537 encode_frame_into(&mut buf, &large).expect("encode");
1538 let large_cap = buf.capacity();
1539 assert!(large_cap > small_cap);
1540
1541 encode_frame_into(&mut buf, &small).expect("encode");
1543 assert_eq!(
1544 buf.capacity(),
1545 large_cap,
1546 "capacity should stay at high-water mark"
1547 );
1548 }
1549
1550 #[test]
1553 fn write_session_id_rejects_oversized() {
1554 let long_sid = "x".repeat(256);
1556 let frame = BinaryFrame::DestroySession {
1557 session_id: long_sid,
1558 };
1559 let result = frame_to_bytes(&frame);
1560 assert!(result.is_err());
1561 let err = result.unwrap_err();
1562 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1563 assert!(err.to_string().contains("session ID byte length"));
1564 assert!(err.to_string().contains("255"));
1565 }
1566
1567 #[test]
1568 fn write_session_id_accepts_max() {
1569 let max_sid = "a".repeat(255);
1571 let frame = BinaryFrame::DestroySession {
1572 session_id: max_sid.clone(),
1573 };
1574 let bytes = frame_to_bytes(&frame).expect("should accept 255-byte session ID");
1575 let decoded = read_frame(&mut std::io::Cursor::new(&bytes)).expect("decode");
1576 assert_eq!(decoded, frame);
1577 }
1578
1579 #[test]
1580 fn write_len_prefixed_u16_rejects_oversized() {
1581 let long_method = "m".repeat(65536);
1583 let frame = BinaryFrame::BridgeCall {
1584 session_id: "s".into(),
1585 call_id: 1,
1586 method: long_method,
1587 payload: vec![],
1588 };
1589 let result = frame_to_bytes(&frame);
1590 assert!(result.is_err());
1591 let err = result.unwrap_err();
1592 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1593 assert!(err.to_string().contains("string byte length"));
1594 assert!(err.to_string().contains("65535"));
1595 }
1596
1597 #[test]
1598 fn write_len_prefixed_u16_accepts_max() {
1599 let max_method = "m".repeat(65535);
1601 let frame = BinaryFrame::BridgeCall {
1602 session_id: "s".into(),
1603 call_id: 1,
1604 method: max_method.clone(),
1605 payload: vec![],
1606 };
1607 let bytes = frame_to_bytes(&frame).expect("should accept 65535-byte method");
1608 let decoded = read_frame(&mut std::io::Cursor::new(&bytes)).expect("decode");
1609 assert_eq!(decoded, frame);
1610 }
1611
1612 #[test]
1613 fn execute_file_path_rejects_oversized() {
1614 let long_path = "/".repeat(65536);
1616 let frame = BinaryFrame::Execute {
1617 session_id: "s".into(),
1618 mode: 0,
1619 file_path: long_path,
1620 bridge_code: "".into(),
1621 post_restore_script: "".into(),
1622 userland_code: String::new(),
1623 high_resolution_time: false,
1624 user_code: "".into(),
1625 };
1626 let result = frame_to_bytes(&frame);
1627 assert!(result.is_err());
1628 let err = result.unwrap_err();
1629 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
1630 assert!(err.to_string().contains("65535"));
1631 }
1632}