Skip to main content

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}