diff --git a/src/commands/store.rs b/src/commands/store.rs index 9e5a6c6..3854b16 100644 --- a/src/commands/store.rs +++ b/src/commands/store.rs @@ -2,6 +2,7 @@ use std::io::Read; use crate::db::{ClipboardDb, SqliteClipboardDb}; +#[allow(clippy::too_many_arguments)] pub trait StoreCommand { fn store( &self, @@ -10,6 +11,8 @@ pub trait StoreCommand { max_items: u64, state: Option, excluded_apps: &[String], + min_size: Option, + max_size: usize, ) -> Result<(), crate::db::StashError>; } @@ -21,6 +24,8 @@ impl StoreCommand for SqliteClipboardDb { max_items: u64, state: Option, excluded_apps: &[String], + min_size: Option, + max_size: usize, ) -> Result<(), crate::db::StashError> { if let Some("sensitive" | "clear") = state.as_deref() { self.delete_last()?; @@ -31,6 +36,8 @@ impl StoreCommand for SqliteClipboardDb { max_dedupe_search, max_items, Some(excluded_apps), + min_size, + max_size, )?; log::info!("Entry stored"); } diff --git a/src/commands/watch.rs b/src/commands/watch.rs index 54dc803..fbc7239 100644 --- a/src/commands/watch.rs +++ b/src/commands/watch.rs @@ -175,6 +175,7 @@ fn negotiate_mime_type( } } +#[allow(clippy::too_many_arguments)] pub trait WatchCommand { fn watch( &self, @@ -183,6 +184,8 @@ pub trait WatchCommand { excluded_apps: &[String], expire_after: Option, mime_type_preference: &str, + min_size: Option, + max_size: usize, ); } @@ -194,6 +197,8 @@ impl WatchCommand for SqliteClipboardDb { excluded_apps: &[String], expire_after: Option, mime_type_preference: &str, + min_size: Option, + max_size: usize, ) { smol::block_on(async { log::info!( @@ -349,6 +354,8 @@ impl WatchCommand for SqliteClipboardDb { max_dedupe_search, max_items, Some(excluded_apps), + min_size, + max_size, ) { Ok(id) => { log::info!("Stored new clipboard entry (id: {id})"); diff --git a/src/db/mod.rs b/src/db/mod.rs index e55f426..ae8d814 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -16,6 +16,8 @@ use rusqlite::{Connection, OptionalExtension, params}; use serde::{Deserialize, Serialize}; use thiserror::Error; +pub const DEFAULT_MAX_ENTRY_SIZE: usize = 5_000_000; + #[derive(Error, Debug)] pub enum StashError { #[error("Input is empty or too large, skipping store.")] @@ -70,7 +72,7 @@ pub trait ClipboardDb { max_items: u64, excluded_apps: Option<&[String]>, min_size: Option, - max_size: Option, + max_size: usize, ) -> Result; fn deduplicate_by_hash( @@ -417,7 +419,7 @@ impl ClipboardDb for SqliteClipboardDb { max_items: u64, excluded_apps: Option<&[String]>, min_size: Option, - max_size: Option, + max_size: usize, ) -> Result { let mut buf = Vec::new(); if input.read_to_end(&mut buf).is_err() || buf.is_empty() { @@ -432,12 +434,8 @@ impl ClipboardDb for SqliteClipboardDb { return Err(StashError::TooSmall(min)); } - if let Some(max) = max_size { - if size > max { - return Err(StashError::TooLarge(max)); - } - } else if size > 5 * 1_000_000 { - return Err(StashError::TooLarge(5 * 1_000_000)); + if size > max_size { + return Err(StashError::TooLarge(max_size)); } if buf.iter().all(u8::is_ascii_whitespace) { @@ -1536,7 +1534,7 @@ mod tests { let cursor = std::io::Cursor::new(test_data.to_vec()); let id = db - .store_entry(cursor, 100, 1000, None, None, None) + .store_entry(cursor, 100, 1000, None, None, DEFAULT_MAX_ENTRY_SIZE) .expect("Failed to store entry"); let content_hash: Option = db @@ -1571,7 +1569,7 @@ mod tests { let test_data = b"Test content for copy"; let cursor = std::io::Cursor::new(test_data.to_vec()); let id_a = db - .store_entry(cursor, 100, 1000, None, None, None) + .store_entry(cursor, 100, 1000, None, None, DEFAULT_MAX_ENTRY_SIZE) .expect("Failed to store entry A"); let original_last_accessed: i64 = db @@ -1672,7 +1670,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store URI list"); @@ -1705,7 +1703,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store image"); @@ -1733,7 +1731,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store first"); let _id2 = db @@ -1743,7 +1741,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store second"); @@ -1778,7 +1776,7 @@ mod tests { 3, // max 3 items None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store"); } @@ -1799,7 +1797,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ); assert!(matches!(result, Err(StashError::EmptyOrTooLarge))); } @@ -1813,7 +1811,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ); assert!(matches!(result, Err(StashError::AllWhitespace))); } @@ -1823,8 +1821,14 @@ mod tests { let db = test_db(); // 5MB + 1 byte let data = vec![b'a'; 5 * 1_000_000 + 1]; - let result = - db.store_entry(std::io::Cursor::new(data), 100, 1000, None, None, None); + let result = db.store_entry( + std::io::Cursor::new(data), + 100, + 1000, + None, + None, + DEFAULT_MAX_ENTRY_SIZE, + ); assert!(matches!(result, Err(StashError::TooLarge(5000000)))); } @@ -1838,7 +1842,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store"); @@ -1864,7 +1868,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store"); db.store_entry( @@ -1873,7 +1877,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store"); @@ -1900,7 +1904,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store"); } @@ -1970,7 +1974,7 @@ mod tests { 1000, None, None, - None, + DEFAULT_MAX_ENTRY_SIZE, ) .expect("Failed to store"); diff --git a/src/main.rs b/src/main.rs index 56c2170..ef12ed1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,15 +15,18 @@ pub(crate) mod mime; mod multicall; #[cfg(feature = "use-toplevel")] mod wayland; -use crate::commands::{ - decode::DecodeCommand, - delete::DeleteCommand, - import::ImportCommand, - list::ListCommand, - query::QueryCommand, - store::StoreCommand, - watch::WatchCommand, - wipe::WipeCommand, +use crate::{ + commands::{ + decode::DecodeCommand, + delete::DeleteCommand, + import::ImportCommand, + list::ListCommand, + query::QueryCommand, + store::StoreCommand, + watch::WatchCommand, + wipe::WipeCommand, + }, + db::DEFAULT_MAX_ENTRY_SIZE, }; #[derive(Parser)] @@ -42,6 +45,16 @@ struct Cli { #[arg(long, default_value_t = 20)] max_dedupe_search: u64, + /// Minimum size (in bytes) for clipboard entries. Entries smaller than this + /// will not be stored. + #[arg(long, env = "STASH_MIN_SIZE")] + min_size: Option, + + /// Maximum size (in bytes) for clipboard entries. Entries larger than this + /// will not be stored. Defaults to 5MB. + #[arg(long, default_value_t = DEFAULT_MAX_ENTRY_SIZE, env = "STASH_MAX_SIZE")] + max_size: usize, + /// Maximum width (in characters) for clipboard entry previews in list /// output. #[arg(long, default_value_t = 100)] @@ -226,6 +239,8 @@ fn main() -> color_eyre::eyre::Result<()> { &cli.excluded_apps, #[cfg(not(feature = "use-toplevel"))] &[], + cli.min_size, + cli.max_size, ), "failed to store entry", ); @@ -451,6 +466,8 @@ fn main() -> color_eyre::eyre::Result<()> { &[], expire_after, &mime_type, + cli.min_size, + cli.max_size, ); },