aboutsummaryrefslogtreecommitdiffstats
path: root/src/protocol.rs
diff options
context:
space:
mode:
authormurilo ijanc2026-03-25 02:07:37 -0300
committermurilo ijanc2026-03-25 02:07:37 -0300
commit7aff2e1d279a4e442b32f49ca0a0eca065355787 (patch)
treebc987ece7eb78bb8375de1b20123ecd0f90472ba /src/protocol.rs
downloadtesseras-paste-7aff2e1d279a4e442b32f49ca0a0eca065355787.tar.gz
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.
Diffstat (limited to 'src/protocol.rs')
-rw-r--r--src/protocol.rs173
1 files changed, 173 insertions, 0 deletions
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 <ttl_secs> <content>\n
+//! PUTP <ttl_secs> <content>\n
+//! GET <key>\n
+//! DEL <key>\n
+//! PIN <key>\n
+//! UNPIN <key>\n
+//! STATUS\n
+//! SHUTDOWN\n
+//!
+//! Responses:
+//! OK <data>\n
+//! ERR <message>\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<Request, String> {
+ 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 <ttl> <data>")?;
+ let content_b58 =
+ parts.next().ok_or("PUT requires: PUT <ttl> <data>")?;
+ 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 <key>")?;
+ Ok(Request::Get {
+ key: key.to_string(),
+ })
+ }
+ "DEL" => {
+ let key = parts.next().ok_or("DEL requires: DEL <key>")?;
+ Ok(Request::Del {
+ key: key.to_string(),
+ })
+ }
+ "PIN" => {
+ let key = parts.next().ok_or("PIN requires: PIN <key>")?;
+ Ok(Request::Pin {
+ key: key.to_string(),
+ })
+ }
+ "UNPIN" => {
+ let key = parts.next().ok_or("UNPIN requires: UNPIN <key>")?;
+ 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");
+ }
+}