From 0c8003d6367d7bf81b0b32990d7af5be88804c08 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 15:31:58 +0300 Subject: [PATCH 1/8] stash: remove redundant import flag Signed-off-by: NotAShelf Change-Id: I6a6a6964348f1ad4915890f0b4ccd59619cc3e1a --- src/main.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main.rs b/src/main.rs index f936774..f8d76ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,9 +37,6 @@ struct Cli { #[arg(long)] db_path: Option, - - #[arg(long)] - import_tsv: bool, } #[derive(Subcommand)] @@ -89,12 +86,8 @@ fn main() { log::error!("Failed to open database: {e}"); process::exit(1); }); - let db = db::SledClipboardDb { db: sled_db }; - if cli.import_tsv { - db.import_tsv(io::stdin()); - return; - } + let db = db::SledClipboardDb { db: sled_db }; match cli.command { Some(Command::Store) => { From 0e4f4018ab29c66c2292f1adaca98fe288ddaf58 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 15:33:53 +0300 Subject: [PATCH 2/8] stash: use more granular `STASH_CLIPBOARD_STATE` Signed-off-by: NotAShelf Change-Id: I6a6a6964d59d2ce974ba482aee9b1c8f20cdb44e --- src/db/mod.rs | 4 +++- src/main.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/db/mod.rs b/src/db/mod.rs index f91570d..66c0205 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -288,7 +288,9 @@ pub fn u64_to_ivec(v: u64) -> IVec { } pub fn ivec_to_u64(v: &IVec) -> u64 { - let arr: [u8; 8] = if let Ok(arr) = v.as_ref().try_into() { arr } else { + 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; }; diff --git a/src/main.rs b/src/main.rs index f8d76ed..0c1a9c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,7 +92,7 @@ fn main() { match cli.command { Some(Command::Store) => { log::info!("Executing: Store"); - let state = env::var("CLIPBOARD_STATE").ok(); + let state = env::var("STASH_CLIPBOARD_STATE").ok(); db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state); } Some(Command::List) => { From d8d27fab22cd6acf5d5b8011339c74add4aa261d Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 15:35:26 +0300 Subject: [PATCH 3/8] stash: remove redundant log messages Ported from print-based logging, now redundant Signed-off-by: NotAShelf Change-Id: I6a6a6964c1ba52c792bd3c2a3e15a97a3b03260e --- src/main.rs | 59 +++++++++++++++++++++++------------------------------ 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0c1a9c0..f00c9ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,54 +91,47 @@ fn main() { match cli.command { Some(Command::Store) => { - log::info!("Executing: Store"); let state = env::var("STASH_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 }) => { - 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 { - db.query_delete(&s); - } - } - (None, _) => { - db.delete(io::stdin()); - } - (_, Some(_)) => { - log::error!("Unknown type for --type. Use \"id\" or \"query\"."); + 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 { + 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 { + db.query_delete(&s); + } + } + (None, _) => { + db.delete(io::stdin()); + } + (_, Some(_)) => { + log::error!("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 { From c23c5af8c18a8379fd9d124d70525b9285582b79 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 15:37:25 +0300 Subject: [PATCH 4/8] stash: allow controlling verbosity from the command line Signed-off-by: NotAShelf Change-Id: I6a6a69641d5d727b1d95f8c9e2b851b3a194315a --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + src/main.rs | 8 +++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 17689e0..bab6c3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,16 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-verbosity-flag" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" version = "4.5.44" @@ -1192,6 +1202,7 @@ name = "stash" version = "0.1.0" dependencies = [ "clap", + "clap-verbosity-flag", "dirs", "env_logger", "image", diff --git a/Cargo.toml b/Cargo.toml index 32b677c..4bfd86a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,4 @@ rmp-serde = "1.3.0" image = "0.25.6" log = "0.4.27" env_logger = "0.11.8" +clap-verbosity-flag = "3.0.3" diff --git a/src/main.rs b/src/main.rs index f00c9ef..b79f9cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,6 +37,9 @@ struct Cli { #[arg(long)] db_path: Option, + + #[command(flatten)] + verbosity: clap_verbosity_flag::Verbosity, } #[derive(Subcommand)] @@ -73,8 +76,11 @@ enum Command { } fn main() { - env_logger::init(); let cli = Cli::parse(); + env_logger::Builder::new() + .filter_level(cli.verbosity.into()) + .init(); + let db_path = cli.db_path.unwrap_or_else(|| { dirs::cache_dir() .unwrap_or_else(|| PathBuf::from("/tmp")) From 7dd3db4c8846959501a4efbe8a20ad187ee4bc55 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 15:42:09 +0300 Subject: [PATCH 5/8] ci: publish tagged releases Signed-off-by: NotAShelf Change-Id: I6a6a6964901463c7111df0764272c26ff83eea9f --- .github/workflows/release.yaml | 104 +++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..8f49c41 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,104 @@ +name: Publish Built Binaries + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + create-release: + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + release_id: ${{ steps.create_release.outputs.id }} + steps: + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v2 + with: + draft: false + prerelease: false + generate_release_notes: true + + build-release: + needs: create-release + strategy: + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + name: stash-linux-amd64 + cross: false + - os: ubuntu-latest + target: aarch64-unknown-linux-gnu + name: stash-linux-arm64 + cross: true + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Cache dependencies + uses: Swatinem/rust-cache@v2 + + - name: Setup cross-compilation (Linux ARM64) + if: matrix.cross && matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Install cross + if: matrix.cross + uses: taiki-e/install-action@v2 + with: + tool: cross + + - name: Build binary (native) + if: ${{ !matrix.cross }} + run: cargo build --release --target ${{ matrix.target }} + + - name: Build binary (cross) + if: ${{ matrix.cross }} + run: cross build --release --target ${{ matrix.target }} + + - name: Prepare binary (Unix) + if: ${{ !contains(matrix.os, 'windows') }} + run: | + cp target/${{ matrix.target }}/release/stash ${{ matrix.name }} + + - name: Upload Release Asset + uses: softprops/action-gh-release@v2 + with: + files: ${{ matrix.name }} + + generate-checksums: + needs: [create-release, build-release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Download Assets + uses: robinraju/release-downloader@v1 + with: + tag: ${{ github.ref_name }} + fileName: "stash-*" + out-file-path: "." + + - name: Generate checksums + run: | + sha256sum stash-* > SHA256SUMS + + - name: Upload Checksums + uses: softprops/action-gh-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + files: SHA256SUMS + From 6e210213068d4d6eb9bfaa0d7a25d13417e0672f Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 16:08:28 +0300 Subject: [PATCH 6/8] treewide: improve logging; custom error types with thiserror Signed-off-by: NotAShelf Change-Id: I6a6a696464e4123d15cfaedf4727776e55948369 --- Cargo.lock | 1 + Cargo.toml | 1 + src/commands/decode.rs | 19 ++- src/commands/delete.rs | 18 ++- src/commands/list.rs | 9 +- src/commands/query.rs | 9 +- src/commands/store.rs | 9 +- src/commands/wipe.rs | 9 +- src/db/mod.rs | 276 +++++++++++++++++++++-------------------- src/main.rs | 36 ++++-- 10 files changed, 221 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bab6c3e..c2c5b62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1210,6 +1210,7 @@ dependencies = [ "rmp-serde", "serde", "sled", + "thiserror 2.0.14", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4bfd86a..0ea81ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ image = "0.25.6" log = "0.4.27" env_logger = "0.11.8" clap-verbosity-flag = "3.0.3" +thiserror = "2.0.14" diff --git a/src/commands/decode.rs b/src/commands/decode.rs index 918bd66..b1c94e8 100644 --- a/src/commands/decode.rs +++ b/src/commands/decode.rs @@ -2,13 +2,26 @@ use crate::db::{ClipboardDb, SledClipboardDb}; use std::io::{Read, Write}; +use crate::db::StashError; + pub trait DecodeCommand { - fn decode(&self, in_: impl Read, out: impl Write, input: Option); + fn decode( + &self, + in_: impl Read, + out: impl Write, + input: Option, + ) -> Result<(), StashError>; } impl DecodeCommand for SledClipboardDb { - fn decode(&self, in_: impl Read, out: impl Write, input: Option) { - self.decode_entry(in_, out, input); + fn decode( + &self, + in_: impl Read, + out: impl Write, + input: Option, + ) -> Result<(), StashError> { + self.decode_entry(in_, out, input)?; log::info!("Entry decoded"); + Ok(()) } } diff --git a/src/commands/delete.rs b/src/commands/delete.rs index 39d36a8..a2822ad 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -1,14 +1,22 @@ -use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::{ClipboardDb, SledClipboardDb, StashError}; use std::io::Read; pub trait DeleteCommand { - fn delete(&self, input: impl Read); + fn delete(&self, input: impl Read) -> Result; } impl DeleteCommand for SledClipboardDb { - fn delete(&self, input: impl Read) { - self.delete_entries(input); - log::info!("Entries deleted"); + fn delete(&self, input: impl Read) -> Result { + match self.delete_entries(input) { + Ok(deleted) => { + log::info!("Deleted {} entries", deleted); + Ok(deleted) + } + Err(e) => { + log::error!("Failed to delete entries: {}", e); + Err(e) + } + } } } diff --git a/src/commands/list.rs b/src/commands/list.rs index 658c34c..28d42a8 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -2,12 +2,13 @@ use crate::db::{ClipboardDb, SledClipboardDb}; use std::io::Write; pub trait ListCommand { - fn list(&self, out: impl Write, preview_width: u32); + fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError>; } impl ListCommand for SledClipboardDb { - fn list(&self, out: impl Write, preview_width: u32) { - self.list_entries(out, preview_width); - log::info!("Entries listed"); + fn list(&self, out: impl Write, preview_width: u32) -> Result<(), crate::db::StashError> { + self.list_entries(out, preview_width)?; + log::info!("Listed clipboard entries"); + Ok(()) } } diff --git a/src/commands/query.rs b/src/commands/query.rs index 7906462..981334f 100644 --- a/src/commands/query.rs +++ b/src/commands/query.rs @@ -1,12 +1,13 @@ use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::StashError; + pub trait QueryCommand { - fn query_delete(&self, query: &str); + fn query_delete(&self, query: &str) -> Result; } impl QueryCommand for SledClipboardDb { - fn query_delete(&self, query: &str) { - ::delete_query(self, query); - log::info!("Entries matching query '{}' deleted", query); + fn query_delete(&self, query: &str) -> Result { + ::delete_query(self, query) } } diff --git a/src/commands/store.rs b/src/commands/store.rs index 4083e93..40db4d0 100644 --- a/src/commands/store.rs +++ b/src/commands/store.rs @@ -9,7 +9,7 @@ pub trait StoreCommand { max_dedupe_search: u64, max_items: u64, state: Option, - ); + ) -> Result<(), crate::db::StashError>; } impl StoreCommand for SledClipboardDb { @@ -19,13 +19,14 @@ impl StoreCommand for SledClipboardDb { max_dedupe_search: u64, max_items: u64, state: Option, - ) { + ) -> Result<(), crate::db::StashError> { if let Some("sensitive" | "clear") = state.as_deref() { - self.delete_last(); + self.delete_last()?; log::info!("Entry deleted"); } else { - self.store_entry(input, max_dedupe_search, max_items); + self.store_entry(input, max_dedupe_search, max_items)?; log::info!("Entry stored"); } + Ok(()) } } diff --git a/src/commands/wipe.rs b/src/commands/wipe.rs index 894c3c2..cfde239 100644 --- a/src/commands/wipe.rs +++ b/src/commands/wipe.rs @@ -1,12 +1,15 @@ use crate::db::{ClipboardDb, SledClipboardDb}; +use crate::db::StashError; + pub trait WipeCommand { - fn wipe(&self); + fn wipe(&self) -> Result<(), StashError>; } impl WipeCommand for SledClipboardDb { - fn wipe(&self) { - self.wipe_db(); + fn wipe(&self) -> Result<(), StashError> { + self.wipe_db()?; log::info!("Database wiped"); + Ok(()) } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 66c0205..82156fb 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -3,21 +3,76 @@ use std::io::{BufRead, BufReader, Read, Write}; use std::str; use image::{GenericImageView, ImageFormat}; -use log::{error, info, warn}; +use log::{error, info}; use rmp_serde::{decode::from_read, encode::to_vec}; use serde::{Deserialize, Serialize}; use sled::{Db, IVec}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum StashError { + #[error("Input is empty or too large, skipping store.")] + EmptyOrTooLarge, + #[error("Input is all whitespace, skipping store.")] + AllWhitespace, + #[error("Failed to serialize entry: {0}")] + Serialize(String), + #[error("Failed to store entry: {0}")] + Store(String), + #[error("Error reading entry during deduplication: {0}")] + DeduplicationRead(String), + #[error("Error decoding entry during deduplication: {0}")] + DeduplicationDecode(String), + #[error("Failed to remove entry during deduplication: {0}")] + DeduplicationRemove(String), + #[error("Failed to trim entry: {0}")] + Trim(String), + #[error("No entries to delete")] + NoEntriesToDelete, + #[error("Failed to delete last entry: {0}")] + DeleteLast(String), + #[error("Failed to wipe database: {0}")] + Wipe(String), + #[error("Failed to decode entry during list: {0}")] + ListDecode(String), + #[error("Failed to read input for decode: {0}")] + DecodeRead(String), + #[error("Failed to extract id for decode: {0}")] + DecodeExtractId(String), + #[error("Failed to get entry for decode: {0}")] + DecodeGet(String), + #[error("No entry found for id {0}")] + DecodeNoEntry(u64), + #[error("Failed to decode entry: {0}")] + DecodeDecode(String), + #[error("Failed to write decoded entry: {0}")] + DecodeWrite(String), + #[error("Failed to delete entry during query delete: {0}")] + QueryDelete(String), + #[error("Failed to delete entry with id {0}: {1}")] + DeleteEntry(u64, String), +} pub trait ClipboardDb { - fn store_entry(&self, input: impl Read, max_dedupe_search: u64, max_items: u64); - fn deduplicate(&self, buf: &[u8], max: u64); - fn trim_db(&self, max: u64); - fn delete_last(&self); - fn wipe_db(&self); - fn list_entries(&self, out: impl Write, preview_width: u32); - fn decode_entry(&self, in_: impl Read, out: impl Write, input: Option); - fn delete_query(&self, query: &str); - fn delete_entries(&self, in_: impl Read); + fn store_entry( + &self, + input: impl Read, + max_dedupe_search: u64, + max_items: u64, + ) -> Result; + fn deduplicate(&self, buf: &[u8], max: u64) -> Result; + fn trim_db(&self, max: u64) -> Result<(), StashError>; + fn delete_last(&self) -> Result<(), StashError>; + fn wipe_db(&self) -> Result<(), StashError>; + fn list_entries(&self, out: impl Write, preview_width: u32) -> Result; + fn decode_entry( + &self, + in_: impl Read, + out: impl Write, + input: Option, + ) -> Result<(), StashError>; + fn delete_query(&self, query: &str) -> Result; + fn delete_entries(&self, in_: impl Read) -> Result; fn next_sequence(&self) -> u64; } @@ -39,20 +94,23 @@ pub struct SledClipboardDb { } impl ClipboardDb for SledClipboardDb { - fn store_entry(&self, mut input: impl Read, max_dedupe_search: u64, max_items: u64) { + fn store_entry( + &self, + mut input: impl Read, + max_dedupe_search: u64, + max_items: u64, + ) -> Result { 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; + return Err(StashError::EmptyOrTooLarge); } if buf.iter().all(u8::is_ascii_whitespace) { - warn!("Input is all whitespace, skipping store."); - return; + return Err(StashError::AllWhitespace); } let mime = detect_mime(&buf); - self.deduplicate(&buf, max_dedupe_search); + self.deduplicate(&buf, max_dedupe_search)?; let entry = Entry { contents: buf.clone(), @@ -60,210 +118,160 @@ impl ClipboardDb for SledClipboardDb { }; let id = self.next_sequence(); - let enc = match to_vec(&entry) { - Ok(enc) => enc, - Err(e) => { - error!("Failed to serialize entry: {e}"); - return; - } - }; + let enc = to_vec(&entry).map_err(|e| StashError::Serialize(e.to_string()))?; - 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); + self.db + .insert(u64_to_ivec(id), enc) + .map_err(|e| StashError::Store(e.to_string()))?; + self.trim_db(max_items)?; + Ok(id) } - fn deduplicate(&self, buf: &[u8], max: u64) { + fn deduplicate(&self, buf: &[u8], max: u64) -> Result { let mut count = 0; let mut deduped = 0; for item in self.db.iter().rev().take(max as usize) { let (k, v) = match item { Ok((k, v)) => (k, v), - Err(e) => { - error!("Error reading entry during deduplication: {e}"); - continue; - } + Err(e) => return Err(StashError::DeduplicationRead(e.to_string())), }; let entry: Entry = match from_read(v.as_ref()) { Ok(e) => e, - Err(e) => { - error!("Error decoding entry during deduplication: {e}"); - continue; - } + Err(e) => return Err(StashError::DeduplicationDecode(e.to_string())), }; if entry.contents == buf { - match self.db.remove(k) { - Ok(_) => { + self.db + .remove(k) + .map(|_| { deduped += 1; - info!("Deduplicated an entry"); - } - Err(e) => error!("Failed to remove entry during deduplication: {e}"), - } + }) + .map_err(|e| StashError::DeduplicationRemove(e.to_string()))?; } count += 1; if count >= max { break; } } - if deduped > 0 { - info!("Deduplicated {deduped} entries"); - } + Ok(deduped) } - fn trim_db(&self, max: u64) { + fn trim_db(&self, max: u64) -> Result<(), StashError> { 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 - } + Err(_e) => None, }) .collect(); - let initial_len = keys.len(); if keys.len() as u64 > max { for k in keys.drain((max as usize)..) { - match self.db.remove(k) { - Ok(_) => info!("Trimmed entry from database"), - Err(e) => error!("Failed to trim entry: {e}"), - } + self.db + .remove(k) + .map_err(|e| StashError::Trim(e.to_string()))?; } - info!( - "Trimmed {} entries from database", - initial_len - max as usize - ); } + Ok(()) } - fn delete_last(&self) { + fn delete_last(&self) -> Result<(), StashError> { if let Some((k, _)) = self.db.iter().next_back().and_then(Result::ok) { - match self.db.remove(k) { - Ok(_) => info!("Deleted last entry"), - Err(e) => error!("Failed to delete last entry: {e}"), - } + self.db + .remove(k) + .map(|_| ()) + .map_err(|e| StashError::DeleteLast(e.to_string())) } else { - warn!("No entries to delete"); + Err(StashError::NoEntriesToDelete) } } - fn wipe_db(&self) { - match self.db.clear() { - Ok(()) => info!("Wiped database"), - Err(e) => error!("Failed to wipe database: {e}"), - } + fn wipe_db(&self) -> Result<(), StashError> { + self.db.clear().map_err(|e| StashError::Wipe(e.to_string())) } - fn list_entries(&self, mut out: impl Write, preview_width: u32) { + fn list_entries(&self, mut out: impl Write, preview_width: u32) -> Result { 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 = match from_read(v.as_ref()) { Ok(e) => e, - Err(e) => { - error!("Failed to decode entry during list: {e}"); - continue; - } + Err(e) => return Err(StashError::ListDecode(e.to_string())), }; let preview = preview_entry(&entry.contents, entry.mime.as_deref(), preview_width); if writeln!(out, "{id}\t{preview}").is_ok() { listed += 1; } } - info!("Listed {listed} entries"); + Ok(listed) } - fn decode_entry(&self, mut in_: impl Read, mut out: impl Write, input: Option) { + fn decode_entry( + &self, + mut in_: impl Read, + mut out: impl Write, + input: Option, + ) -> Result<(), StashError> { let s = if let Some(input) = input { input } else { let mut buf = String::new(); - if let Err(e) = in_.read_to_string(&mut buf) { - error!("Failed to read input for decode: {e}"); - return; - } + in_.read_to_string(&mut buf) + .map_err(|e| StashError::DecodeRead(e.to_string()))?; buf }; - 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}"); - } + let id = extract_id(&s).map_err(|e| StashError::DecodeExtractId(e.to_string()))?; + let v = self + .db + .get(u64_to_ivec(id)) + .map_err(|e| StashError::DecodeGet(e.to_string()))? + .ok_or_else(|| StashError::DecodeNoEntry(id))?; + let entry: Entry = + from_read(v.as_ref()).map_err(|e| StashError::DecodeDecode(e.to_string()))?; + + out.write_all(&entry.contents) + .map_err(|e| StashError::DecodeWrite(e.to_string()))?; + info!("Decoded entry with id {id}"); + Ok(()) } - fn delete_query(&self, query: &str) { + fn delete_query(&self, query: &str) -> Result { let mut deleted = 0; for (k, v) in self.db.iter().filter_map(Result::ok) { let entry: Entry = match from_read(v.as_ref()) { Ok(e) => e, - Err(e) => { - error!("Failed to decode entry during query delete: {e}"); - continue; - } + Err(_) => continue, }; if entry .contents .windows(query.len()) .any(|w| w == query.as_bytes()) { - match self.db.remove(k) { - Ok(_) => { + self.db + .remove(k) + .map(|_| { deleted += 1; - info!("Deleted entry matching query"); - } - Err(e) => error!("Failed to delete entry during query delete: {e}"), - } + }) + .map_err(|e| StashError::QueryDelete(e.to_string()))?; } } - info!("Deleted {deleted} entries matching query '{query}'"); + Ok(deleted) } - fn delete_entries(&self, in_: impl Read) { + fn delete_entries(&self, in_: impl Read) -> Result { 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) { - match self.db.remove(u64_to_ivec(id)) { - Ok(_) => { + self.db + .remove(u64_to_ivec(id)) + .map(|_| { 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}"); + }) + .map_err(|e| StashError::DeleteEntry(id, e.to_string()))?; } } - info!("Deleted {deleted} entries by id from stdin"); + Ok(deleted) } fn next_sequence(&self) -> u64 { diff --git a/src/main.rs b/src/main.rs index b79f9cd..97e4d7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,43 +98,61 @@ fn main() { match cli.command { Some(Command::Store) => { let state = env::var("STASH_CLIPBOARD_STATE").ok(); - db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state); + if let Err(e) = db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state) { + log::error!("Failed to store entry: {e}"); + } } Some(Command::List) => { - db.list(io::stdout(), cli.preview_width); + if let Err(e) = db.list(io::stdout(), cli.preview_width) { + log::error!("Failed to list entries: {e}"); + } } Some(Command::Decode { input }) => { - db.decode(io::stdin(), io::stdout(), input); + if let Err(e) = db.decode(io::stdin(), io::stdout(), input) { + log::error!("Failed to decode entry: {e}"); + } } 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"))); + if let Err(e) = db.delete(Cursor::new(format!("{id}\n"))) { + log::error!("Failed to delete entry by id: {e}"); + } } else { log::error!("Argument is not a valid id"); } } (Some(s), Some("query")) => { - db.query_delete(&s); + if let Err(e) = db.query_delete(&s) { + log::error!("Failed to delete entry by query: {e}"); + } } (Some(s), None) => { if let Ok(id) = s.parse::() { use std::io::Cursor; - db.delete(Cursor::new(format!("{id}\n"))); + if let Err(e) = db.delete(Cursor::new(format!("{id}\n"))) { + log::error!("Failed to delete entry by id: {e}"); + } } else { - db.query_delete(&s); + if let Err(e) = db.query_delete(&s) { + log::error!("Failed to delete entry by query: {e}"); + } } } (None, _) => { - db.delete(io::stdin()); + if let Err(e) = db.delete(io::stdin()) { + log::error!("Failed to delete entry from stdin: {e}"); + } } (_, Some(_)) => { log::error!("Unknown type for --type. Use \"id\" or \"query\"."); } }, Some(Command::Wipe) => { - db.wipe(); + if let Err(e) = db.wipe() { + log::error!("Failed to wipe database: {e}"); + } } Some(Command::Import { r#type }) => { From 7ebe514e3a17adf9f35be5874bc6999d673edd5b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 16:43:43 +0300 Subject: [PATCH 7/8] stash: deduplicate error reporting Signed-off-by: NotAShelf Change-Id: I6a6a6964f0f3f13190dde392fc859718716b56a8 --- src/main.rs | 61 +++++++++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/src/main.rs b/src/main.rs index 97e4d7d..c6c39d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -75,6 +75,16 @@ enum Command { }, } +fn report_error(result: Result, context: &str) -> Option { + match result { + Ok(val) => Some(val), + Err(e) => { + log::error!("{context}: {e}"); + None + } + } +} + fn main() { let cli = Cli::parse(); env_logger::Builder::new() @@ -98,61 +108,58 @@ fn main() { match cli.command { Some(Command::Store) => { let state = env::var("STASH_CLIPBOARD_STATE").ok(); - if let Err(e) = db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state) { - log::error!("Failed to store entry: {e}"); - } + report_error( + db.store(io::stdin(), cli.max_dedupe_search, cli.max_items, state), + "Failed to store entry", + ); } Some(Command::List) => { - if let Err(e) = db.list(io::stdout(), cli.preview_width) { - log::error!("Failed to list entries: {e}"); - } + report_error( + db.list(io::stdout(), cli.preview_width), + "Failed to list entries", + ); } Some(Command::Decode { input }) => { - if let Err(e) = db.decode(io::stdin(), io::stdout(), input) { - log::error!("Failed to decode entry: {e}"); - } + report_error( + db.decode(io::stdin(), io::stdout(), input), + "Failed to decode entry", + ); } 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; - if let Err(e) = db.delete(Cursor::new(format!("{id}\n"))) { - log::error!("Failed to delete entry by id: {e}"); - } + report_error( + db.delete(Cursor::new(format!("{id}\n"))), + "Failed to delete entry by id", + ); } else { log::error!("Argument is not a valid id"); } } (Some(s), Some("query")) => { - if let Err(e) = db.query_delete(&s) { - log::error!("Failed to delete entry by query: {e}"); - } + report_error(db.query_delete(&s), "Failed to delete entry by query"); } (Some(s), None) => { if let Ok(id) = s.parse::() { use std::io::Cursor; - if let Err(e) = db.delete(Cursor::new(format!("{id}\n"))) { - log::error!("Failed to delete entry by id: {e}"); - } + report_error( + db.delete(Cursor::new(format!("{id}\n"))), + "Failed to delete entry by id", + ); } else { - if let Err(e) = db.query_delete(&s) { - log::error!("Failed to delete entry by query: {e}"); - } + report_error(db.query_delete(&s), "Failed to delete entry by query"); } } (None, _) => { - if let Err(e) = db.delete(io::stdin()) { - log::error!("Failed to delete entry from stdin: {e}"); - } + report_error(db.delete(io::stdin()), "Failed to delete entry from stdin"); } (_, Some(_)) => { log::error!("Unknown type for --type. Use \"id\" or \"query\"."); } }, Some(Command::Wipe) => { - if let Err(e) = db.wipe() { - log::error!("Failed to wipe database: {e}"); - } + report_error(db.wipe(), "Failed to wipe database"); } Some(Command::Import { r#type }) => { From aa602edbee5909f260f0db50fb51b8f7f4749d88 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Tue, 12 Aug 2025 16:45:00 +0300 Subject: [PATCH 8/8] treewide: fix remaining clippy warnings Signed-off-by: NotAShelf Change-Id: I6a6a6964a4f18b72e3fa774cec6b81baa8267cb6 --- src/commands/delete.rs | 4 ++-- src/db/mod.rs | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/commands/delete.rs b/src/commands/delete.rs index a2822ad..bd5d5c7 100644 --- a/src/commands/delete.rs +++ b/src/commands/delete.rs @@ -10,11 +10,11 @@ impl DeleteCommand for SledClipboardDb { fn delete(&self, input: impl Read) -> Result { match self.delete_entries(input) { Ok(deleted) => { - log::info!("Deleted {} entries", deleted); + log::info!("Deleted {deleted} entries"); Ok(deleted) } Err(e) => { - log::error!("Failed to delete entries: {}", e); + log::error!("Failed to delete entries: {e}"); Err(e) } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 82156fb..598d70b 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -130,7 +130,12 @@ impl ClipboardDb for SledClipboardDb { fn deduplicate(&self, buf: &[u8], max: u64) -> Result { let mut count = 0; let mut deduped = 0; - for item in self.db.iter().rev().take(max as usize) { + for item in self + .db + .iter() + .rev() + .take(usize::try_from(max).unwrap_or(usize::MAX)) + { let (k, v) = match item { Ok((k, v)) => (k, v), Err(e) => return Err(StashError::DeduplicationRead(e.to_string())), @@ -166,7 +171,7 @@ impl ClipboardDb for SledClipboardDb { }) .collect(); if keys.len() as u64 > max { - for k in keys.drain((max as usize)..) { + for k in keys.drain(usize::try_from(max).unwrap_or(0)..) { self.db .remove(k) .map_err(|e| StashError::Trim(e.to_string()))?; @@ -225,7 +230,7 @@ impl ClipboardDb for SledClipboardDb { .db .get(u64_to_ivec(id)) .map_err(|e| StashError::DecodeGet(e.to_string()))? - .ok_or_else(|| StashError::DecodeNoEntry(id))?; + .ok_or(StashError::DecodeNoEntry(id))?; let entry: Entry = from_read(v.as_ref()).map_err(|e| StashError::DecodeDecode(e.to_string()))?; @@ -361,7 +366,7 @@ pub fn truncate(s: &str, max: usize, ellip: &str) -> String { pub fn size_str(size: usize) -> String { let units = ["B", "KiB", "MiB"]; - let mut fsize = size as f64; + let mut fsize = f64::from(u32::try_from(size).unwrap_or(u32::MAX)); let mut i = 0; while fsize >= 1024.0 && i < units.len() - 1 { fsize /= 1024.0;