diff --git a/src/commands/watch.rs b/src/commands/watch.rs index ddfdbea..542937d 100644 --- a/src/commands/watch.rs +++ b/src/commands/watch.rs @@ -1,4 +1,4 @@ -use std::{collections::BinaryHeap, io::Read, time::Duration}; +use std::{collections::BinaryHeap, hash::Hasher, io::Read, time::Duration}; use smol::Timer; use wl_clipboard_rs::{ @@ -15,36 +15,9 @@ use wl_clipboard_rs::{ use crate::{ clipboard::{self, ClipboardData, get_serving_pid}, db::{SqliteClipboardDb, nonblocking::AsyncClipboardDb}, + hash::Fnv1aHasher, }; -/// FNV-1a hasher for deterministic hashing across process runs. -/// Unlike `DefaultHasher` (`SipHash`), this produces stable hashes. -struct Fnv1aHasher { - state: u64, -} - -impl Fnv1aHasher { - const FNV_OFFSET: u64 = 0xCBF29CE484222325; - const FNV_PRIME: u64 = 0x100000001B3; - - fn new() -> Self { - Self { - state: Self::FNV_OFFSET, - } - } - - fn write(&mut self, bytes: &[u8]) { - for byte in bytes { - self.state ^= u64::from(*byte); - self.state = self.state.wrapping_mul(Self::FNV_PRIME); - } - } - - fn finish(&self) -> u64 { - self.state - } -} - /// Wrapper to provide [`Ord`] implementation for `f64` by negating values. /// This allows [`BinaryHeap`], which is a max-heap, to function as a min-heap. /// Also see: diff --git a/src/db/mod.rs b/src/db/mod.rs index 441495f..f907b3b 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -11,6 +11,10 @@ use std::{ pub mod nonblocking; +use std::hash::Hasher; + +use crate::hash::Fnv1aHasher; + /// Cache for process scanning results to avoid expensive `/proc` reads on every /// store operation. TTL of 5 seconds balances freshness with performance. struct ProcessCache { @@ -66,35 +70,6 @@ impl ProcessCache { } } -/// FNV-1a hasher for deterministic hashing across process runs. -/// Unlike `DefaultHasher` (`SipHash` with random seed), this produces stable -/// hashes. -pub struct Fnv1aHasher { - state: u64, -} - -impl Fnv1aHasher { - const FNV_OFFSET: u64 = 0xCBF29CE484222325; - const FNV_PRIME: u64 = 0x100000001B3; - - pub fn new() -> Self { - Self { - state: Self::FNV_OFFSET, - } - } - - pub fn write(&mut self, bytes: &[u8]) { - for byte in bytes { - self.state ^= u64::from(*byte); - self.state = self.state.wrapping_mul(Self::FNV_PRIME); - } - } - - pub fn finish(&self) -> u64 { - self.state - } -} - use base64::prelude::*; use log::{debug, error, info, warn}; use mime_sniffer::MimeTypeSniffer; diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..f017a51 --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,101 @@ +/// FNV-1a hasher for deterministic hashing across process runs. +/// +/// Unlike `std::collections::hash_map::DefaultHasher` (which uses SipHash +/// with a random seed), this produces stable hashes suitable for persistent +/// storage and cross-process comparison. +/// +/// # Example +/// +/// ``` +/// use std::hash::Hasher; +/// +/// use stash::hash::Fnv1aHasher; +/// +/// let mut hasher = Fnv1aHasher::new(); +/// hasher.write(b"hello"); +/// let hash = hasher.finish(); +/// ``` +#[derive(Clone, Copy, Debug)] +pub struct Fnv1aHasher { + state: u64, +} + +impl Fnv1aHasher { + const FNV_OFFSET: u64 = 0xCBF29CE484222325; + const FNV_PRIME: u64 = 0x100000001B3; + + /// Creates a new hasher initialized with the FNV-1a offset basis. + #[must_use] + pub fn new() -> Self { + Self { + state: Self::FNV_OFFSET, + } + } +} + +impl Default for Fnv1aHasher { + fn default() -> Self { + Self::new() + } +} + +impl std::hash::Hasher for Fnv1aHasher { + fn write(&mut self, bytes: &[u8]) { + for byte in bytes { + self.state ^= u64::from(*byte); + self.state = self.state.wrapping_mul(Self::FNV_PRIME); + } + } + + fn finish(&self) -> u64 { + self.state + } +} + +#[cfg(test)] +mod tests { + use std::hash::Hasher; + + use super::*; + + #[test] + fn test_fnv1a_basic() { + let mut hasher = Fnv1aHasher::new(); + hasher.write(b"hello"); + // FNV-1a hash for "hello" (little-endian u64) + assert_eq!(hasher.finish(), 0xA430D84680AABD0B); + } + + #[test] + fn test_fnv1a_empty() { + let hasher = Fnv1aHasher::new(); + // Empty input should return offset basis + assert_eq!(hasher.finish(), Fnv1aHasher::FNV_OFFSET); + } + + #[test] + fn test_fnv1a_deterministic() { + // Same input must produce same hash + let mut h1 = Fnv1aHasher::new(); + let mut h2 = Fnv1aHasher::new(); + h1.write(b"test data"); + h2.write(b"test data"); + assert_eq!(h1.finish(), h2.finish()); + } + + #[test] + fn test_default_trait() { + let h1 = Fnv1aHasher::new(); + let h2 = Fnv1aHasher::default(); + assert_eq!(h1.finish(), h2.finish()); + } + + #[test] + fn test_copy_trait() { + let mut hasher = Fnv1aHasher::new(); + hasher.write(b"data"); + let copied = hasher; + // Both should have same state after copy + assert_eq!(hasher.finish(), copied.finish()); + } +} diff --git a/src/main.rs b/src/main.rs index 53ed1c9..32c271d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod clipboard; mod commands; mod db; +mod hash; mod mime; mod multicall;