tesseras-paste 0.1.3

Decentralized pastebin built on tesseras-dht
//! tp — tesseras-paste CLI client.
//!
//! Sends commands to the `tpd` daemon over a Unix socket.
//! Reads paste content from stdin (put) and writes it to
//! stdout (get).

use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;

#[path = "../base58.rs"]
mod base58;
#[path = "../sandbox.rs"]
mod sandbox;

fn default_socket() -> PathBuf {
    PathBuf::from("/var/tesseras-paste/daemon.sock")
}

fn usage() {
    eprintln!("usage: tp [-s sock] [-v] <command> [args]");
    eprintln!();
    eprintln!("commands:");
    eprintln!("  put [-t ttl] [-p]  read stdin, store paste");
    eprintln!("                     -p  public (no encryption)");
    eprintln!("  get <key>          retrieve paste to stdout");
    eprintln!("  del <key>       delete paste");
    eprintln!("  pin <key>       pin (never expires)");
    eprintln!("  unpin <key>     unpin");
    eprintln!("  status          show daemon status");
    eprintln!();
    eprintln!("  -s sock   Unix socket path");
    eprintln!("  -v        verbose output");
    eprintln!("  -t ttl    time-to-live (e.g. 24h 30m 3600)");
}

fn parse_ttl(s: &str) -> Result<u64, String> {
    let s = s.trim();
    if let Some(h) = s.strip_suffix('h') {
        h.parse::<u64>()
            .map(|v| v * 3600)
            .map_err(|e| e.to_string())
    } else if let Some(m) = s.strip_suffix('m') {
        m.parse::<u64>().map(|v| v * 60).map_err(|e| e.to_string())
    } else if let Some(sec) = s.strip_suffix('s') {
        sec.parse::<u64>().map_err(|e| e.to_string())
    } else {
        s.parse::<u64>().map_err(|e| e.to_string())
    }
}

fn main() {
    let args: Vec<String> = std::env::args().collect();

    let mut sock_path = default_socket();
    let mut verbose = false;
    let mut cmd_start = 1;

    // Parse global options before command
    let mut i = 1;
    while i < args.len() {
        match args[i].as_str() {
            "-s" => {
                i += 1;
                sock_path =
                    args.get(i).map(PathBuf::from).unwrap_or_else(|| {
                        eprintln!("error: -s requires path");
                        std::process::exit(1);
                    });
                cmd_start = i + 1;
            }
            "-v" => {
                verbose = true;
                cmd_start = i + 1;
            }
            "-h" | "--help" => {
                usage();
                return;
            }
            _ => break,
        }
        i += 1;
    }

    let cmd_args = &args[cmd_start..];
    if cmd_args.is_empty() {
        usage();
        std::process::exit(1);
    }

    let command = &cmd_args[0];
    let mut is_get = false;

    let request = match command.as_str() {
        "put" => {
            let mut ttl = "24h".to_string();
            let mut public = false;
            let mut j = 1;
            while j < cmd_args.len() {
                match cmd_args[j].as_str() {
                    "-t" => {
                        j += 1;
                        if j < cmd_args.len() {
                            ttl = cmd_args[j].clone();
                        }
                    }
                    "-p" => public = true,
                    _ => {}
                }
                j += 1;
            }
            let ttl_secs = match parse_ttl(&ttl) {
                Ok(s) => s,
                Err(e) => {
                    eprintln!("error: bad TTL: {e}");
                    std::process::exit(1);
                }
            };
            // Read at most MAX_PASTE + 1 byte so we can detect
            // oversized input without unbounded allocation.
            const MAX_PASTE: usize = 64 * 1024;
            let mut content = Vec::new();
            match std::io::stdin()
                .take((MAX_PASTE + 1) as u64)
                .read_to_end(&mut content)
            {
                Ok(0) => {
                    eprintln!("error: empty input");
                    std::process::exit(1);
                }
                Ok(n) if n > MAX_PASTE => {
                    eprintln!("error: input exceeds 64 KiB limit");
                    std::process::exit(1);
                }
                Err(e) => {
                    eprintln!("error: reading stdin: {e}");
                    std::process::exit(1);
                }
                _ => {}
            }
            let cmd = if public { "PUTP" } else { "PUT" };
            format!("{cmd} {ttl_secs} {}\n", base58::encode(&content))
        }
        "get" => {
            let key = cmd_args.get(1).unwrap_or_else(|| {
                eprintln!("error: get requires a key");
                std::process::exit(1);
            });
            is_get = true;
            format!("GET {key}\n")
        }
        "del" => {
            let key = cmd_args.get(1).unwrap_or_else(|| {
                eprintln!("error: del requires a key");
                std::process::exit(1);
            });
            format!("DEL {key}\n")
        }
        "pin" => {
            let key = cmd_args.get(1).unwrap_or_else(|| {
                eprintln!("error: pin requires a key");
                std::process::exit(1);
            });
            format!("PIN {key}\n")
        }
        "unpin" => {
            let key = cmd_args.get(1).unwrap_or_else(|| {
                eprintln!("error: unpin requires a key");
                std::process::exit(1);
            });
            format!("UNPIN {key}\n")
        }
        "status" => "STATUS\n".to_string(),
        other => {
            eprintln!("unknown command: {other}");
            usage();
            std::process::exit(1);
        }
    };

    // ── Sandbox ─────────────────────────────────────
    sandbox::do_unveil(&sock_path, "rw");
    sandbox::unveil_lock();
    sandbox::do_pledge("stdio unix rpath");

    if verbose {
        eprintln!("socket: {}", sock_path.display());
        eprintln!(">> {}", request.trim());
    }

    let stream = match UnixStream::connect(&sock_path) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("error: cannot connect to {}: {e}", sock_path.display(),);
            eprintln!("hint: is tpd running?");
            std::process::exit(1);
        }
    };

    stream
        .set_read_timeout(Some(std::time::Duration::from_secs(60)))
        .ok();

    let mut writer = &stream;
    if let Err(e) = writer.write_all(request.as_bytes()) {
        eprintln!("error: writing to socket: {e}");
        std::process::exit(1);
    }

    let reader = BufReader::new(&stream);
    for line in reader.lines() {
        let line = match line {
            Ok(l) => l,
            Err(_) => break,
        };
        if verbose {
            eprintln!("<< {}", line);
        }
        if let Some(data) = line.strip_prefix("OK ") {
            if is_get {
                // Decode base58 → raw bytes → stdout
                match base58::decode(data) {
                    Some(bytes) => {
                        if let Err(e) = std::io::stdout().write_all(&bytes) {
                            eprintln!("error: writing to stdout: {e}");
                            std::process::exit(1);
                        }
                    }
                    None => println!("{data}"),
                }
            } else {
                println!("{data}");
            }
            break;
        } else if let Some(msg) = line.strip_prefix("ERR ") {
            eprintln!("error: {msg}");
            std::process::exit(1);
        }
    }
}