Files
nomina/crates/nomina-core/src/rules/hash.rs
lashman 6f5b862234 pipeline cards, context menus, presets, settings overhaul
rewrote pipeline as draggable card strip with per-rule config popovers,
added right-click menus to pipeline cards, sidebar tree, and file list,
preset import/export with BRU format support, new rules (hash, swap,
truncate, sanitize, padding, randomize, text editor, folder name,
transliterate), settings dialog with all sections, overlay collision
containment, tooltips on icon buttons, empty pipeline default
2026-03-14 19:04:35 +02:00

143 lines
3.5 KiB
Rust

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");
}
}