mothership 0.0.100

Process supervisor with HTTP exposure - wrap, monitor, and expose your fleet
Documentation
//! Docking Protocol - Mothership ↔ Ship communication
//!
//! MVP protocol for multiplexed WebSocket connections over Unix sockets.
//!
//! ## Message Types
//!
//! | Code | Name      | Direction | Purpose                          |
//! |------|-----------|-----------|----------------------------------|
//! | 0x01 | Dock      | S→M       | Ship ready, sends version        |
//! | 0x02 | Moored    | M→S       | Docking confirmed                |
//! | 0x10 | Boarding  | M→S       | New connection with metadata     |
//! | 0x11 | Disembark | M→S       | Connection closed                |
//! | 0x12 | Cargo     | Both      | Data payload                     |
//!
//! ## Frame Format
//!
//! ```text
//! ┌─────────┬──────────┬─────────────┐
//! │ Type(1) │ Length(4)│ Payload(N)  │
//! └─────────┴──────────┴─────────────┘
//! ```

mod airlock;

pub use airlock::{DockingConnector, next_conn_id};

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Protocol version
pub const VERSION: u8 = 1;

/// Message type codes
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum MessageType {
    /// Ship announces readiness (Ship → Mothership)
    Dock = 0x01,
    /// Mothership confirms docking (Mothership → Ship)
    Moored = 0x02,
    /// New client connection (Mothership → Ship)
    Boarding = 0x10,
    /// Client disconnected (Mothership → Ship)
    Disembark = 0x11,
    /// Data payload (bidirectional)
    Cargo = 0x12,
}

impl TryFrom<u8> for MessageType {
    type Error = ProtocolError;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            0x01 => Ok(MessageType::Dock),
            0x02 => Ok(MessageType::Moored),
            0x10 => Ok(MessageType::Boarding),
            0x11 => Ok(MessageType::Disembark),
            0x12 => Ok(MessageType::Cargo),
            _ => Err(ProtocolError::UnknownMessageType(value)),
        }
    }
}

/// Protocol errors
#[derive(Debug, thiserror::Error)]
pub enum ProtocolError {
    #[error("Unknown message type: 0x{0:02x}")]
    UnknownMessageType(u8),
    #[error("Invalid frame: {0}")]
    InvalidFrame(String),
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),
}

/// Ship announces readiness to Mothership
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dock {
    pub version: u8,
    pub ship: String,
}

/// Mothership confirms docking
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Moored {
    pub version: u8,
    #[serde(default)]
    pub config: HashMap<String, String>,
}

/// New client connection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Boarding {
    pub conn_id: u32,
    pub path: String,
    pub remote_addr: String,
    pub headers: HashMap<String, String>,
}

/// Client disconnected
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Disembark {
    pub conn_id: u32,
    pub code: u16,
    pub reason: String,
}

/// Data payload
#[derive(Debug, Clone)]
pub struct Cargo {
    pub conn_id: u32,
    pub data: Vec<u8>,
}

/// Encode frame header
pub fn encode_header(msg_type: MessageType, payload_len: u32) -> [u8; 5] {
    let mut header = [0u8; 5];
    header[0] = msg_type as u8;
    header[1..5].copy_from_slice(&payload_len.to_be_bytes());
    header
}

/// Decode frame header
pub fn decode_header(buf: &[u8]) -> Result<(MessageType, usize), ProtocolError> {
    if buf.len() < 5 {
        return Err(ProtocolError::InvalidFrame("Header too short".into()));
    }
    let msg_type = MessageType::try_from(buf[0])?;
    let len = u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]) as usize;
    Ok((msg_type, len))
}

/// Encode Dock message
pub fn encode_dock(dock: &Dock) -> Vec<u8> {
    let payload = serde_json::to_vec(dock).unwrap();
    let header = encode_header(MessageType::Dock, payload.len() as u32);
    [header.as_slice(), &payload].concat()
}

/// Encode Moored message
pub fn encode_moored(moored: &Moored) -> Vec<u8> {
    let payload = serde_json::to_vec(moored).unwrap();
    let header = encode_header(MessageType::Moored, payload.len() as u32);
    [header.as_slice(), &payload].concat()
}

/// Encode Boarding message
pub fn encode_boarding(boarding: &Boarding) -> Vec<u8> {
    let payload = serde_json::to_vec(boarding).unwrap();
    let header = encode_header(MessageType::Boarding, payload.len() as u32);
    [header.as_slice(), &payload].concat()
}

/// Encode Disembark message
pub fn encode_disembark(disembark: &Disembark) -> Vec<u8> {
    let payload = serde_json::to_vec(disembark).unwrap();
    let header = encode_header(MessageType::Disembark, payload.len() as u32);
    [header.as_slice(), &payload].concat()
}

/// Encode Cargo message
pub fn encode_cargo(cargo: &Cargo) -> Vec<u8> {
    // Cargo payload: conn_id (4 bytes) + data
    let payload_len = 4 + cargo.data.len();
    let header = encode_header(MessageType::Cargo, payload_len as u32);
    let mut buf = Vec::with_capacity(5 + payload_len);
    buf.extend_from_slice(&header);
    buf.extend_from_slice(&cargo.conn_id.to_be_bytes());
    buf.extend_from_slice(&cargo.data);
    buf
}

/// Decode Cargo from payload (after header)
pub fn decode_cargo(payload: &[u8]) -> Result<Cargo, ProtocolError> {
    if payload.len() < 4 {
        return Err(ProtocolError::InvalidFrame(
            "Cargo payload too short".into(),
        ));
    }
    let conn_id = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
    let data = payload[4..].to_vec();
    Ok(Cargo { conn_id, data })
}

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

    #[test]
    fn test_header_roundtrip() {
        let header = encode_header(MessageType::Cargo, 1234);
        let (msg_type, len) = decode_header(&header).unwrap();
        assert_eq!(msg_type, MessageType::Cargo);
        assert_eq!(len, 1234);
    }

    #[test]
    fn test_cargo_roundtrip() {
        let cargo = Cargo {
            conn_id: 42,
            data: b"hello docking".to_vec(),
        };
        let encoded = encode_cargo(&cargo);
        let (msg_type, len) = decode_header(&encoded).unwrap();
        assert_eq!(msg_type, MessageType::Cargo);
        let decoded = decode_cargo(&encoded[5..5 + len]).unwrap();
        assert_eq!(decoded.conn_id, 42);
        assert_eq!(decoded.data, b"hello docking");
    }

    #[test]
    fn test_dock_encode() {
        let dock = Dock {
            version: VERSION,
            ship: "orbitcast".into(),
        };
        let encoded = encode_dock(&dock);
        let (msg_type, _) = decode_header(&encoded).unwrap();
        assert_eq!(msg_type, MessageType::Dock);
    }
}