microsandbox_agent_client/transport.rs
1//! Transport packet abstraction for agent protocol frames.
2//!
3//! The transport layer is intentionally CBOR-blind. It moves complete
4//! length-prefixed packets and leaves message-type validation to higher layers.
5
6use microsandbox_protocol::codec::{self, RawFrame};
7use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
8
9use crate::error::{AgentClientError, AgentClientResult};
10
11//--------------------------------------------------------------------------------------------------
12// Types
13//--------------------------------------------------------------------------------------------------
14
15/// Exact bytes sent over an agent transport.
16///
17/// A packet contains the four-byte length prefix followed by one binary frame:
18/// `[len: u32 BE][id: u32 BE][flags: u8][body...]`.
19#[derive(Clone)]
20pub struct TransportPacket {
21 bytes: Vec<u8>,
22}
23
24/// Owned byte transport accepted directly by `AgentClient::connect_stream`.
25pub use microsandbox_protocol_client::ByteTransport as AgentTransport;
26
27//--------------------------------------------------------------------------------------------------
28// Methods
29//--------------------------------------------------------------------------------------------------
30
31impl TransportPacket {
32 /// Validate and wrap exact wire bytes.
33 ///
34 /// The input must contain exactly one complete transport packet. It may be
35 /// used by unchecked write paths, but it is still structurally validated so
36 /// callers cannot accidentally concatenate packets or pass a truncated
37 /// frame.
38 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> AgentClientResult<Self> {
39 let bytes = bytes.into();
40 if bytes.len() < 9 {
41 return Err(AgentClientError::InvalidPacket(
42 "packet does not contain a complete frame".to_string(),
43 ));
44 }
45 let length = u32::from_be_bytes(bytes[..4].try_into().unwrap());
46 if !(5..=codec::MAX_FRAME_SIZE).contains(&length) || length as usize + 4 != bytes.len() {
47 return Err(AgentClientError::InvalidPacket(
48 "packet must contain exactly one bounded frame".to_string(),
49 ));
50 }
51 Ok(Self { bytes })
52 }
53
54 /// Create a packet from a structured raw frame.
55 ///
56 /// The frame body is left opaque; this method only applies the binary
57 /// transport framing.
58 pub fn from_frame(frame: &RawFrame) -> AgentClientResult<Self> {
59 let mut bytes = Vec::new();
60 codec::encode_raw_to_buf(frame, &mut bytes)?;
61 Ok(Self { bytes })
62 }
63
64 /// Borrow the exact transport bytes.
65 pub fn as_bytes(&self) -> &[u8] {
66 &self.bytes
67 }
68
69 /// Consume the packet and return its exact transport bytes.
70 pub fn into_bytes(self) -> Vec<u8> {
71 self.bytes
72 }
73}
74
75//--------------------------------------------------------------------------------------------------
76// Trait Implementations
77//--------------------------------------------------------------------------------------------------
78
79impl std::fmt::Debug for TransportPacket {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("TransportPacket")
82 .field("bytes", &self.bytes.len())
83 .finish()
84 }
85}
86
87//--------------------------------------------------------------------------------------------------
88// Functions
89//--------------------------------------------------------------------------------------------------
90
91/// Read one length-prefixed packet from a byte stream.
92///
93/// Returns `Ok(None)` on clean EOF before a new packet begins.
94pub async fn read_packet_from_io<R>(reader: &mut R) -> AgentClientResult<Option<TransportPacket>>
95where
96 R: AsyncRead + Unpin,
97{
98 let mut prefix = [0; 4];
99 if reader.read(&mut prefix[..1]).await? == 0 {
100 return Ok(None);
101 }
102 // Only EOF before the first byte is clean; a partial prefix is truncation.
103 reader.read_exact(&mut prefix[1..]).await?;
104 let length = u32::from_be_bytes(prefix);
105 if !(5..=codec::MAX_FRAME_SIZE).contains(&length) {
106 return Err(AgentClientError::InvalidPacket(
107 "invalid frame length".into(),
108 ));
109 }
110 let mut bytes = vec![0; length as usize + 4];
111 bytes[..4].copy_from_slice(&prefix);
112 reader.read_exact(&mut bytes[4..]).await?;
113 Ok(Some(TransportPacket { bytes }))
114}
115
116/// Write one packet to a byte stream.
117pub async fn write_packet_to_io<W>(writer: &mut W, packet: TransportPacket) -> AgentClientResult<()>
118where
119 W: AsyncWrite + Unpin,
120{
121 writer.write_all(packet.as_bytes()).await?;
122 writer.flush().await?;
123 Ok(())
124}