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;
38
39/// A decoded binary frame — all fields are borrowed or owned depending on use.
40#[derive(Debug, Clone, PartialEq)]
41pub enum BinaryFrame {
42    // Host → Rust
43    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>, // V8-serialized { processConfig, osConfig }
58    },
59    Execute {
60        session_id: String,
61        mode: u8, // 0 = exec, 1 = run
62        file_path: String,
63        bridge_code: String,
64        post_restore_script: String,
65        // Optional agent-SDK bundle evaluated into the per-sidecar snapshot alongside
66        // the bridge (empty = bridge-only snapshot, unchanged behavior). The snapshot
67        // is cached process-wide keyed by sha256(bridge_code + userland_code).
68        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,       // 0 = success, 1 = error
76        payload: Vec<u8>, // V8-serialized result OR UTF-8 error message
77    },
78    StreamEvent {
79        session_id: String,
80        event_type: String,
81        payload: Vec<u8>, // V8-serialized payload
82    },
83    TerminateExecution {
84        session_id: String,
85    },
86    WarmSnapshot {
87        bridge_code: String,
88        // Optional agent-SDK bundle to pre-warm into the snapshot (empty = bridge-only).
89        userland_code: String,
90    },
91
92    // Rust → Host
93    BridgeCall {
94        session_id: String,
95        call_id: u64,
96        method: String,
97        payload: Vec<u8>, // V8-serialized args
98    },
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, // 0 = stdout, 1 = stderr
108        message: String,
109    },
110    StreamCallback {
111        session_id: String,
112        callback_type: String,
113        payload: Vec<u8>, // V8-serialized payload
114    },
115}
116
117/// Structured error in binary format.
118#[derive(Debug, Clone, PartialEq)]
119pub struct ExecutionErrorBin {
120    pub error_type: String,
121    pub message: String,
122    pub stack: String,
123    pub code: String, // empty string = no code
124}
125
126/// Encode a binary frame into a provided buffer (length prefix + body).
127/// The buffer is cleared first; capacity is preserved across calls.
128/// Used by per-session buffering to avoid per-call allocation.
129pub fn encode_frame_into(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
130    buf.clear();
131    // Reserve 4 bytes for the length prefix (filled after body)
132    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
146/// Serialize a binary frame to a complete byte vector (length prefix + body).
147/// Used by per-session buffering to build the frame without holding any shared lock.
148pub 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
154/// Write a binary frame to a writer.
155pub 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
161/// Read a binary frame from a reader.
162pub 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/// Extract session_id from raw frame bytes without full deserialization.
180/// `raw` starts at the first byte after the 4-byte length prefix (i.e. the msg_type byte).
181/// Returns None for Authenticate (which has no session_id).
182#[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
206// -- Internal encode/decode --
207
208fn encode_body(buf: &mut Vec<u8>, frame: &BinaryFrame) -> io::Result<()> {
209    match frame {
210        BinaryFrame::Authenticate { token } => {
211            buf.push(MSG_AUTHENTICATE);
212            // Authenticate has no session_id — sid_len = 0
213            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            // file_path length (u16 BE)
254            write_len_prefixed_u16(buf, file_path)?;
255            // bridge_code length (u32 BE)
256            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            // post_restore_script length (u32 BE)
260            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            // userland_code length (u32 BE)
264            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            // user_code (rest of frame)
269            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); // no session_id
303            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            // userland_code length (u32 BE) + bytes (rest)
307            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    // Read the session_id field uniformly. Sessionless frame types validate
384    // that it is empty after the message type is known.
385    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            // Token is rest of frame after sid (sid is empty for Authenticate)
392            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
557// -- Primitive read/write helpers --
558
559fn 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    // -- Host → Rust message types --
716
717    #[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    // -- Rust → Host message types --
824
825    #[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    // -- WarmSnapshot --
929
930    #[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    // -- Edge cases --
968
969    #[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    // -- Framing validation --
1012
1013    #[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        // Craft a frame with unknown message type 0xFF
1075        let body = vec![0xFF, 0x00]; // msg_type=0xFF, sid_len=0
1076        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        // WarmSnapshot body: no-session-id flag, bridge_code (u32 len = 0), then
1112        // userland_code (u32 len = 0); a single trailing 0xAA must be rejected.
1113        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    // -- Session ID routing --
1151
1152    #[test]
1153    fn extract_session_id_from_raw_bytes() {
1154        // Build a BridgeCall frame and verify we can extract session_id from raw bytes
1155        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        // Raw bytes start after the 4-byte length prefix
1165        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            // Verify it matches the expected session_id
1221            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]); // msg_type only, no sid_len
1249        assert!(result.is_err());
1250    }
1251
1252    // -- Wire format byte-level verification --
1253
1254    #[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            // Byte 4 (after 4-byte length prefix) is the message type
1362            assert_eq!(buf[4], *expected_type, "type mismatch for: {:?}", frame);
1363        }
1364    }
1365
1366    // -- frame_to_bytes tests --
1367
1368    #[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        // Verify frame_to_bytes produces a single contiguous byte vector
1399        // (no intermediate writes that could interleave)
1400        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        // Length prefix matches body
1408        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        // First encode grows the buffer
1423        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        // Second encode reuses capacity (no new allocation if same size)
1429        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        // Verify round-trip correctness
1442        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        // Small frame
1467        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        // Large frame grows buffer
1476        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        // Small frame again — capacity stays at high-water mark
1487        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    // -- Overflow guard tests --
1496
1497    #[test]
1498    fn write_session_id_rejects_oversized() {
1499        // Session ID > 255 bytes must be rejected
1500        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        // Session ID of exactly 255 bytes must succeed
1515        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        // String > 65535 bytes in a u16-prefixed field must be rejected
1527        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        // String of exactly 65535 bytes in a u16-prefixed field must succeed
1545        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        // file_path > 65535 bytes must be rejected (encoded as u16)
1560        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}