use std::collections::HashMap; use serde::{Deserialize, Serialize}; /// Messages sent from client → daemon. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ClientMessage { Auth { token: String, }, CreateSession { id: String, shell: Option, cwd: Option, env: Option>, cols: u16, rows: u16, }, WriteInput { session_id: String, /// Raw bytes encoded as a base64 string. data: String, }, Resize { session_id: String, cols: u16, rows: u16, }, Subscribe { session_id: String, }, Unsubscribe { session_id: String, }, CloseSession { session_id: String, }, ListSessions, Ping, } /// Messages sent from daemon → client. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum DaemonMessage { AuthResult { ok: bool, }, SessionCreated { session_id: String, pid: u32, }, /// PTY output bytes base64-encoded so they survive JSON transport. SessionOutput { session_id: String, data: String, }, SessionClosed { session_id: String, exit_code: Option, }, SessionList { sessions: Vec, }, Pong, Error { message: String, }, } /// Snapshot of a running or recently-exited session. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionInfo { pub id: String, pub pid: u32, pub shell: String, pub cwd: String, pub cols: u16, pub rows: u16, /// Unix timestamp of session creation. pub created_at: u64, pub alive: bool, } /// Encode raw bytes as base64 for embedding in JSON. pub fn encode_output(bytes: &[u8]) -> String { use std::fmt::Write as FmtWrite; // Manual base64 — avoids adding a new crate; the hex crate IS available but // base64 better compresses binary PTY data (33% overhead vs 100% for hex). // We use a simple lookup table implementation that stays within 300 lines. const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4); let mut i = 0; while i + 2 < bytes.len() { let b0 = bytes[i] as usize; let b1 = bytes[i + 1] as usize; let b2 = bytes[i + 2] as usize; let _ = write!( out, "{}{}{}{}", TABLE[b0 >> 2] as char, TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char, TABLE[((b1 & 0xf) << 2) | (b2 >> 6)] as char, TABLE[b2 & 0x3f] as char ); i += 3; } let rem = bytes.len() - i; if rem == 1 { let b0 = bytes[i] as usize; let _ = write!( out, "{}{}==", TABLE[b0 >> 2] as char, TABLE[(b0 & 3) << 4] as char ); } else if rem == 2 { let b0 = bytes[i] as usize; let b1 = bytes[i + 1] as usize; let _ = write!( out, "{}{}{}=", TABLE[b0 >> 2] as char, TABLE[((b0 & 3) << 4) | (b1 >> 4)] as char, TABLE[(b1 & 0xf) << 2] as char ); } out } /// Decode base64 string back to bytes. Returns error string on invalid input. pub fn decode_input(s: &str) -> Result, String> { fn val(c: u8) -> Result { match c { b'A'..=b'Z' => Ok(c - b'A'), b'a'..=b'z' => Ok(c - b'a' + 26), b'0'..=b'9' => Ok(c - b'0' + 52), b'+' => Ok(62), b'/' => Ok(63), b'=' => Ok(0), _ => Err(format!("invalid base64 char: {c}")), } } let bytes = s.as_bytes(); if bytes.len() % 4 != 0 { return Err("base64 length not a multiple of 4".into()); } let mut out = Vec::with_capacity(bytes.len() / 4 * 3); let mut i = 0; while i < bytes.len() { let v0 = val(bytes[i])?; let v1 = val(bytes[i + 1])?; let v2 = val(bytes[i + 2])?; let v3 = val(bytes[i + 3])?; out.push((v0 << 2) | (v1 >> 4)); if bytes[i + 2] != b'=' { out.push((v1 << 4) | (v2 >> 2)); } if bytes[i + 3] != b'=' { out.push((v2 << 6) | v3); } i += 4; } Ok(out) }