From 7aff2e1d279a4e442b32f49ca0a0eca065355787 Mon Sep 17 00:00:00 2001 From: murilo ijanc Date: Wed, 25 Mar 2026 02:07:37 -0300 Subject: Initial commit: tesseras-paste decentralized pastebin DHT-backed encrypted pastebin with two binaries (tp/tpd), XChaCha20-Poly1305 encryption, content-addressed storage, and Unix socket + HTTP interfaces. --- src/protocol.rs | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 src/protocol.rs (limited to 'src/protocol.rs') diff --git a/src/protocol.rs b/src/protocol.rs new file mode 100644 index 0000000..d45cdd8 --- /dev/null +++ b/src/protocol.rs @@ -0,0 +1,173 @@ +//! 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"); + } +} -- cgit v1.2.3