//! tu — tesseras-url CLI client. //! //! Sends commands to the `tud` daemon over a Unix socket. use std::io::{BufRead, BufReader, 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-url/daemon.sock") } fn usage() { eprintln!("usage: tu [-s sock] [-v] [args]"); eprintln!(); eprintln!("commands:"); eprintln!(" shorten [-t ttl] [-s slug] create short URL"); eprintln!(" resolve show target URL"); eprintln!(" del delete short URL"); eprintln!(" list list local entries"); 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, 0=forever)"); } fn parse_ttl(s: &str) -> Result { let s = s.trim(); if s == "0" { return Ok(0); } if let Some(h) = s.strip_suffix('h') { h.parse::() .map(|v| v * 3600) .map_err(|e| e.to_string()) } else if let Some(m) = s.strip_suffix('m') { m.parse::().map(|v| v * 60).map_err(|e| e.to_string()) } else if let Some(sec) = s.strip_suffix('s') { sec.parse::().map_err(|e| e.to_string()) } else { s.parse::().map_err(|e| e.to_string()) } } fn main() { let args: Vec = 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 request = match command.as_str() { "shorten" => { let mut ttl = "0".to_string(); let mut slug = "auto".to_string(); let mut url: Option = None; 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(); } } "-s" => { j += 1; if j < cmd_args.len() { slug = cmd_args[j].clone(); } } arg if !arg.starts_with('-') => { url = Some(arg.to_string()); } _ => {} } j += 1; } let url = match url { Some(u) => u, None => { eprintln!("error: shorten requires a URL"); std::process::exit(1); } }; let ttl_secs = match parse_ttl(&ttl) { Ok(s) => s, Err(e) => { eprintln!("error: bad TTL: {e}"); std::process::exit(1); } }; format!("SHORTEN {ttl_secs} {slug} {url}\n") } "resolve" => { let slug = cmd_args.get(1).unwrap_or_else(|| { eprintln!("error: resolve requires a slug"); std::process::exit(1); }); format!("RESOLVE {slug}\n") } "del" => { let slug = cmd_args.get(1).unwrap_or_else(|| { eprintln!("error: del requires a slug"); std::process::exit(1); }); format!("DEL {slug}\n") } "list" => "LIST\n".to_string(), "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 tud 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 command == "list" && data != "(empty)" { // Decode base58-encoded list if let Some(decoded) = base58::decode(data) { if let Ok(text) = std::str::from_utf8(&decoded) { for entry in text.lines() { println!("{entry}"); } } else { println!("{data}"); } } else { println!("{data}"); } } else { println!("{data}"); } break; } else if let Some(msg) = line.strip_prefix("ERR ") { eprintln!("error: {msg}"); std::process::exit(1); } } }