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