use std::sync::Arc;
use microsandbox_protocol::{
codec::{self, MAX_FRAME_SIZE, RawFrame},
core::Ready,
message::{FRAME_HEADER_SIZE, MessageType, PROTOCOL_VERSION},
};
use microsandbox_protocol_client::{
BoxFuture, BoxTransport, CborEnvelopeCodec, ClientError, ClientResult, ConnectOptions,
ErrorKind, Established, IdRange, Protocol, SendMetadata,
};
use tokio::io::AsyncReadExt;
pub const LEGACY_PROTOCOL_VERSION: u8 = 1;
const LEGACY_RELAY_ID_RANGE_STEP: u32 = u32::MAX / 16;
pub struct AgentProtocol;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentWireFormat {
Current,
LegacyV1,
}
#[derive(Debug, Clone)]
pub struct AgentReady {
pub wire_format: AgentWireFormat,
pub negotiated_version: u8,
pub agent: Ready,
ready_body: Vec<u8>,
}
impl AgentWireFormat {
pub fn version(self) -> u8 {
match self {
Self::Current => PROTOCOL_VERSION,
Self::LegacyV1 => LEGACY_PROTOCOL_VERSION,
}
}
}
impl AgentReady {
pub fn ready_bytes(&self) -> &[u8] {
&self.ready_body
}
pub fn supports(&self, message_type: MessageType) -> bool {
message_type.is_available_at(self.negotiated_version)
}
pub fn agent_version(&self) -> &str {
&self.agent.agent_version
}
pub fn is_legacy_protocol(&self) -> bool {
self.wire_format == AgentWireFormat::LegacyV1
}
}
impl AgentProtocol {
pub fn ensure_version_compat_for(
message_type: MessageType,
negotiated: u8,
) -> ClientResult<()> {
if message_type.is_available_at(negotiated) {
Ok(())
} else {
Err(ClientError::new(ErrorKind::UnsupportedOperation))
}
}
}
impl Protocol for AgentProtocol {
const REUSE_IDS: bool = false;
type Ready = AgentReady;
fn establish(
mut stream: BoxTransport,
options: ConnectOptions,
) -> BoxFuture<'static, ClientResult<Established<AgentReady>>> {
Box::pin(async move {
let mut prologue = [0u8; 8];
stream.read_exact(&mut prologue).await?;
let first = u32::from_be_bytes(prologue[..4].try_into().unwrap());
let second = u32::from_be_bytes(prologue[4..].try_into().unwrap());
let legacy = (FRAME_HEADER_SIZE as u32..=MAX_FRAME_SIZE).contains(&second)
&& (first == 0 || first >= second);
let (ids, frame, wire_format) = if legacy {
let frame = read_after_prefix(&mut stream, second).await?;
(
IdRange {
start: first.saturating_add(1),
end_exclusive: first.saturating_add(LEGACY_RELAY_ID_RANGE_STEP).into(),
},
frame,
AgentWireFormat::LegacyV1,
)
} else {
let ids = IdRange {
start: first.max(1),
end_exclusive: second.into(),
};
ids.validate()?;
let frame = codec::read_raw_frame(&mut stream)
.await
.map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
(ids, frame, AgentWireFormat::Current)
};
ids.validate()?;
let ready_message = codec::raw_frame_to_message(frame.clone())
.map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
if ready_message.t != MessageType::Ready {
return Err(ClientError::new(ErrorKind::InvalidData));
}
let agent = ready_message
.payload::<Ready>()
.map_err(|_| ClientError::new(ErrorKind::InvalidData))?;
let ready = AgentReady {
wire_format,
negotiated_version: wire_format.version().min(ready_message.v),
agent,
ready_body: frame.body,
};
if ready.is_legacy_protocol() {
tracing::warn!(
"agent client: legacy pre-0.5 exec protocol; filesystem operations require a newer agent"
);
}
Ok(Established {
transport: stream,
codec: Arc::new(CborEnvelopeCodec),
ids,
ready,
limits: options.limits,
})
})
}
fn prepare(ready: &AgentReady, wire_name: &str) -> ClientResult<SendMetadata> {
let flags = match MessageType::from_wire_str(wire_name) {
Some(message_type) => {
Self::ensure_version_compat_for(message_type, ready.negotiated_version)?;
message_type.flags()
}
None => 0,
};
Ok(SendMetadata {
generation: ready.wire_format.version(),
flags,
})
}
}
async fn read_after_prefix(stream: &mut BoxTransport, length: u32) -> ClientResult<RawFrame> {
if !(FRAME_HEADER_SIZE as u32..=MAX_FRAME_SIZE).contains(&length) {
return Err(ClientError::new(ErrorKind::InvalidData));
}
let mut header = [0u8; FRAME_HEADER_SIZE];
stream.read_exact(&mut header).await?;
let mut body = vec![0; length as usize - FRAME_HEADER_SIZE];
stream.read_exact(&mut body).await?;
Ok(RawFrame {
id: u32::from_be_bytes(header[..4].try_into().unwrap()),
flags: header[4],
body,
})
}