use serde::{Deserialize, Serialize}; use sha2::Digest; use crate::{RenameContext, RenameRule}; #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] pub enum HashMode { #[default] None, Prefix, Suffix, Replace, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] pub enum HashAlgorithm { MD5, SHA1, #[default] SHA256, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HashRule { pub mode: HashMode, pub algorithm: HashAlgorithm, pub length: usize, pub separator: String, pub uppercase: bool, pub enabled: bool, } impl HashRule { pub fn new() -> Self { Self { mode: HashMode::None, algorithm: HashAlgorithm::SHA256, length: 0, separator: "_".into(), uppercase: false, enabled: true, } } fn compute_hash(&self, data: &[u8]) -> String { let raw = match self.algorithm { HashAlgorithm::MD5 => { let mut h = md5::Md5::new(); h.update(data); format!("{:x}", h.finalize()) } HashAlgorithm::SHA1 => { let mut h = sha1::Sha1::new(); h.update(data); format!("{:x}", h.finalize()) } HashAlgorithm::SHA256 => { let mut h = sha2::Sha256::new(); h.update(data); format!("{:x}", h.finalize()) } }; let truncated = if self.length > 0 && self.length < raw.len() { &raw[..self.length] } else { &raw }; if self.uppercase { truncated.to_uppercase() } else { truncated.to_string() } } } impl RenameRule for HashRule { fn apply(&self, filename: &str, context: &RenameContext) -> String { if self.mode == HashMode::None { return filename.to_string(); } let data = match std::fs::read(&context.path) { Ok(d) => d, Err(_) => return filename.to_string(), }; let hash = self.compute_hash(&data); match self.mode { HashMode::None => filename.to_string(), HashMode::Prefix => format!("{}{}{}", hash, self.separator, filename), HashMode::Suffix => format!("{}{}{}", filename, self.separator, hash), HashMode::Replace => hash, } } fn display_name(&self) -> &str { "Hash" } fn rule_type(&self) -> &str { "hash" } fn is_enabled(&self) -> bool { self.enabled } } #[cfg(test)] mod tests { use super::*; #[test] fn mode_none_returns_unchanged() { let rule = HashRule::new(); let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("photo.jpg", &ctx), "photo.jpg"); } #[test] fn file_read_fails_returns_unchanged() { let mut rule = HashRule::new(); rule.mode = HashMode::Prefix; let ctx = RenameContext::dummy(0); // dummy context points to a nonexistent file, so read fails assert_eq!(rule.apply("photo.jpg", &ctx), "photo.jpg"); } #[test] fn suffix_mode_read_fail_unchanged() { let mut rule = HashRule::new(); rule.mode = HashMode::Suffix; rule.algorithm = HashAlgorithm::MD5; rule.length = 8; rule.uppercase = true; let ctx = RenameContext::dummy(0); assert_eq!(rule.apply("doc.pdf", &ctx), "doc.pdf"); } }