mod airlock;
pub use airlock::{DockingConnector, next_conn_id};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub const VERSION: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum MessageType {
Dock = 0x01,
Moored = 0x02,
Boarding = 0x10,
Disembark = 0x11,
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)),
}
}
}
#[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),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dock {
pub version: u8,
pub ship: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Moored {
pub version: u8,
#[serde(default)]
pub config: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Boarding {
pub conn_id: u32,
pub path: String,
pub remote_addr: String,
pub headers: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Disembark {
pub conn_id: u32,
pub code: u16,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct Cargo {
pub conn_id: u32,
pub data: Vec<u8>,
}
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
}
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))
}
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()
}
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()
}
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()
}
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()
}
pub fn encode_cargo(cargo: &Cargo) -> Vec<u8> {
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
}
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);
}
}