From 254c288111643e80e328fa158fcd99c6b7b0464c Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 14:40:53 +0300 Subject: [PATCH] treewide: improve logging; get rid of `unwrap()`s Signed-off-by: NotAShelf Change-Id: I6a6a696442ff25c3f65bb2d5f68e0d78a569fd76 --- Cargo.lock | 102 ++++++++++++++++++++++++ Cargo.toml | 3 + src/commands/decode.rs | 2 + src/commands/delete.rs | 2 + src/commands/list.rs | 1 + src/commands/query.rs | 1 + src/commands/store.rs | 14 ++-- src/commands/wipe.rs | 1 + src/db/mod.rs | 172 +++++++++++++++++++++++++++++++++++------ src/import.rs | 24 +++++- src/main.rs | 71 ++++++++--------- 11 files changed, 324 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b48c803..17689e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -325,6 +334,29 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equator" version = "0.4.2" @@ -543,6 +575,30 @@ dependencies = [ "either", ] +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -788,6 +844,21 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -975,6 +1046,35 @@ dependencies = [ "thiserror 2.0.14", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rgb" version = "0.8.52" @@ -1093,7 +1193,9 @@ version = "0.1.0" dependencies = [ "clap", "dirs", + "env_logger", "image", + "log", "rmp-serde", "serde", "sled", diff --git a/Cargo.toml b/Cargo.toml index 1f1934f..44ec44c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "stash" version = "0.1.0" edition = "2024" +author = "NotAShelf " [dependencies] clap = { version = "4.5.44", features = ["derive"] } @@ -10,3 +11,5 @@ dirs = "6.0.0" serde = { version = "1.0.219", features = ["derive"] } rmp-serde = "1.3.0" image = "0.25.6" +log = "0.4.27" +env_logger = "0.11.8" diff --git a/src/commands/decode.rs b/src/commands/decode.rs index ff5427a..918bd66 100644 --- a/src/commands/decode.rs +++ b/src/commands/decode.rs @@ -1,4 +1,5 @@ use crate::db::{ClipboardDb, SledClipboardDb}; + use std::io::{Read, Write}; pub trait DecodeCommand { @@ -8,5 +9,6 @@ pub trait DecodeCommand { impl DecodeCommand for SledClipboardDb { fn decode(&self, in_: impl Read, out: impl Write, input: Option) { self.decode_entry(in_, out, input); + log::info!("Entry decoded"); } } diff --git a/src/commands/delete.rs b/src/commands/delete.rs index b147717..39d36a8 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,4 +1,5 @@ use crate::db::{ClipboardDb, SledClipboardDb}; + use std::io::Read; pub trait DeleteCommand { @@ -8,5 +9,6 @@ pub trait DeleteCommand { impl DeleteCommand for SledClipboardDb { fn delete(&self, input: impl Read) { self.delete_entries(input); + log::info!("Entries deleted"); } } diff --git a/src/commands/list.rs b/src/commands/list.rs index 2f4666c..658c34c 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -8,5 +8,6 @@ pub trait ListCommand { impl ListCommand for SledClipboardDb { fn list(&self, out: impl Write, preview_width: u32) { self.list_entries(out, preview_width); + log::info!("Entries listed"); } } diff --git a/src/commands/query.rs b/src/commands/query.rs index 078b1e3..7906462 100644 --- a/src/commands/query.rs +++ b/src/commands/query.rs @@ -7,5 +7,6 @@ pub trait QueryCommand { impl QueryCommand for SledClipboardDb { fn query_delete(&self, query: &str) { ::delete_query(self, query); + log::info!("Entries matching query '{}' deleted", query); } } diff --git a/src/commands/store.rs b/src/commands/store.rs index 858c084..4083e93 100644 --- a/src/commands/store.rs +++ b/src/commands/store.rs @@ -1,4 +1,5 @@ use crate::db::{ClipboardDb, SledClipboardDb}; + use std::io::Read; pub trait StoreCommand { @@ -19,13 +20,12 @@ impl StoreCommand for SledClipboardDb { max_items: u64, state: Option, ) { - match state.as_deref() { - Some("sensitive") | Some("clear") => { - self.delete_last(); - } - _ => { - self.store_entry(input, max_dedupe_search, max_items); - } + if let Some("sensitive" | "clear") = state.as_deref() { + self.delete_last(); + log::info!("Entry deleted"); + } else { + self.store_entry(input, max_dedupe_search, max_items); + log::info!("Entry stored"); } } } diff --git a/src/commands/wipe.rs b/src/commands/wipe.rs index ad05377..894c3c2 100644 --- a/src/commands/wipe.rs +++ b/src/commands/wipe.rs @@ -7,5 +7,6 @@ pub trait WipeCommand { impl WipeCommand for SledClipboardDb { fn wipe(&self) { self.wipe_db(); + log::info!("Database wiped"); } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 70ea7f4..f91570d 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,6 +3,7 @@ use std::io::{BufRead, BufReader, Read, Write}; use std::str; use image::{GenericImageView, ImageFormat}; +use log::{error, info, warn}; use rmp_serde::{decode::from_read, encode::to_vec}; use serde::{Deserialize, Serialize}; use sled::{Db, IVec}; @@ -41,9 +42,11 @@ impl ClipboardDb for SledClipboardDb { fn store_entry(&self, mut input: impl Read, max_dedupe_search: u64, max_items: u64) { let mut buf = Vec::new(); if input.read_to_end(&mut buf).is_err() || buf.is_empty() || buf.len() > 5 * 1_000_000 { + warn!("Input is empty or too large, skipping store."); return; } - if buf.iter().all(|b| b.is_ascii_whitespace()) { + if buf.iter().all(u8::is_ascii_whitespace) { + warn!("Input is all whitespace, skipping store."); return; } @@ -57,53 +60,121 @@ impl ClipboardDb for SledClipboardDb { }; let id = self.next_sequence(); - let enc = to_vec(&entry).unwrap(); + let enc = match to_vec(&entry) { + Ok(enc) => enc, + Err(e) => { + error!("Failed to serialize entry: {e}"); + return; + } + }; - self.db.insert(u64_to_ivec(id), enc).unwrap(); + match self.db.insert(u64_to_ivec(id), enc) { + Ok(_) => info!("Stored entry with id {id}"), + Err(e) => error!("Failed to store entry: {e}"), + } self.trim_db(max_items); } fn deduplicate(&self, buf: &[u8], max: u64) { let mut count = 0; + let mut deduped = 0; for item in self.db.iter().rev().take(max as usize) { - let (k, v) = item.unwrap(); - let entry: Entry = from_read(v.as_ref()).unwrap(); + let (k, v) = match item { + Ok((k, v)) => (k, v), + Err(e) => { + error!("Error reading entry during deduplication: {e}"); + continue; + } + }; + let entry: Entry = match from_read(v.as_ref()) { + Ok(e) => e, + Err(e) => { + error!("Error decoding entry during deduplication: {e}"); + continue; + } + }; if entry.contents == buf { - self.db.remove(k).unwrap(); + match self.db.remove(k) { + Ok(_) => { + deduped += 1; + info!("Deduplicated an entry"); + } + Err(e) => error!("Failed to remove entry during deduplication: {e}"), + } } count += 1; if count >= max { break; } } + if deduped > 0 { + info!("Deduplicated {deduped} entries"); + } } fn trim_db(&self, max: u64) { - let mut keys: Vec<_> = self.db.iter().rev().map(|kv| kv.unwrap().0).collect(); + let mut keys: Vec<_> = self + .db + .iter() + .rev() + .filter_map(|kv| match kv { + Ok((k, _)) => Some(k), + Err(e) => { + error!("Failed to read key during trim: {e}"); + None + } + }) + .collect(); + let initial_len = keys.len(); if keys.len() as u64 > max { for k in keys.drain((max as usize)..) { - self.db.remove(k).unwrap(); + match self.db.remove(k) { + Ok(_) => info!("Trimmed entry from database"), + Err(e) => error!("Failed to trim entry: {e}"), + } } + info!( + "Trimmed {} entries from database", + initial_len - max as usize + ); } } fn delete_last(&self) { if let Some((k, _)) = self.db.iter().next_back().and_then(Result::ok) { - self.db.remove(k).unwrap(); + match self.db.remove(k) { + Ok(_) => info!("Deleted last entry"), + Err(e) => error!("Failed to delete last entry: {e}"), + } + } else { + warn!("No entries to delete"); } } fn wipe_db(&self) { - self.db.clear().unwrap(); + match self.db.clear() { + Ok(()) => info!("Wiped database"), + Err(e) => error!("Failed to wipe database: {e}"), + } } fn list_entries(&self, mut out: impl Write, preview_width: u32) { + let mut listed = 0; for (k, v) in self.db.iter().rev().filter_map(Result::ok) { let id = ivec_to_u64(&k); - let entry: Entry = from_read(v.as_ref()).unwrap(); + let entry: Entry = match from_read(v.as_ref()) { + Ok(e) => e, + Err(e) => { + error!("Failed to decode entry during list: {e}"); + continue; + } + }; let preview = preview_entry(&entry.contents, entry.mime.as_deref(), preview_width); - writeln!(out, "{id}\t{preview}").unwrap(); + if writeln!(out, "{id}\t{preview}").is_ok() { + listed += 1; + } } + info!("Listed {listed} entries"); } fn decode_entry(&self, mut in_: impl Read, mut out: impl Write, input: Option) { @@ -111,36 +182,88 @@ impl ClipboardDb for SledClipboardDb { input } else { let mut buf = String::new(); - in_.read_to_string(&mut buf).unwrap(); + if let Err(e) = in_.read_to_string(&mut buf) { + error!("Failed to read input for decode: {e}"); + return; + } buf }; - - let id = extract_id(&s).unwrap(); - let v = self.db.get(u64_to_ivec(id)).unwrap().unwrap(); - let entry: Entry = from_read(v.as_ref()).unwrap(); - out.write_all(&entry.contents).unwrap(); + let id = match extract_id(&s) { + Ok(id) => id, + Err(e) => { + error!("Failed to extract id for decode: {e}"); + return; + } + }; + let v = match self.db.get(u64_to_ivec(id)) { + Ok(Some(v)) => v, + Ok(None) => { + warn!("No entry found for id {id}"); + return; + } + Err(e) => { + error!("Failed to get entry for decode: {e}"); + return; + } + }; + let entry: Entry = match from_read(v.as_ref()) { + Ok(e) => e, + Err(e) => { + error!("Failed to decode entry: {e}"); + return; + } + }; + if let Err(e) = out.write_all(&entry.contents) { + error!("Failed to write decoded entry: {e}"); + } else { + info!("Decoded entry with id {id}"); + } } fn delete_query(&self, query: &str) { + let mut deleted = 0; for (k, v) in self.db.iter().filter_map(Result::ok) { - let entry: Entry = from_read(v.as_ref()).unwrap(); + let entry: Entry = match from_read(v.as_ref()) { + Ok(e) => e, + Err(e) => { + error!("Failed to decode entry during query delete: {e}"); + continue; + } + }; if entry .contents .windows(query.len()) .any(|w| w == query.as_bytes()) { - self.db.remove(k).unwrap(); + match self.db.remove(k) { + Ok(_) => { + deleted += 1; + info!("Deleted entry matching query"); + } + Err(e) => error!("Failed to delete entry during query delete: {e}"), + } } } + info!("Deleted {deleted} entries matching query '{query}'"); } fn delete_entries(&self, in_: impl Read) { let reader = BufReader::new(in_); + let mut deleted = 0; for line in reader.lines().map_while(Result::ok) { if let Ok(id) = extract_id(&line) { - self.db.remove(u64_to_ivec(id)).unwrap(); + match self.db.remove(u64_to_ivec(id)) { + Ok(_) => { + deleted += 1; + info!("Deleted entry with id {id}"); + } + Err(e) => error!("Failed to delete entry with id {id}: {e}"), + } + } else { + warn!("Failed to extract id from line: {line}"); } } + info!("Deleted {deleted} entries by id from stdin"); } fn next_sequence(&self) -> u64 { @@ -148,7 +271,7 @@ impl ClipboardDb for SledClipboardDb { .db .iter() .next_back() - .and_then(|r| r.ok()) + .and_then(std::result::Result::ok) .map(|(k, _)| ivec_to_u64(&k)); last.unwrap_or(0) + 1 } @@ -165,7 +288,10 @@ pub fn u64_to_ivec(v: u64) -> IVec { } pub fn ivec_to_u64(v: &IVec) -> u64 { - let arr: [u8; 8] = v.as_ref().try_into().unwrap(); + let arr: [u8; 8] = if let Ok(arr) = v.as_ref().try_into() { arr } else { + error!("Failed to convert IVec to u64: invalid length"); + return 0; + }; u64::from_be_bytes(arr) } diff --git a/src/import.rs b/src/import.rs index 1447c2b..2276489 100644 --- a/src/import.rs +++ b/src/import.rs @@ -1,4 +1,5 @@ use crate::db::{Entry, SledClipboardDb, detect_mime, u64_to_ivec}; +use log::{error, info}; use std::io::{self, BufRead}; pub trait ImportCommand { @@ -17,12 +18,27 @@ impl ImportCommand for SledClipboardDb { contents: val.as_bytes().to_vec(), mime: detect_mime(val.as_bytes()), }; - let enc = rmp_serde::encode::to_vec(&entry).unwrap(); - self.db.insert(u64_to_ivec(id), enc).unwrap(); - imported += 1; + let enc = match rmp_serde::encode::to_vec(&entry) { + Ok(enc) => enc, + Err(e) => { + error!("Failed to encode entry for id {id}: {e}"); + continue; + } + }; + match self.db.insert(u64_to_ivec(id), enc) { + Ok(_) => { + imported += 1; + info!("Imported entry with id {id}"); + } + Err(e) => error!("Failed to insert entry with id {id}: {e}"), + } + } else { + error!("Failed to parse id from line: {id_str}"); } + } else { + error!("Malformed TSV line: {line:?}"); } } - eprintln!("Imported {imported} records from TSV into sled database."); + info!("Imported {imported} records from TSV into sled database."); } } diff --git a/src/main.rs b/src/main.rs index a3c6c6b..f936774 100644 --- a/src/main.rs +++ b/src/main.rs @@ -76,6 +76,7 @@ enum Command { } fn main() { + env_logger::init(); let cli = Cli::parse(); let db_path = cli.db_path.unwrap_or_else(|| { dirs::cache_dir() @@ -85,10 +86,9 @@ fn main() { }); let sled_db = sled::open(&db_path).unwrap_or_else(|e| { - eprintln!("Failed to open database: {e}"); + log::error!("Failed to open database: {e}"); process::exit(1); }); - let db = db::SledClipboardDb { db: sled_db }; if cli.import_tsv { @@ -98,53 +98,54 @@ fn main() { match cli.command { Some(Command::Store) => { - let state = env::var("STASH_CLIPBOARD_STATE").ok(); + log::info!("Executing: Store"); + let state = env::var("CLIPBOARD_STATE").ok(); db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state); } - Some(Command::List) => { + log::info!("Executing: List"); db.list(io::stdout(), cli.preview_width); } - Some(Command::Decode { input }) => { + log::info!("Executing: Decode"); db.decode(io::stdin(), io::stdout(), input); } - - Some(Command::Delete { arg, r#type }) => match (arg, r#type.as_deref()) { - (Some(s), Some("id")) => { - if let Ok(id) = s.parse::() { - use std::io::Cursor; - db.delete(Cursor::new(format!("{id}\n"))); - } else { - eprintln!("Argument is not a valid id"); + Some(Command::Delete { arg, r#type }) => { + log::info!("Executing: Delete"); + match (arg, r#type.as_deref()) { + (Some(s), Some("id")) => { + if let Ok(id) = s.parse::() { + use std::io::Cursor; + db.delete(Cursor::new(format!("{id}\n"))); + } else { + log::error!("Argument is not a valid id"); + } } - } - - (Some(s), Some("query")) => { - db.query_delete(&s); - } - - (Some(s), None) => { - if let Ok(id) = s.parse::() { - use std::io::Cursor; - db.delete(Cursor::new(format!("{id}\n"))); - } else { + (Some(s), Some("query")) => { db.query_delete(&s); } + (Some(s), None) => { + if let Ok(id) = s.parse::() { + use std::io::Cursor; + db.delete(Cursor::new(format!("{id}\n"))); + } else { + db.query_delete(&s); + } + } + (None, _) => { + db.delete(io::stdin()); + } + (_, Some(_)) => { + log::error!("Unknown type for --type. Use \"id\" or \"query\"."); + } } - - (None, _) => { - db.delete(io::stdin()); - } - - (_, Some(_)) => { - eprintln!("Unknown type for --type. Use \"id\" or \"query\"."); - } - }, + } Some(Command::Wipe) => { + log::info!("Executing: Wipe"); db.wipe(); } Some(Command::Import { r#type }) => { + log::info!("Executing: Import"); // Default format is TSV (Cliphist compatible) let format = r#type.as_deref().unwrap_or("tsv"); match format { @@ -152,12 +153,12 @@ fn main() { db.import_tsv(io::stdin()); } _ => { - eprintln!("Unsupported import format: {format}"); + log::error!("Unsupported import format: {format}"); } } } _ => { - eprintln!("No subcommand provided"); + log::warn!("No subcommand provided"); } } }