use serde::{Deserialize, Serialize};
use crate::{RmuxError, RMUX_WIRE_VERSION};
pub const CAPABILITY_DETACHED_RPC: &str = "rpc.detached";
pub const CAPABILITY_HANDSHAKE: &str = "protocol.capabilities";
pub const CAPABILITY_FRAMED_ERRORS: &str = "protocol.framed_errors";
pub const CAPABILITY_ATTACH_STREAM: &str = "stream.attach";
pub const CAPABILITY_ATTACH_RESIZE_GEOMETRY: &str = "stream.attach.resize_geometry";
pub const CAPABILITY_ATTACH_RENDER: &str = "stream.attach.render";
pub const CAPABILITY_ATTACH_WINDOWS_CONSOLE_KEY: &str = "stream.attach.windows_console_key";
pub const CAPABILITY_CONTROL_STREAM: &str = "stream.control";
pub const CAPABILITY_DAEMON_SHUTDOWN: &str = "daemon.shutdown";
pub const CAPABILITY_DAEMON_STATUS: &str = "daemon.status";
pub const CAPABILITY_DAEMON_SHUTDOWN_IF_IDLE: &str = "daemon.shutdown_if_idle";
pub const CAPABILITY_SDK_WAITS: &str = "sdk.waits";
pub const CAPABILITY_SDK_WAITS_ARMED: &str = "sdk.waits.armed";
pub const CAPABILITY_SDK_PANE_BY_ID: &str = "sdk.pane.by_id";
pub const CAPABILITY_SDK_PANE_BROADCAST: &str = "sdk.pane.broadcast";
pub const CAPABILITY_SDK_PANE_OPTIONS: &str = "sdk.pane.options";
pub const CAPABILITY_SDK_PANE_STATE_EVENTS: &str = "sdk.pane.state_events";
pub const CAPABILITY_SDK_PANE_FOREGROUND: &str = "sdk.pane.foreground";
pub const CAPABILITY_SDK_PANE_SPLIT_IDENTITY: &str = "sdk.pane.split_identity";
pub const CAPABILITY_SDK_PANE_RAW_RECOVERY: &str = "sdk.pane.raw_recovery";
pub const CAPABILITY_SDK_PANE_SURFACE_STREAM: &str = "sdk.pane.surface_stream";
pub const CAPABILITY_SDK_SESSION_LEASE: &str = "sdk.session.lease";
pub const CAPABILITY_SDK_SESSION_LEASE_BY_ID: &str = "sdk.session.lease.by_id";
pub const CAPABILITY_SDK_SESSION_LEASE_BY_ID_V2: &str = "sdk.session.lease.by_id.v2";
pub const CAPABILITY_SDK_OWNED_SESSION_STABLE_IDENTITY: &str = "sdk.owned_session.stable_identity";
pub const CAPABILITY_SDK_PROCESS_COMMAND: &str = "sdk.process.command";
pub const CAPABILITY_TARGET_CLIENT_COMMANDS: &str = "commands.target_client";
pub const CAPABILITY_CLI_TARGET_ACTIONS: &str = "commands.cli_target_actions";
pub const CAPABILITY_CLI_CAPTURE_TARGET_ACTION: &str = "commands.cli_capture_target_action";
pub const CAPABILITY_CLI_RUNTIME_COMMAND_EXPANSION: &str = "commands.cli_runtime_command_expansion";
pub const CAPABILITY_CLI_LIST_WINDOWS_ALL_QUEUE: &str = "commands.cli_list_windows_all_queue";
pub const CAPABILITY_WEB_SHARE: &str = "web.share";
pub const SUPPORTED_CAPABILITIES: &[&str] = &[
CAPABILITY_DETACHED_RPC,
CAPABILITY_HANDSHAKE,
CAPABILITY_FRAMED_ERRORS,
CAPABILITY_ATTACH_STREAM,
CAPABILITY_ATTACH_RESIZE_GEOMETRY,
CAPABILITY_ATTACH_RENDER,
CAPABILITY_ATTACH_WINDOWS_CONSOLE_KEY,
CAPABILITY_CONTROL_STREAM,
CAPABILITY_DAEMON_SHUTDOWN,
CAPABILITY_DAEMON_STATUS,
CAPABILITY_DAEMON_SHUTDOWN_IF_IDLE,
CAPABILITY_SDK_WAITS,
CAPABILITY_SDK_WAITS_ARMED,
CAPABILITY_SDK_PANE_BY_ID,
CAPABILITY_SDK_PANE_BROADCAST,
CAPABILITY_SDK_PANE_OPTIONS,
CAPABILITY_SDK_PANE_STATE_EVENTS,
CAPABILITY_SDK_PANE_FOREGROUND,
CAPABILITY_SDK_SESSION_LEASE,
CAPABILITY_SDK_SESSION_LEASE_BY_ID_V2,
CAPABILITY_SDK_OWNED_SESSION_STABLE_IDENTITY,
CAPABILITY_SDK_PROCESS_COMMAND,
CAPABILITY_TARGET_CLIENT_COMMANDS,
CAPABILITY_CLI_TARGET_ACTIONS,
CAPABILITY_CLI_CAPTURE_TARGET_ACTION,
CAPABILITY_CLI_RUNTIME_COMMAND_EXPANSION,
CAPABILITY_CLI_LIST_WINDOWS_ALL_QUEUE,
CAPABILITY_SDK_PANE_SPLIT_IDENTITY,
CAPABILITY_SDK_PANE_RAW_RECOVERY,
CAPABILITY_SDK_PANE_SURFACE_STREAM,
];
#[must_use]
pub fn capabilities_for_features(web_share: bool) -> Vec<&'static str> {
let mut capabilities = SUPPORTED_CAPABILITIES.to_vec();
if web_share {
capabilities.push(CAPABILITY_WEB_SHARE);
}
capabilities
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HandshakeRequest {
pub minimum_wire_version: u32,
pub maximum_wire_version: u32,
pub required_capabilities: Vec<String>,
}
impl HandshakeRequest {
#[must_use]
pub fn current() -> Self {
Self::requiring(std::iter::empty::<&str>())
}
#[must_use]
pub fn requiring<I, S>(required_capabilities: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self {
minimum_wire_version: RMUX_WIRE_VERSION,
maximum_wire_version: RMUX_WIRE_VERSION,
required_capabilities: required_capabilities
.into_iter()
.map(|capability| capability.as_ref().to_owned())
.collect(),
}
}
pub fn validate_against(&self, supported_capabilities: &[&str]) -> Result<(), RmuxError> {
if self.minimum_wire_version > RMUX_WIRE_VERSION
|| self.maximum_wire_version < RMUX_WIRE_VERSION
{
return Err(RmuxError::UnsupportedWireVersion {
got: RMUX_WIRE_VERSION,
minimum: self.minimum_wire_version,
maximum: self.maximum_wire_version,
});
}
if let Some(feature) = self
.required_capabilities
.iter()
.find(|feature| !supported_capabilities.contains(&feature.as_str()))
{
return Err(RmuxError::UnsupportedCapability {
feature: feature.clone(),
supported: supported_capabilities
.iter()
.copied()
.map(str::to_owned)
.collect(),
});
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HandshakeResponse {
pub wire_version: u32,
pub capabilities: Vec<String>,
}
impl HandshakeResponse {
#[must_use]
pub fn current() -> Self {
Self {
wire_version: RMUX_WIRE_VERSION,
capabilities: SUPPORTED_CAPABILITIES
.iter()
.copied()
.map(str::to_owned)
.collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::{
capabilities_for_features, HandshakeRequest, HandshakeResponse, CAPABILITY_ATTACH_RENDER,
CAPABILITY_ATTACH_WINDOWS_CONSOLE_KEY, CAPABILITY_CLI_CAPTURE_TARGET_ACTION,
CAPABILITY_CLI_LIST_WINDOWS_ALL_QUEUE, CAPABILITY_CLI_TARGET_ACTIONS, CAPABILITY_HANDSHAKE,
CAPABILITY_SDK_OWNED_SESSION_STABLE_IDENTITY, CAPABILITY_SDK_PANE_RAW_RECOVERY,
CAPABILITY_SDK_PANE_SPLIT_IDENTITY, CAPABILITY_SDK_PANE_SURFACE_STREAM,
CAPABILITY_SDK_SESSION_LEASE_BY_ID, CAPABILITY_SDK_SESSION_LEASE_BY_ID_V2,
CAPABILITY_SDK_WAITS_ARMED, CAPABILITY_WEB_SHARE,
};
use crate::{RmuxError, RMUX_WIRE_VERSION};
#[test]
fn current_handshake_advertises_attach_stream_capabilities() {
let response = HandshakeResponse::current();
assert!(response
.capabilities
.iter()
.any(|capability| capability == CAPABILITY_ATTACH_RENDER));
assert!(response
.capabilities
.iter()
.any(|capability| capability == CAPABILITY_ATTACH_WINDOWS_CONSOLE_KEY));
}
#[test]
fn current_handshake_advertises_cli_target_action_capabilities() {
let response = HandshakeResponse::current();
for expected in [
CAPABILITY_CLI_TARGET_ACTIONS,
CAPABILITY_CLI_CAPTURE_TARGET_ACTION,
CAPABILITY_CLI_LIST_WINDOWS_ALL_QUEUE,
CAPABILITY_SDK_WAITS_ARMED,
] {
assert!(
response
.capabilities
.iter()
.any(|capability| capability == expected),
"missing capability {expected}"
);
}
}
#[test]
fn current_handshake_versions_session_lease_identity_addressing() {
let response = HandshakeResponse::current();
assert!(!response
.capabilities
.iter()
.any(|capability| capability == CAPABILITY_SDK_SESSION_LEASE_BY_ID));
assert!(response
.capabilities
.iter()
.any(|capability| capability == CAPABILITY_SDK_SESSION_LEASE_BY_ID_V2));
}
#[test]
fn current_handshake_advertises_owned_session_stable_identity() {
let response = HandshakeResponse::current();
assert!(response
.capabilities
.iter()
.any(|capability| capability == CAPABILITY_SDK_OWNED_SESSION_STABLE_IDENTITY));
}
#[test]
fn current_handshake_advertises_atomic_sdk_split_identity() {
let response = HandshakeResponse::current();
assert!(response
.capabilities
.iter()
.any(|capability| capability == CAPABILITY_SDK_PANE_SPLIT_IDENTITY));
}
#[test]
fn current_handshake_advertises_both_pane_stream_projections() {
let response = HandshakeResponse::current();
for expected in [
CAPABILITY_SDK_PANE_RAW_RECOVERY,
CAPABILITY_SDK_PANE_SURFACE_STREAM,
] {
assert!(
response
.capabilities
.iter()
.any(|capability| capability == expected),
"missing capability {expected}"
);
}
}
#[test]
fn optional_web_capability_follows_the_compiled_feature() {
assert!(!capabilities_for_features(false).contains(&CAPABILITY_WEB_SHARE));
assert!(capabilities_for_features(true).contains(&CAPABILITY_WEB_SHARE));
}
#[test]
fn current_handshake_uses_exact_wire_window() {
let request = HandshakeRequest::current();
assert_eq!(request.minimum_wire_version, RMUX_WIRE_VERSION);
assert_eq!(request.maximum_wire_version, RMUX_WIRE_VERSION);
}
#[test]
fn handshake_wire_window_is_advisory_after_envelope_decode() {
let request = HandshakeRequest {
minimum_wire_version: RMUX_WIRE_VERSION.saturating_sub(1),
maximum_wire_version: RMUX_WIRE_VERSION + 1,
required_capabilities: vec![CAPABILITY_HANDSHAKE.to_owned()],
};
request
.validate_against(&[CAPABILITY_HANDSHAKE])
.expect("post-decode compatible wire window should validate");
let future_only = HandshakeRequest {
minimum_wire_version: RMUX_WIRE_VERSION + 1,
maximum_wire_version: RMUX_WIRE_VERSION + 1,
required_capabilities: Vec::new(),
};
assert!(matches!(
future_only.validate_against(&[CAPABILITY_HANDSHAKE]),
Err(RmuxError::UnsupportedWireVersion { .. })
));
}
#[test]
fn required_handshake_capabilities_are_mandatory() {
let request = HandshakeRequest::requiring(["missing.capability"]);
assert!(matches!(
request.validate_against(&[CAPABILITY_HANDSHAKE]),
Err(RmuxError::UnsupportedCapability { feature, .. })
if feature == "missing.capability"
));
}
}