use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::error::ProtocolResult;
pub const PROTOCOL_VERSION: u8 = 5;
pub const FLAG_TERMINAL: u8 = 0b0000_0001;
pub const FLAG_SESSION_START: u8 = 0b0000_0010;
pub const FLAG_SHUTDOWN: u8 = 0b0000_0100;
pub const FRAME_HEADER_SIZE: usize = 5;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub v: u8,
pub t: MessageType,
#[serde(skip)]
pub id: u32,
#[serde(skip)]
pub flags: u8,
#[serde(with = "serde_bytes")]
pub p: Vec<u8>,
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
strum::IntoStaticStr,
strum::EnumString,
strum::EnumIter,
)]
pub enum MessageType {
#[strum(serialize = "core.ready")]
Ready,
#[strum(serialize = "core.init.resolved")]
InitResolved,
#[strum(serialize = "core.init.ack")]
InitAck,
#[strum(serialize = "core.shutdown")]
Shutdown,
#[strum(serialize = "core.relay.client.disconnected")]
RelayClientDisconnected,
#[strum(serialize = "core.clock.sync")]
ClockSync,
#[strum(serialize = "core.error")]
CoreError,
#[strum(serialize = "core.exec.request")]
ExecRequest,
#[strum(serialize = "core.exec.started")]
ExecStarted,
#[strum(serialize = "core.exec.stdin")]
ExecStdin,
#[strum(serialize = "core.exec.stdin.error")]
ExecStdinError,
#[strum(serialize = "core.exec.stdout")]
ExecStdout,
#[strum(serialize = "core.exec.stderr")]
ExecStderr,
#[strum(serialize = "core.exec.exited")]
ExecExited,
#[strum(serialize = "core.exec.failed")]
ExecFailed,
#[strum(serialize = "core.exec.resize")]
ExecResize,
#[strum(serialize = "core.exec.signal")]
ExecSignal,
#[strum(serialize = "core.fs.request")]
FsRequest,
#[strum(serialize = "core.fs.response")]
FsResponse,
#[strum(serialize = "core.fs.data")]
FsData,
#[strum(serialize = "core.tcp.connect")]
TcpConnect,
#[strum(serialize = "core.tcp.connected")]
TcpConnected,
#[strum(serialize = "core.tcp.data")]
TcpData,
#[strum(serialize = "core.tcp.eof")]
TcpEof,
#[strum(serialize = "core.tcp.close")]
TcpClose,
#[strum(serialize = "core.tcp.closed")]
TcpClosed,
#[strum(serialize = "core.tcp.failed")]
TcpFailed,
}
impl Message {
pub fn new(t: MessageType, id: u32, p: Vec<u8>) -> Self {
let flags = t.flags();
Self {
v: PROTOCOL_VERSION,
t,
id,
flags,
p,
}
}
pub fn with_payload<T: Serialize>(
t: MessageType,
id: u32,
payload: &T,
) -> ProtocolResult<Self> {
let mut p = Vec::new();
ciborium::into_writer(payload, &mut p)?;
let flags = t.flags();
Ok(Self {
v: PROTOCOL_VERSION,
t,
id,
flags,
p,
})
}
pub fn payload<T: DeserializeOwned>(&self) -> ProtocolResult<T> {
Ok(ciborium::from_reader(&self.p[..])?)
}
}
impl MessageType {
pub fn flags(&self) -> u8 {
match self {
Self::CoreError
| Self::ExecExited
| Self::ExecFailed
| Self::FsResponse
| Self::TcpClosed
| Self::TcpFailed => FLAG_TERMINAL,
Self::ExecRequest | Self::FsRequest | Self::TcpConnect => FLAG_SESSION_START,
Self::Shutdown => FLAG_SHUTDOWN,
_ => 0,
}
}
pub fn min_protocol_version(&self) -> u8 {
match self {
Self::Ready
| Self::InitResolved
| Self::InitAck
| Self::Shutdown
| Self::RelayClientDisconnected
| Self::ClockSync
| Self::ExecRequest
| Self::ExecStarted
| Self::ExecStdin
| Self::ExecStdinError
| Self::ExecStdout
| Self::ExecStderr
| Self::ExecExited
| Self::ExecFailed
| Self::ExecResize
| Self::ExecSignal => 1,
Self::FsRequest | Self::FsResponse | Self::FsData => 2,
Self::CoreError => 5,
Self::TcpConnect
| Self::TcpConnected
| Self::TcpData
| Self::TcpEof
| Self::TcpClose
| Self::TcpClosed
| Self::TcpFailed => 4,
}
}
pub fn is_available_at(&self, peer_generation: u8) -> bool {
self.min_protocol_version() <= peer_generation
}
pub fn as_str(&self) -> &'static str {
(*self).into()
}
pub fn from_wire_str(s: &str) -> Option<Self> {
s.parse().ok()
}
}
impl Serialize for MessageType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for MessageType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_wire_str(&s)
.ok_or_else(|| serde::de::Error::custom(format!("unknown message type: {s}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_type_roundtrip() {
let types = [
(MessageType::Ready, "core.ready"),
(MessageType::InitResolved, "core.init.resolved"),
(MessageType::InitAck, "core.init.ack"),
(MessageType::Shutdown, "core.shutdown"),
(
MessageType::RelayClientDisconnected,
"core.relay.client.disconnected",
),
(MessageType::ClockSync, "core.clock.sync"),
(MessageType::CoreError, "core.error"),
(MessageType::ExecRequest, "core.exec.request"),
(MessageType::ExecStarted, "core.exec.started"),
(MessageType::ExecStdin, "core.exec.stdin"),
(MessageType::ExecStdinError, "core.exec.stdin.error"),
(MessageType::ExecStdout, "core.exec.stdout"),
(MessageType::ExecStderr, "core.exec.stderr"),
(MessageType::ExecExited, "core.exec.exited"),
(MessageType::ExecFailed, "core.exec.failed"),
(MessageType::ExecResize, "core.exec.resize"),
(MessageType::ExecSignal, "core.exec.signal"),
(MessageType::FsRequest, "core.fs.request"),
(MessageType::FsResponse, "core.fs.response"),
(MessageType::FsData, "core.fs.data"),
(MessageType::TcpConnect, "core.tcp.connect"),
(MessageType::TcpConnected, "core.tcp.connected"),
(MessageType::TcpData, "core.tcp.data"),
(MessageType::TcpEof, "core.tcp.eof"),
(MessageType::TcpClose, "core.tcp.close"),
(MessageType::TcpClosed, "core.tcp.closed"),
(MessageType::TcpFailed, "core.tcp.failed"),
];
for (mt, expected_str) in &types {
assert_eq!(mt.as_str(), *expected_str);
assert_eq!(MessageType::from_wire_str(expected_str).unwrap(), *mt);
}
}
#[test]
fn test_message_type_serde_roundtrip() {
let types = [
MessageType::Ready,
MessageType::InitResolved,
MessageType::InitAck,
MessageType::Shutdown,
MessageType::RelayClientDisconnected,
MessageType::ClockSync,
MessageType::CoreError,
MessageType::ExecRequest,
MessageType::ExecStarted,
MessageType::ExecStdin,
MessageType::ExecStdinError,
MessageType::ExecStdout,
MessageType::ExecStderr,
MessageType::ExecExited,
MessageType::ExecFailed,
MessageType::ExecResize,
MessageType::ExecSignal,
MessageType::FsRequest,
MessageType::FsResponse,
MessageType::FsData,
MessageType::TcpConnect,
MessageType::TcpConnected,
MessageType::TcpData,
MessageType::TcpEof,
MessageType::TcpClose,
MessageType::TcpClosed,
MessageType::TcpFailed,
];
for mt in &types {
let mut buf = Vec::new();
ciborium::into_writer(mt, &mut buf).unwrap();
let decoded: MessageType = ciborium::from_reader(&buf[..]).unwrap();
assert_eq!(&decoded, mt);
}
}
#[test]
fn test_unknown_message_type() {
assert!(MessageType::from_wire_str("core.unknown").is_none());
}
#[test]
fn test_message_with_payload_roundtrip() {
use crate::exec::ExecExited;
let msg =
Message::with_payload(MessageType::ExecExited, 7, &ExecExited { code: 42 }).unwrap();
assert_eq!(msg.t, MessageType::ExecExited);
assert_eq!(msg.id, 7);
assert_eq!(msg.flags, FLAG_TERMINAL);
let payload: ExecExited = msg.payload().unwrap();
assert_eq!(payload.code, 42);
}
#[test]
fn test_message_type_flags() {
assert_eq!(MessageType::ExecExited.flags(), FLAG_TERMINAL);
assert_eq!(MessageType::ExecFailed.flags(), FLAG_TERMINAL);
assert_eq!(MessageType::FsResponse.flags(), FLAG_TERMINAL);
assert_eq!(MessageType::TcpClosed.flags(), FLAG_TERMINAL);
assert_eq!(MessageType::TcpFailed.flags(), FLAG_TERMINAL);
assert_eq!(MessageType::ExecRequest.flags(), FLAG_SESSION_START);
assert_eq!(MessageType::FsRequest.flags(), FLAG_SESSION_START);
assert_eq!(MessageType::TcpConnect.flags(), FLAG_SESSION_START);
assert_eq!(MessageType::Ready.flags(), 0);
assert_eq!(MessageType::InitResolved.flags(), 0);
assert_eq!(MessageType::InitAck.flags(), 0);
assert_eq!(MessageType::Shutdown.flags(), FLAG_SHUTDOWN);
assert_eq!(MessageType::ClockSync.flags(), 0);
assert_eq!(MessageType::ExecStarted.flags(), 0);
assert_eq!(MessageType::ExecStdin.flags(), 0);
assert_eq!(MessageType::ExecStdout.flags(), 0);
assert_eq!(MessageType::ExecStderr.flags(), 0);
assert_eq!(MessageType::ExecResize.flags(), 0);
assert_eq!(MessageType::ExecSignal.flags(), 0);
assert_eq!(MessageType::FsData.flags(), 0);
assert_eq!(MessageType::TcpConnected.flags(), 0);
assert_eq!(MessageType::TcpData.flags(), 0);
assert_eq!(MessageType::TcpEof.flags(), 0);
assert_eq!(MessageType::TcpClose.flags(), 0);
}
#[test]
fn test_additive_fields_keep_old_and_new_compatible() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Old {
a: u32,
b: u32,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct New {
a: u32,
b: u32,
#[serde(default)]
c: u32,
}
let mut new_bytes = Vec::new();
ciborium::into_writer(&New { a: 1, b: 2, c: 3 }, &mut new_bytes).unwrap();
let as_old: Old = ciborium::from_reader(&new_bytes[..]).unwrap();
assert_eq!((as_old.a, as_old.b), (1, 2));
let mut old_bytes = Vec::new();
ciborium::into_writer(&Old { a: 1, b: 2 }, &mut old_bytes).unwrap();
let as_new: New = ciborium::from_reader(&old_bytes[..]).unwrap();
assert_eq!(as_new, New { a: 1, b: 2, c: 0 });
}
#[test]
fn test_is_available_at() {
assert!(MessageType::ExecRequest.is_available_at(1));
assert!(MessageType::ExecRequest.is_available_at(2));
assert!(MessageType::ExecRequest.is_available_at(PROTOCOL_VERSION));
assert!(!MessageType::FsRequest.is_available_at(1));
assert!(MessageType::FsRequest.is_available_at(2));
assert!(MessageType::FsRequest.is_available_at(PROTOCOL_VERSION));
}
#[test]
fn test_min_protocol_version_per_type() {
let baseline = [
MessageType::Ready,
MessageType::InitResolved,
MessageType::InitAck,
MessageType::Shutdown,
MessageType::RelayClientDisconnected,
MessageType::ClockSync,
MessageType::ExecRequest,
MessageType::ExecStarted,
MessageType::ExecStdin,
MessageType::ExecStdinError,
MessageType::ExecStdout,
MessageType::ExecStderr,
MessageType::ExecExited,
MessageType::ExecFailed,
MessageType::ExecResize,
MessageType::ExecSignal,
];
for mt in &baseline {
assert_eq!(mt.min_protocol_version(), 1, "{mt:?} should be v1 baseline");
}
for mt in [
MessageType::FsRequest,
MessageType::FsResponse,
MessageType::FsData,
] {
assert_eq!(mt.min_protocol_version(), 2, "{mt:?} should require gen 2");
}
assert!(MessageType::FsRequest.min_protocol_version() <= PROTOCOL_VERSION);
}
#[test]
fn test_message_new_computes_flags() {
let msg = Message::new(MessageType::ExecRequest, 1, Vec::new());
assert_eq!(msg.flags, FLAG_SESSION_START);
let msg = Message::new(MessageType::ExecStdout, 1, Vec::new());
assert_eq!(msg.flags, 0);
}
}