rmux-proto 0.10.0

RMUX detached IPC protocol DTOs, framing, and wire-safe error types.
Documentation
#![deny(missing_docs)]
#![forbid(unsafe_code)]

//! Shared detached protocol types for RMUX.

pub mod attach;
pub mod capabilities;
pub mod codec;
pub mod control;
pub mod envelope;
pub mod error;
pub mod frame_kind;
pub mod identity;
pub mod request;
pub mod response;
pub mod types;

pub use attach::{
    decode_attach_data_frame, decode_attach_data_frame_with_limit, encode_attach_data,
    encode_attach_data_into_slice, encode_attach_message, AttachDataFrame, AttachFrameDecoder,
    AttachMessage, AttachShellCommand, AttachedKeystroke, AttachedWindowsConsoleKey, KeyDispatched,
    ATTACH_DATA_HEADER_LEN,
};
pub use capabilities::{
    capabilities_for_features, HandshakeRequest, HandshakeResponse, CAPABILITY_ATTACH_RENDER,
    CAPABILITY_ATTACH_RESIZE_GEOMETRY, CAPABILITY_ATTACH_STREAM,
    CAPABILITY_ATTACH_WINDOWS_CONSOLE_KEY, CAPABILITY_CLI_CAPTURE_TARGET_ACTION,
    CAPABILITY_CLI_LIST_WINDOWS_ALL_QUEUE, CAPABILITY_CLI_RUNTIME_COMMAND_EXPANSION,
    CAPABILITY_CLI_TARGET_ACTIONS, CAPABILITY_CONTROL_STREAM, CAPABILITY_DAEMON_SHUTDOWN,
    CAPABILITY_DAEMON_SHUTDOWN_IF_IDLE, CAPABILITY_DAEMON_STATUS, CAPABILITY_DETACHED_RPC,
    CAPABILITY_FRAMED_ERRORS, CAPABILITY_HANDSHAKE, CAPABILITY_SDK_OWNED_SESSION_STABLE_IDENTITY,
    CAPABILITY_SDK_PANE_BROADCAST, CAPABILITY_SDK_PANE_BY_ID, CAPABILITY_SDK_PANE_FOREGROUND,
    CAPABILITY_SDK_PANE_OPTIONS, CAPABILITY_SDK_PANE_RAW_RECOVERY,
    CAPABILITY_SDK_PANE_SPLIT_IDENTITY, CAPABILITY_SDK_PANE_STATE_EVENTS,
    CAPABILITY_SDK_PANE_SURFACE_STREAM, CAPABILITY_SDK_PROCESS_COMMAND,
    CAPABILITY_SDK_SESSION_LEASE, CAPABILITY_SDK_SESSION_LEASE_BY_ID,
    CAPABILITY_SDK_SESSION_LEASE_BY_ID_V2, CAPABILITY_SDK_WAITS, CAPABILITY_SDK_WAITS_ARMED,
    CAPABILITY_TARGET_CLIENT_COMMANDS, CAPABILITY_WEB_SHARE, SUPPORTED_CAPABILITIES,
};
#[cfg(feature = "fuzzing")]
pub use codec::fuzz_detached_frame_decoder;
pub use codec::{
    decode_frame, encode_frame, FrameDecoder, DEFAULT_MAX_DETACHED_FRAME_LENGTH,
    DEFAULT_MAX_FRAME_LENGTH,
};
pub use control::{
    format_continue_line, format_exit_line, format_extended_output_line, format_guard_line,
    format_output_line, format_pause_line, octal_escape, ClientTerminalContext, ControlGuardKind,
    ControlMode, ControlModeRequest, ControlModeResponse, CONTROL_BUFFER_HIGH, CONTROL_BUFFER_LOW,
    CONTROL_CONTROL_END, CONTROL_CONTROL_START, CONTROL_MAXIMUM_AGE_MS, CONTROL_STDIN_EOF_MARKER,
    CONTROL_WRITE_MINIMUM, MAX_INITIAL_CONTROL_COMMANDS,
};
pub use envelope::{RMUX_FRAME_MAGIC, RMUX_WIRE_VERSION};
pub use error::{
    RmuxError, OWNED_SESSION_LEASE_LOST_MESSAGE_PREFIX, PANE_STILL_ACTIVE_MESSAGE,
    PROCESS_COMMAND_EMPTY_MESSAGE, SPAWN_FAILED_MESSAGE_PREFIX,
};
pub use frame_kind::{
    frame_kind_for_request, frame_kind_for_response, ledger_entry_for, FrameDirection,
    FrameFeature, FrameKind, FrameLedgerEntry, FrameStatus, V1_FRAME_LEDGER,
};
pub use identity::{PaneId, SessionId, SessionName, WindowId};
pub use request::*;
pub use response::*;
pub use types::*;
pub use types::{
    OptionScopeSelector, PaneOutputSubscriptionId, PaneStateSubscriptionId, SdkWaitId,
    SdkWaitOwnerId,
};

/// Detached request/response protocol revision.
pub const PROTOCOL_VERSION: u16 = RMUX_WIRE_VERSION as u16;

/// Non-filesystem path used by the CLI's internal runtime command
/// canonicalization request. OS argument vectors cannot contain NUL, so a
/// public `source-file` invocation cannot collide with this transport.
pub const INTERNAL_RUNTIME_COMMAND_EXPANSION_PATH: &str = "\0rmux-runtime-command-expansion-v1";

/// Non-filesystem path used to apply parse-time assignments only after the
/// CLI has validated the canonicalized command queue.
pub const INTERNAL_PARSE_TIME_ASSIGNMENTS_PATH: &str = "\0rmux-parse-time-assignments-v1";

/// Non-filesystem path used to execute a command queue that the daemon has
/// already canonicalized. This prevents the public `source-file` parser from
/// applying the current `command-alias` table a second time.
pub const INTERNAL_CANONICAL_COMMAND_EXECUTION_PATH: &str = "\0rmux-canonical-command-execution-v1";

/// Non-filesystem path used for a validated, read-only `list-windows -a`
/// invocation encoded as a single argument vector.
pub const INTERNAL_LIST_WINDOWS_ALL_EXECUTION_PATH: &str = "\0rmux-list-windows-all-execution-v1";

/// Prefix for the CLI's internal stable pane-exit probe carried through the
/// existing `list-panes` format field. OS argument vectors cannot contain NUL,
/// so a public format cannot collide with this transport-only request.
pub const INTERNAL_PANE_EXIT_PROBE_PREFIX: &str = "\0rmux-pane-exit-probe-v1:";

/// Encodes stable session and pane identities for an internal pane-exit probe.
#[must_use]
pub fn encode_internal_pane_exit_probe(session_id: SessionId, pane_id: PaneId) -> String {
    format!(
        "{INTERNAL_PANE_EXIT_PROBE_PREFIX}{}:{}",
        session_id.as_u32(),
        pane_id.as_u32()
    )
}

/// Decodes stable session and pane identities from an internal pane-exit probe.
#[must_use]
pub fn decode_internal_pane_exit_probe(value: &str) -> Option<(SessionId, PaneId)> {
    let payload = value.strip_prefix(INTERNAL_PANE_EXIT_PROBE_PREFIX)?;
    let (session_id, pane_id) = payload.split_once(':')?;
    if pane_id.contains(':') {
        return None;
    }
    Some((
        SessionId::new(session_id.parse().ok()?),
        PaneId::new(pane_id.parse().ok()?),
    ))
}

/// Serializes an already-tokenized command argv for the internal runtime
/// canonicalization request.
pub fn encode_internal_runtime_command_arguments(
    arguments: &[String],
) -> Result<String, serde_json::Error> {
    serde_json::to_string(arguments)
}

/// Deserializes an already-tokenized command argv from the internal runtime
/// canonicalization request.
pub fn decode_internal_runtime_command_arguments(
    payload: &str,
) -> Result<Vec<String>, serde_json::Error> {
    serde_json::from_str(payload)
}

/// Decodes and validates the dedicated read-only `list-windows -a` argument
/// vector. Only the exact non-mutating flags emitted by the CLI are accepted.
#[must_use]
pub fn decode_internal_list_windows_all_arguments(payload: &str) -> Option<Vec<String>> {
    let arguments = decode_internal_runtime_command_arguments(payload).ok()?;
    internal_list_windows_all_arguments_are_valid(&arguments).then_some(arguments)
}

/// Checks the narrow argument-vector schema accepted by the internal
/// all-session window listing path.
#[must_use]
pub fn internal_list_windows_all_arguments_are_valid(arguments: &[String]) -> bool {
    if arguments.first().map(String::as_str) != Some("list-windows") {
        return false;
    }

    let mut seen_flags = 0_u8;
    let mut index = 1;
    while index < arguments.len() {
        let (bit, takes_value) = match arguments[index].as_str() {
            "-a" => (1 << 0, false),
            "-r" => (1 << 1, false),
            "-t" => (1 << 2, true),
            "-F" => (1 << 3, true),
            "-f" => (1 << 4, true),
            "-O" => (1 << 5, true),
            _ => return false,
        };
        if seen_flags & bit != 0 {
            return false;
        }
        seen_flags |= bit;
        index += 1;
        if takes_value {
            if index == arguments.len() {
                return false;
            }
            index += 1;
        }
    }

    seen_flags & 1 != 0
}

/// Minimum daemon-side TTL accepted for owned-session leases.
pub const MIN_SESSION_LEASE_TTL_MILLIS: u64 = 500;

#[cfg(test)]
mod internal_pane_exit_probe_tests {
    use super::*;

    #[test]
    fn pane_exit_probe_round_trips_stable_identities() {
        let encoded = encode_internal_pane_exit_probe(SessionId::new(17), PaneId::new(42));

        assert_eq!(
            decode_internal_pane_exit_probe(&encoded),
            Some((SessionId::new(17), PaneId::new(42)))
        );
    }

    #[test]
    fn pane_exit_probe_rejects_public_and_malformed_formats() {
        for value in [
            "#{pane_id}",
            "\0rmux-pane-exit-probe-v1:1",
            "\0rmux-pane-exit-probe-v1:1:2:3",
            "\0rmux-pane-exit-probe-v1:session:2",
        ] {
            assert_eq!(decode_internal_pane_exit_probe(value), None);
        }
    }

    #[test]
    fn list_windows_all_arguments_accept_only_the_read_only_internal_shape() {
        let valid = vec![
            "list-windows".to_owned(),
            "-a".to_owned(),
            "-t".to_owned(),
            "alpha".to_owned(),
            "-F".to_owned(),
            "#{window_name}".to_owned(),
            "-r".to_owned(),
        ];
        let encoded = encode_internal_runtime_command_arguments(&valid).expect("argv encodes");
        assert_eq!(
            decode_internal_list_windows_all_arguments(&encoded),
            Some(valid)
        );

        for invalid in [
            vec!["list-windows".to_owned()],
            vec!["list-windows".to_owned(), "-aFname".to_owned()],
            vec![
                "list-windows".to_owned(),
                "-a".to_owned(),
                "; kill-server".to_owned(),
            ],
            vec!["kill-server".to_owned(), "-a".to_owned()],
        ] {
            let encoded =
                encode_internal_runtime_command_arguments(&invalid).expect("argv encodes");
            assert_eq!(decode_internal_list_windows_all_arguments(&encoded), None);
        }
    }
}