stash: deduplicate Fnv1aHasher; add derive for u64 wrapper

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic2886815721f6eefc66a8ddacd44fb286a6a6964
This commit is contained in:
raf 2026-04-01 14:38:47 +03:00
commit d643376cd7
4 changed files with 108 additions and 58 deletions

View file

@ -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 smol::Timer;
use wl_clipboard_rs::{ use wl_clipboard_rs::{
@ -15,36 +15,9 @@ use wl_clipboard_rs::{
use crate::{ use crate::{
clipboard::{self, ClipboardData, get_serving_pid}, clipboard::{self, ClipboardData, get_serving_pid},
db::{SqliteClipboardDb, nonblocking::AsyncClipboardDb}, 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. /// Wrapper to provide [`Ord`] implementation for `f64` by negating values.
/// This allows [`BinaryHeap`], which is a max-heap, to function as a min-heap. /// This allows [`BinaryHeap`], which is a max-heap, to function as a min-heap.
/// Also see: /// Also see:

View file

@ -11,6 +11,10 @@ use std::{
pub mod nonblocking; pub mod nonblocking;
use std::hash::Hasher;
use crate::hash::Fnv1aHasher;
/// Cache for process scanning results to avoid expensive `/proc` reads on every /// Cache for process scanning results to avoid expensive `/proc` reads on every
/// store operation. TTL of 5 seconds balances freshness with performance. /// store operation. TTL of 5 seconds balances freshness with performance.
struct ProcessCache { 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 base64::prelude::*;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use mime_sniffer::MimeTypeSniffer; use mime_sniffer::MimeTypeSniffer;

101
src/hash.rs Normal file
View file

@ -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());
}
}

View file

@ -1,6 +1,7 @@
mod clipboard; mod clipboard;
mod commands; mod commands;
mod db; mod db;
mod hash;
mod mime; mod mime;
mod multicall; mod multicall;