Skip to main content

agentos_v8_runtime/
ipc_binary.rs

1// Binary header IPC framing — custom wire format for all message types.
2//
3// Wire format per frame:
4//   [4B total_len (u32 BE, excludes self)]
5//   [1B msg_type]
6//   [1B sid_len (N)]
7//   [N bytes session_id (UTF-8)]
8//   [... type-specific fixed fields ...]
9//   [M bytes payload (rest of frame)]
10//
11// Existing ipc.rs (MessagePack framing) is left unchanged.
12
13use std::io::{self, Read, Write};
14
15/// Maximum frame payload: 64 MB (same limit as MessagePack framing).
16const MAX_FRAME_SIZE: u32 = 64 * 1024 * 1024;
17
18// Host → Rust message type codes
19const 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
29// Rust → Host message type codes
30const MSG_BRIDGE_CALL: u8 = 0x81;
31const MSG_EXECUTION_RESULT: u8 = 0x82;
32const MSG_LOG: u8 = 0x83;
33const MSG_STREAM_CALLBACK: u8 = 0x84;
34
35// ExecutionResult flags
36const FLAG_HAS_EXPORTS: u8 = 0x01;
37const FLAG_HAS_ERROR: u8 = 0x02;
38const FLAG_LIMIT_EXCEEDED: u8 = 0x04;
39
40/// A decoded binary frame — all fields are borrowed or owned depending on use.
41#[derive(Debug, Clone, PartialEq)]
42pub enum BinaryFrame {
43    // Host → Rust
44    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>, // V8-serialized { processConfig, osConfig }
59    },
60    Execute {
61        session_id: String,
62        mode: u8, // 0 = exec, 1 = run
63        file_path: String,
64        bridge_code: String,
65        post_restore_script: String,
66        // Optional agent-SDK bundle evaluated into the per-sidecar snapshot alongside
67        // the bridge (empty = bridge-only snapshot, unchanged behavior). The snapshot
68        // is cached process-wide keyed by sha256(bridge_code + userland_code).
69        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,       // 0 = success, 1 = error
77        payload: Vec<u8>, // V8-serialized result OR UTF-8 error message
78    },
79    StreamEvent {
80        session_id: String,
81        event_type: String,
82        payload: Vec<u8>, // V8-serialized payload
83    },
84    TerminateExecution {
85        session_id: String,
86    },
87    WarmSnapshot {
88        bridge_code: String,
89        // Optional agent-SDK bundle to pre-warm into the snapshot (empty = bridge-only).
90        userland_code: String,
91    },
92
93    // Rust → Host
94    BridgeCall {
95        session_id: String,
96        call_id: u64,
97        method: String,
98        payload: Vec<u8>, // V8-serialized args
99    },
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, // 0 = stdout, 1 = stderr
110        message: String,
111    },
112    StreamCallback {
113        session_id: String,
114        callback_type: String,
115        payload: Vec<u8>, // V8-serialized payload
116    },
117}
118
119/// Structured error in binary format.
120#[derive(Debug, Clone, PartialEq)]
121pub struct ExecutionErrorBin {
122    pub error_type: String,
123    pub message: String,
124    pub stack: String,
125    pub code: String, // empty string = no code
126}
127
128/// A runtime limit that stopped the script. The runtime sets this from its own
129/// state, never from guest values, so callers can trust it to name the cause.
130#[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
159/// Encode a binary frame into a provided buffer (length prefix + body).
160/// The buffer is cleared first; capacity is preserved across calls.
161/// Used by per-session buffering to avoid per-call allocation.
162pub fn encode_frame_into(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
163    buf.clear();
164    // Reserve 4 bytes for the length prefix (filled after body)
165    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
179/// Serialize a binary frame to a complete byte vector (length prefix + body).
180/// Used by per-session buffering to build the frame without holding any shared lock.
181pub 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
187/// Write a binary frame to a writer.
188pub 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
194/// Read a binary frame from a reader.
195pub 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/// Extract session_id from raw frame bytes without full deserialization.
213/// `raw` starts at the first byte after the 4-byte length prefix (i.e. the msg_type byte).
214/// Returns None for Authenticate (which has no session_id).
215#[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
239// -- Internal encode/decode --
240
241fn encode_body(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
242    match frame {
243        BinaryFrame::Authenticate { token } => {
244            buf.push(MSG_AUTHENTICATE);
245            // Authenticate has no session_id — sid_len = 0
246            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            // file_path length (u16 BE)
287            write_len_prefixed_u16(buf, file_path)?;
288            // bridge_code length (u32 BE)
289            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            // post_restore_script length (u32 BE)
293            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            // userland_code length (u32 BE)
297            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            // user_code (rest of frame)
302            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); // no session_id
336            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            // userland_code length (u32 BE) + bytes (rest)
340            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    // Read the session_id field uniformly. Sessionless frame types validate
424    // that it is empty after the message type is known.
425    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            // Token is rest of frame after sid (sid is empty for Authenticate)
432            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
603// -- Primitive read/write helpers --
604
605fn 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    // -- Host → Rust message types --
762
763    #[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    // -- Rust → Host message types --
870
871    #[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    // -- WarmSnapshot --
980
981    #[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    // -- Edge cases --
1019
1020    #[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    // -- Framing validation --
1063
1064    #[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        // Craft a frame with unknown message type 0xFF
1126        let body = vec![0xFF, 0x00]; // msg_type=0xFF, sid_len=0
1127        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        // WarmSnapshot body: no-session-id flag, bridge_code (u32 len = 0), then
1163        // userland_code (u32 len = 0); a single trailing 0xAA must be rejected.
1164        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    // -- Session ID routing --
1202
1203    #[test]
1204    fn extract_session_id_from_raw_bytes() {
1205        // Build a BridgeCall frame and verify we can extract session_id from raw bytes
1206        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        // Raw bytes start after the 4-byte length prefix
1216        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            // Verify it matches the expected session_id
1273            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]); // msg_type only, no sid_len
1301        assert!(result.is_err());
1302    }
1303
1304    // -- Wire format byte-level verification --
1305
1306    #[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            // Byte 4 (after 4-byte length prefix) is the message type
1415            assert_eq!(buf[4], *expected_type, "type mismatch for: {:?}", frame);
1416        }
1417    }
1418
1419    // -- frame_to_bytes tests --
1420
1421    #[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        // Verify frame_to_bytes produces a single contiguous byte vector
1453        // (no intermediate writes that could interleave)
1454        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        // Length prefix matches body
1462        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        // First encode grows the buffer
1477        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        // Second encode reuses capacity (no new allocation if same size)
1483        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        // Verify round-trip correctness
1496        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        // Small frame
1522        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        // Large frame grows buffer
1531        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        // Small frame again — capacity stays at high-water mark
1542        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    // -- Overflow guard tests --
1551
1552    #[test]
1553    fn write_session_id_rejects_oversized() {
1554        // Session ID > 255 bytes must be rejected
1555        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        // Session ID of exactly 255 bytes must succeed
1570        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        // String > 65535 bytes in a u16-prefixed field must be rejected
1582        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        // String of exactly 65535 bytes in a u16-prefixed field must succeed
1600        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        // file_path > 65535 bytes must be rejected (encoded as u16)
1615        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}