//! Unix socket protocol for daemon ↔ CLI. //! //! Simple line-oriented text protocol: //! PUT \n //! PUTP \n //! GET \n //! DEL \n //! PIN \n //! UNPIN \n //! STATUS\n //! SHUTDOWN\n //! //! Responses: //! OK \n //! ERR \n /// A parsed request received from the CLI over the Unix socket. pub enum Request { Put { ttl_secs: u64, content_b58: String, encrypt: bool, }, Get { key: String, }, Del { key: String, }, Pin { key: String, }, Unpin { key: String, }, Status, Shutdown, } /// A response sent back to the CLI over the Unix socket. pub enum Response { Ok(String), Err(String), } /// Parse a single protocol line into a [`Request`]. pub fn parse_request(line: &str) -> Result { let line = line.trim(); if line.is_empty() { return Err("empty request".into()); } let mut parts = line.splitn(3, ' '); let cmd = parts.next().unwrap(); match cmd { "PUT" | "PUTP" => { let ttl_str = parts.next().ok_or("PUT requires: PUT ")?; let content_b58 = parts.next().ok_or("PUT requires: PUT ")?; let ttl_secs: u64 = ttl_str.parse().map_err(|_| "invalid TTL number")?; Ok(Request::Put { ttl_secs, content_b58: content_b58.to_string(), encrypt: cmd == "PUT", }) } "GET" => { let key = parts.next().ok_or("GET requires: GET ")?; Ok(Request::Get { key: key.to_string(), }) } "DEL" => { let key = parts.next().ok_or("DEL requires: DEL ")?; Ok(Request::Del { key: key.to_string(), }) } "PIN" => { let key = parts.next().ok_or("PIN requires: PIN ")?; Ok(Request::Pin { key: key.to_string(), }) } "UNPIN" => { let key = parts.next().ok_or("UNPIN requires: UNPIN ")?; Ok(Request::Unpin { key: key.to_string(), }) } "STATUS" => Ok(Request::Status), "SHUTDOWN" => Ok(Request::Shutdown), _ => Err(format!("unknown command: {cmd}")), } } /// Serialize a [`Response`] into a protocol line (`OK ...\n` or `ERR ...\n`). pub fn format_response(r: &Response) -> String { match r { Response::Ok(data) => format!("OK {data}\n"), Response::Err(msg) => format!("ERR {msg}\n"), } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_put() { let r = parse_request("PUT 3600 deadbeef").unwrap(); match r { Request::Put { ttl_secs, content_b58, encrypt, } => { assert_eq!(ttl_secs, 3600); assert_eq!(content_b58, "deadbeef"); assert!(encrypt); } _ => panic!("expected Put"), } } #[test] fn parse_putp() { let r = parse_request("PUTP 60 abc").unwrap(); match r { Request::Put { encrypt, .. } => assert!(!encrypt), _ => panic!("expected Put"), } } #[test] fn parse_get() { let r = parse_request("GET abc123#key456").unwrap(); match r { Request::Get { key } => assert_eq!(key, "abc123#key456"), _ => panic!("expected Get"), } } #[test] fn parse_status() { assert!(matches!(parse_request("STATUS").unwrap(), Request::Status)); } #[test] fn parse_empty_fails() { assert!(parse_request("").is_err()); } #[test] fn parse_unknown_fails() { assert!(parse_request("FOOBAR").is_err()); } #[test] fn format_ok() { let r = format_response(&Response::Ok("hello".into())); assert_eq!(r, "OK hello\n"); } #[test] fn format_err() { let r = format_response(&Response::Err("oops".into())); assert_eq!(r, "ERR oops\n"); } }