#![cfg_attr(
not(any(windows, test)),
expect(
dead_code,
reason = "named-pipe framing is Windows transport plus tests"
)
)]
use std::mem::size_of;
use crate::SessionId;
use crate::constants::MAX_FRAME_LEN;
use crate::durability::LauncherTie;
use crate::pal::pseudoconsole::WindowSize;
pub(crate) const PROTOCOL_VERSION: u32 = 1;
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub(crate) enum Message {
Attach {
size: WindowSize,
},
Input(Vec<u8>),
Resize {
size: WindowSize,
},
Attached {
session_id: SessionId,
},
Output(Vec<u8>),
Displaced,
AppExited {
status: i32,
},
StartupOk {
session_id: SessionId,
launcher_tie: LauncherTie,
pipe_name: String,
},
StartupErr {
step: StartupStep,
},
StartupCommit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum StartupStep {
LifetimeJob,
Pseudoconsole,
App,
Listener,
Identity,
SessionId,
PublishRecord,
}
impl StartupStep {
pub(crate) fn describe(self) -> &'static str {
match self {
Self::LifetimeJob => "creating the session's lifetime job",
Self::Pseudoconsole => "creating the session's console",
Self::App => "starting the app",
Self::Listener => "opening the session pipe",
Self::Identity => "reading the supervisor's process identity",
Self::SessionId => "reserving a session id",
Self::PublishRecord => "publishing the session record",
}
}
}
const STEP_LIFETIME_JOB: u8 = 1;
const STEP_PSEUDOCONSOLE: u8 = 2;
const STEP_APP: u8 = 3;
const STEP_LISTENER: u8 = 4;
const STEP_IDENTITY: u8 = 5;
const STEP_SESSION_ID: u8 = 6;
const STEP_PUBLISH_RECORD: u8 = 7;
const LAUNCHER_TIE_NONE_DETECTED: u8 = 1;
const LAUNCHER_TIE_CONFIRMED: u8 = 2;
const LAUNCHER_TIE_UNKNOWN: u8 = 3;
const KIND_ATTACH: u8 = 1;
const KIND_INPUT: u8 = 2;
const KIND_RESIZE: u8 = 3;
const KIND_ATTACHED: u8 = 4;
const KIND_OUTPUT: u8 = 5;
const KIND_DISPLACED: u8 = 6;
const KIND_APP_EXITED: u8 = 7;
const KIND_STARTUP_OK: u8 = 8;
const KIND_STARTUP_ERR: u8 = 9;
const KIND_STARTUP_COMMIT: u8 = 10;
#[must_use]
pub(crate) fn encode(message: &Message) -> Vec<u8> {
let mut frame = vec![0_u8; size_of::<u32>()];
let payload = &mut frame;
match message {
Message::Attach { size } => {
payload.push(KIND_ATTACH);
payload.extend_from_slice(&size.cols.get().to_le_bytes());
payload.extend_from_slice(&size.rows.get().to_le_bytes());
}
Message::Input(data) => {
payload.push(KIND_INPUT);
payload.extend_from_slice(data);
}
Message::Resize { size } => {
payload.push(KIND_RESIZE);
payload.extend_from_slice(&size.cols.get().to_le_bytes());
payload.extend_from_slice(&size.rows.get().to_le_bytes());
}
Message::Attached { session_id } => {
payload.push(KIND_ATTACHED);
payload.extend_from_slice(&session_id.get().to_le_bytes());
}
Message::Output(data) => {
payload.push(KIND_OUTPUT);
payload.extend_from_slice(data);
}
Message::Displaced => payload.push(KIND_DISPLACED),
Message::AppExited { status } => {
payload.push(KIND_APP_EXITED);
payload.extend_from_slice(&status.to_le_bytes());
}
Message::StartupOk {
session_id,
launcher_tie,
pipe_name,
} => {
payload.push(KIND_STARTUP_OK);
payload.extend_from_slice(&session_id.get().to_le_bytes());
payload.push(match *launcher_tie {
LauncherTie::NoneDetected => LAUNCHER_TIE_NONE_DETECTED,
LauncherTie::Confirmed => LAUNCHER_TIE_CONFIRMED,
LauncherTie::Unknown => LAUNCHER_TIE_UNKNOWN,
});
payload.extend_from_slice(pipe_name.as_bytes());
}
Message::StartupErr { step } => {
payload.push(KIND_STARTUP_ERR);
payload.push(match *step {
StartupStep::LifetimeJob => STEP_LIFETIME_JOB,
StartupStep::Pseudoconsole => STEP_PSEUDOCONSOLE,
StartupStep::App => STEP_APP,
StartupStep::Listener => STEP_LISTENER,
StartupStep::Identity => STEP_IDENTITY,
StartupStep::SessionId => STEP_SESSION_ID,
StartupStep::PublishRecord => STEP_PUBLISH_RECORD,
});
}
Message::StartupCommit => payload.push(KIND_STARTUP_COMMIT),
}
let len = u32::try_from(
frame
.len()
.checked_sub(size_of::<u32>())
.expect("the length prefix was reserved above"),
)
.expect("frame payload fits in u32");
let (prefix, _) = frame
.split_at_mut_checked(size_of::<u32>())
.expect("the length prefix was reserved above");
prefix.copy_from_slice(&len.to_le_bytes());
frame
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum DecodeError {
Invalid,
}
pub(crate) fn decode_body(kind: u8, body: Vec<u8>) -> Result<Message, DecodeError> {
match kind {
KIND_INPUT => Ok(Message::Input(body)),
KIND_OUTPUT => Ok(Message::Output(body)),
_ => decode_rest(kind, &body),
}
}
fn decode_rest(kind: u8, rest: &[u8]) -> Result<Message, DecodeError> {
match kind {
KIND_ATTACH => decode_size(rest).map(|size| Message::Attach { size }),
KIND_RESIZE => decode_size(rest).map(|size| Message::Resize { size }),
KIND_ATTACHED => decode_session_id(rest).map(|session_id| Message::Attached { session_id }),
KIND_DISPLACED if rest.is_empty() => Ok(Message::Displaced),
KIND_APP_EXITED => decode_i32(rest).map(|status| Message::AppExited { status }),
KIND_STARTUP_OK => {
let Some((id_bytes, rest)) = rest.split_first_chunk::<{ size_of::<u32>() }>() else {
return Err(DecodeError::Invalid);
};
let Some((launcher_tie, pipe_name)) = rest.split_first() else {
return Err(DecodeError::Invalid);
};
let launcher_tie = match *launcher_tie {
LAUNCHER_TIE_NONE_DETECTED => LauncherTie::NoneDetected,
LAUNCHER_TIE_CONFIRMED => LauncherTie::Confirmed,
LAUNCHER_TIE_UNKNOWN => LauncherTie::Unknown,
_ => return Err(DecodeError::Invalid),
};
let Ok(pipe_name) = str::from_utf8(pipe_name) else {
return Err(DecodeError::Invalid);
};
decode_session_id(id_bytes).map(|session_id| Message::StartupOk {
session_id,
launcher_tie,
pipe_name: pipe_name.to_string(),
})
}
KIND_STARTUP_ERR => {
let [step] = rest else {
return Err(DecodeError::Invalid);
};
let step = match *step {
STEP_LIFETIME_JOB => StartupStep::LifetimeJob,
STEP_PSEUDOCONSOLE => StartupStep::Pseudoconsole,
STEP_APP => StartupStep::App,
STEP_LISTENER => StartupStep::Listener,
STEP_IDENTITY => StartupStep::Identity,
STEP_SESSION_ID => StartupStep::SessionId,
STEP_PUBLISH_RECORD => StartupStep::PublishRecord,
_ => return Err(DecodeError::Invalid),
};
Ok(Message::StartupErr { step })
}
KIND_STARTUP_COMMIT if rest.is_empty() => Ok(Message::StartupCommit),
_ => Err(DecodeError::Invalid),
}
}
#[must_use]
pub(crate) fn payload_len_ok(len: u32) -> bool {
len > 0 && len <= MAX_FRAME_LEN
}
fn decode_size(rest: &[u8]) -> Result<WindowSize, DecodeError> {
let (cols_bytes, rest) = rest.split_at_checked(2).ok_or(DecodeError::Invalid)?;
let (rows_bytes, rest) = rest.split_at_checked(2).ok_or(DecodeError::Invalid)?;
if !rest.is_empty() {
return Err(DecodeError::Invalid);
}
let cols = u16::from_le_bytes(
cols_bytes
.try_into()
.map_err(|_error| DecodeError::Invalid)?,
);
let rows = u16::from_le_bytes(
rows_bytes
.try_into()
.map_err(|_error| DecodeError::Invalid)?,
);
WindowSize::new(cols, rows).ok_or(DecodeError::Invalid)
}
fn decode_u32(rest: &[u8]) -> Result<u32, DecodeError> {
let (bytes, rest) = rest.split_at_checked(4).ok_or(DecodeError::Invalid)?;
if !rest.is_empty() {
return Err(DecodeError::Invalid);
}
Ok(u32::from_le_bytes(
bytes.try_into().map_err(|_error| DecodeError::Invalid)?,
))
}
fn decode_session_id(rest: &[u8]) -> Result<SessionId, DecodeError> {
SessionId::from_u32(decode_u32(rest)?).ok_or(DecodeError::Invalid)
}
fn decode_i32(rest: &[u8]) -> Result<i32, DecodeError> {
Ok(i32::from_le_bytes(decode_u32(rest)?.to_le_bytes()))
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use crate::pal::pseudoconsole::MAX_DIMENSION;
fn decode_payload(payload: &[u8]) -> Result<Message, DecodeError> {
let Some((kind, rest)) = payload.split_first() else {
return Err(DecodeError::Invalid);
};
decode_body(*kind, rest.to_vec())
}
#[test]
fn round_trips_each_kind() {
let id = SessionId::MIN;
let messages = [
Message::Attach {
size: WindowSize::new(80, 24).expect("a fixture size is not empty"),
},
Message::Input(b"hi".to_vec()),
Message::Resize {
size: WindowSize::new(120, 30).expect("a fixture size is not empty"),
},
Message::Attached { session_id: id },
Message::Output(b"out".to_vec()),
Message::Displaced,
Message::AppExited { status: 7 },
Message::StartupOk {
session_id: id,
launcher_tie: LauncherTie::NoneDetected,
pipe_name: "\\\\.\\pipe\\dure-abc".to_string(),
},
Message::StartupOk {
session_id: id,
launcher_tie: LauncherTie::Confirmed,
pipe_name: "\\\\.\\pipe\\dure-abc".to_string(),
},
Message::StartupOk {
session_id: id,
launcher_tie: LauncherTie::Unknown,
pipe_name: "\\\\.\\pipe\\dure-abc".to_string(),
},
Message::StartupErr {
step: StartupStep::App,
},
Message::StartupCommit,
];
for message in messages {
let frame = encode(&message);
let (header, payload) = frame
.split_first_chunk::<4>()
.expect("encode always writes a length prefix");
let len = u32::from_le_bytes(*header);
assert!(payload_len_ok(len));
assert_eq!(payload.len(), len as usize);
assert_eq!(decode_payload(payload).unwrap(), message);
}
}
#[test]
fn rejects_empty_payload() {
assert_eq!(decode_payload(&[]).unwrap_err(), DecodeError::Invalid);
}
#[test]
fn rejects_zero_session_id() {
let mut payload = vec![KIND_ATTACHED];
payload.extend_from_slice(&0_u32.to_le_bytes());
assert_eq!(decode_payload(&payload).unwrap_err(), DecodeError::Invalid);
}
#[test]
fn payload_len_rejects_zero_and_over_cap() {
assert!(!payload_len_ok(0));
assert!(!payload_len_ok(MAX_FRAME_LEN.saturating_add(1)));
assert!(payload_len_ok(1));
assert!(payload_len_ok(64 * 1024));
assert!(payload_len_ok(MAX_FRAME_LEN));
}
#[test]
fn empty_messages_reject_trailing_bytes() {
assert_eq!(
decode_payload(&[KIND_DISPLACED, 0]).unwrap_err(),
DecodeError::Invalid
);
assert_eq!(
decode_payload(&[KIND_STARTUP_COMMIT, 1]).unwrap_err(),
DecodeError::Invalid
);
}
#[test]
fn a_startup_failure_names_a_step_this_build_assigned() {
assert_eq!(
decode_payload(&[KIND_STARTUP_ERR]).unwrap_err(),
DecodeError::Invalid
);
assert_eq!(
decode_payload(&[KIND_STARTUP_ERR, STEP_APP, 0]).unwrap_err(),
DecodeError::Invalid
);
assert_eq!(
decode_payload(&[KIND_STARTUP_ERR, 0]).unwrap_err(),
DecodeError::Invalid
);
}
const EVERY_STEP: [StartupStep; 7] = [
StartupStep::LifetimeJob,
StartupStep::Pseudoconsole,
StartupStep::App,
StartupStep::Listener,
StartupStep::Identity,
StartupStep::SessionId,
StartupStep::PublishRecord,
];
#[test]
fn every_startup_step_survives_the_wire() {
for step in EVERY_STEP {
let frame = encode(&Message::StartupErr { step });
let (_, payload) = frame
.split_first_chunk::<4>()
.expect("a frame carries its length");
assert_eq!(
decode_payload(payload).unwrap(),
Message::StartupErr { step },
"{step:?} must arrive as itself"
);
}
}
#[test]
fn each_startup_step_says_something_of_its_own() {
let described: Vec<&str> = EVERY_STEP.iter().map(|step| step.describe()).collect();
for (at, description) in described.iter().enumerate() {
assert!(
!description.is_empty(),
"{:?} must describe itself",
EVERY_STEP.get(at)
);
assert_eq!(
described
.iter()
.filter(|other| *other == description)
.count(),
1,
"{description:?} is used for more than one step"
);
}
assert_eq!(StartupStep::App.describe(), "starting the app");
}
#[test]
fn startup_ok_rejects_a_missing_or_unassigned_launcher_tie() {
let id = SessionId::MIN;
let mut without_tie = vec![KIND_STARTUP_OK];
without_tie.extend_from_slice(&id.get().to_le_bytes());
assert_eq!(
decode_payload(&without_tie).unwrap_err(),
DecodeError::Invalid
);
let mut unassigned_tie = without_tie.clone();
unassigned_tie.push(0);
assert_eq!(
decode_payload(&unassigned_tie).unwrap_err(),
DecodeError::Invalid
);
assert_eq!(
decode_payload(&[KIND_STARTUP_OK]).unwrap_err(),
DecodeError::Invalid
);
}
#[test]
fn startup_ok_rejects_a_pipe_name_that_is_not_utf8() {
let mut payload = vec![KIND_STARTUP_OK];
payload.extend_from_slice(&SessionId::MIN.get().to_le_bytes());
payload.push(LAUNCHER_TIE_NONE_DETECTED);
payload.push(0xFF);
assert_eq!(decode_payload(&payload).unwrap_err(), DecodeError::Invalid);
}
#[test]
fn a_size_no_console_could_have_is_refused() {
let too_large = MAX_DIMENSION.saturating_add(1);
for (cols, rows) in [
(0_u16, 24_u16),
(80, 0),
(0, 0),
(too_large, 24),
(80, too_large),
] {
let mut attach = vec![KIND_ATTACH];
attach.extend_from_slice(&cols.to_le_bytes());
attach.extend_from_slice(&rows.to_le_bytes());
assert_eq!(
decode_payload(&attach).unwrap_err(),
DecodeError::Invalid,
"{cols}x{rows} is not a console"
);
}
}
#[test]
fn sized_and_numeric_payloads_reject_trailing_bytes() {
let mut attach = vec![KIND_ATTACH];
attach.extend_from_slice(&80_u16.to_le_bytes());
attach.extend_from_slice(&24_u16.to_le_bytes());
attach.push(0);
assert_eq!(decode_payload(&attach).unwrap_err(), DecodeError::Invalid);
let mut attached = vec![KIND_ATTACHED];
attached.extend_from_slice(&1_u32.to_le_bytes());
attached.push(0);
assert_eq!(decode_payload(&attached).unwrap_err(), DecodeError::Invalid);
}
}