use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use chrono::Utc;
use sha2::{Digest, Sha256};
use uuid::Uuid;
use super::db::ClipboardDb;
use super::entry::{ClipboardContent, ClipboardEntry};
use super::sensitive::SensitiveDetector;
const POLL_INTERVAL_MS: u64 = 500;
pub struct ClipboardMonitor {
handle: Option<JoinHandle<()>>,
stop_signal: Arc<AtomicBool>,
last_hash: Arc<Mutex<String>>,
running: Arc<AtomicBool>,
}
impl ClipboardMonitor {
pub fn new() -> Self {
Self {
handle: None,
stop_signal: Arc::new(AtomicBool::new(false)),
last_hash: Arc::new(Mutex::new(String::new())),
running: Arc::new(AtomicBool::new(false)),
}
}
pub fn start(&mut self, db: Arc<Mutex<ClipboardDb>>) -> Result<(), String> {
if self.running.load(Ordering::SeqCst) {
return Err("Monitor is already running".to_string());
}
self.stop_signal.store(false, Ordering::SeqCst);
self.running.store(true, Ordering::SeqCst);
let stop_signal = Arc::clone(&self.stop_signal);
let last_hash = Arc::clone(&self.last_hash);
let running = Arc::clone(&self.running);
let handle = thread::spawn(move || {
let detector = SensitiveDetector::new();
while !stop_signal.load(Ordering::SeqCst) {
if let Some((content, hash)) = read_clipboard() {
let mut last = last_hash.lock().unwrap();
if *last != hash {
*last = hash.clone();
drop(last);
let mut entry = ClipboardEntry::new(
Uuid::new_v4().to_string(),
content,
&hash,
Utc::now().to_rfc3339(),
);
if let ClipboardContent::Text(ref text) = entry.content {
if let Some(sensitive_type) = detector.detect(text) {
entry.is_sensitive = true;
entry.sensitive_type = Some(sensitive_type);
}
}
if let Ok(db) = db.lock() {
let _ = db.insert_entry(&entry);
}
}
}
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
running.store(false, Ordering::SeqCst);
});
self.handle = Some(handle);
Ok(())
}
pub fn stop(&mut self) {
self.stop_signal.store(true, Ordering::SeqCst);
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::SeqCst)
}
pub fn last_hash(&self) -> String {
self.last_hash.lock().unwrap().clone()
}
}
impl Default for ClipboardMonitor {
fn default() -> Self {
Self::new()
}
}
impl Drop for ClipboardMonitor {
fn drop(&mut self) {
self.stop();
}
}
fn read_clipboard() -> Option<(ClipboardContent, String)> {
use arboard::Clipboard;
let mut clipboard = Clipboard::new().ok()?;
if let Ok(text) = clipboard.get_text() {
if !text.is_empty() {
let hash = compute_hash(text.as_bytes());
return Some((ClipboardContent::Text(text), hash));
}
}
if let Ok(image) = clipboard.get_image() {
let rgba_data = image.bytes.into_owned();
let width = image.width as u32;
let height = image.height as u32;
if let Some(img) = image::RgbaImage::from_raw(width, height, rgba_data.clone()) {
let mut png_data = Vec::new();
if let Ok(()) =
img.write_to(&mut std::io::Cursor::new(&mut png_data), image::ImageFormat::Png)
{
let hash = compute_hash(&png_data);
return Some((ClipboardContent::Image { data: png_data, width, height }, hash));
}
}
let hash = compute_hash(&rgba_data);
return Some((ClipboardContent::Image { data: rgba_data, width, height }, hash));
}
None
}
fn compute_hash(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
hex::encode(result)
}
mod hex {
pub fn encode(bytes: impl AsRef<[u8]>) -> String {
bytes.as_ref().iter().map(|b| format!("{:02x}", b)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_hash() {
let hash1 = compute_hash(b"Hello, World!");
let hash2 = compute_hash(b"Hello, World!");
let hash3 = compute_hash(b"Different content");
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
assert_eq!(hash1.len(), 64); }
#[test]
fn test_hex_encode() {
assert_eq!(hex::encode([0x00]), "00");
assert_eq!(hex::encode([0xff]), "ff");
assert_eq!(hex::encode([0x01, 0x02, 0x03]), "010203");
}
#[test]
fn test_monitor_lifecycle() {
let monitor = ClipboardMonitor::new();
assert!(!monitor.is_running());
}
}