use crate::crypto::Crypto; use crate::database::Database; use crate::models::{ClipboardData, ClipboardEntry}; use serde::{Deserialize, Serialize}; use std::fs; use std::io::{Read, Write}; use std::os::unix::net::UnixListener; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const IPC_READ_TIMEOUT: Duration = Duration::from_secs(5); const IPC_MAX_REQUEST_BYTES: usize = 4 * 1024 * 1024; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct HistoryItem { pub content: String, pub timestamp: i64, #[serde(default)] pub pinned: bool, } #[derive(Serialize, Deserialize, Debug)] pub enum IpcRequest { GetHistory { limit: usize, }, SetClipboard { content: String, }, DeleteEntry { content: String, }, UpdateEntry { old_content: String, new_content: String, }, AddEntry { content: String, }, PinEntry { content: String, pinned: bool, }, ClearHistory, } #[derive(Serialize, Deserialize, Debug)] pub enum IpcResponse { History(Vec), Ok, Error(String), } fn reply(stream: &mut std::os::unix::net::UnixStream, resp: IpcResponse) { if let Ok(json) = serde_json::to_string(&resp) { let _ = stream.write_all(json.as_bytes()); } } pub fn start_server( db: Arc>, crypto: Arc, socket_path: &Path, data_dir: Arc, ) { if socket_path.exists() { let _ = fs::remove_file(socket_path); } let listener = match UnixListener::bind(socket_path) { Ok(l) => l, Err(e) => { eprintln!("Erreur socket IPC : {e}"); return; } }; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; if let Err(e) = fs::set_permissions(socket_path, fs::Permissions::from_mode(0o600)) { eprintln!("Impossible de restreindre les permissions du socket : {e}"); } } println!("IPC server en écoute sur {:?}", socket_path); for stream in listener.incoming() { match stream { Ok(mut stream) => { if let Err(e) = stream.set_read_timeout(Some(IPC_READ_TIMEOUT)) { eprintln!("Impossible de définir le timeout IPC : {e}"); } let db_clone = Arc::clone(&db); let crypto_clone = Arc::clone(&crypto); let data_dir_clone = Arc::clone(&data_dir); std::thread::spawn(move || { handle_connection(&mut stream, db_clone, crypto_clone, data_dir_clone); }); } Err(e) => eprintln!("Erreur connexion IPC : {e}"), } } } fn handle_connection( stream: &mut std::os::unix::net::UnixStream, db: Arc>, crypto: Arc, data_dir: Arc, ) { let mut buf = Vec::new(); let mut tmp = [0u8; 4096]; loop { match stream.read(&mut tmp) { Ok(0) => break, Ok(n) => { buf.extend_from_slice(&tmp[..n]); if buf.len() > IPC_MAX_REQUEST_BYTES { eprintln!("IPC : requête trop grande, abandon"); return; } } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => { eprintln!("IPC : timeout de lecture"); return; } Err(e) => { eprintln!("IPC read error : {e}"); return; } } } let buf_str = match String::from_utf8(buf) { Ok(s) => s, Err(e) => { eprintln!("IPC : requête non-UTF8 : {e}"); return; } }; let req = match serde_json::from_str::(&buf_str) { Ok(r) => r, Err(e) => { eprintln!("IPC parse error : {e}"); return; } }; match req { IpcRequest::GetHistory { limit } => { let limit = limit.min(1000); let lock = db.lock().unwrap_or_else(|p| p.into_inner()); let history = lock.read_history(limit).unwrap_or_default(); let items: Vec = history .into_iter() .map(|e| { let content = match e.content { ClipboardData::Text(t) => t, ClipboardData::Image(img) => format!("{}.jpg", img.id), }; let ts = e .timestamp .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis() as i64; HistoryItem { content, timestamp: ts, pinned: e.pinned, } }) .collect(); reply(stream, IpcResponse::History(items)); } IpcRequest::SetClipboard { content } => { let actual = if Crypto::is_legacy_encrypted(&content) { crypto.decrypt(&content).unwrap_or_else(|e| { eprintln!("Impossible de déchiffrer l'entrée enc: : {e}"); content.clone() }) } else if Crypto::is_password_encrypted(&content) { reply( stream, IpcResponse::Error( "Entrée chiffrée par mot de passe : déchiffrez côté client avant de coller" .to_string(), ), ); return; } else { content }; match arboard::Clipboard::new() { Ok(mut cb) => { if actual.ends_with(".jpg") || actual.ends_with(".png") { let path = data_dir.join("images").join(&actual); if let Ok(img) = image::open(&path) { let rgba = img.into_rgba8(); let (w, h) = (rgba.width() as usize, rgba.height() as usize); let _ = cb.set_image(arboard::ImageData { width: w, height: h, bytes: std::borrow::Cow::Owned(rgba.into_raw()), }); reply(stream, IpcResponse::Ok); } else { reply( stream, IpcResponse::Error(format!("Image introuvable : {actual}")), ); } } else { let _ = cb.set_text(actual); reply(stream, IpcResponse::Ok); } } Err(e) => reply(stream, IpcResponse::Error(e.to_string())), } } IpcRequest::DeleteEntry { content } => { { let lock = db.lock().unwrap_or_else(|p| p.into_inner()); let _ = lock.delete_entry_by_content(&content); } if !Crypto::is_any_encrypted(&content) && (content.ends_with(".jpg") || content.ends_with(".png")) { let p = data_dir.join("images").join(&content); if p.exists() { let _ = fs::remove_file(p); } } reply(stream, IpcResponse::Ok); } IpcRequest::UpdateEntry { old_content, new_content, } => { let lock = db.lock().unwrap_or_else(|p| p.into_inner()); match lock.update_entry_content(&old_content, &new_content) { Ok(_) => reply(stream, IpcResponse::Ok), Err(e) => reply(stream, IpcResponse::Error(e.to_string())), } } IpcRequest::AddEntry { content } => { let entry = ClipboardEntry { content: ClipboardData::Text(content), timestamp: SystemTime::now(), pinned: false, }; let lock = db.lock().unwrap_or_else(|p| p.into_inner()); match lock.append(entry) { Ok(_) => reply(stream, IpcResponse::Ok), Err(e) => reply(stream, IpcResponse::Error(e.to_string())), } } IpcRequest::PinEntry { content, pinned } => { let lock = db.lock().unwrap_or_else(|p| p.into_inner()); match lock.set_pin(&content, pinned) { Ok(_) => reply(stream, IpcResponse::Ok), Err(e) => reply(stream, IpcResponse::Error(e.to_string())), } } IpcRequest::ClearHistory => { let lock = db.lock().unwrap_or_else(|p| p.into_inner()); match lock.clear_history() { Ok(_) => reply(stream, IpcResponse::Ok), Err(e) => reply(stream, IpcResponse::Error(e.to_string())), } } } }