use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HashAlgorithm {
SipHash,
Fnv1a32,
Fnv1a64,
}
impl Default for HashAlgorithm {
fn default() -> Self {
Self::SipHash
}
}
impl HashAlgorithm {
pub fn as_str(&self) -> &'static str {
match self {
Self::SipHash => "siphash",
Self::Fnv1a32 => "fnv1a32",
Self::Fnv1a64 => "fnv1a64",
}
}
pub fn parse_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"siphash" => Some(Self::SipHash),
"fnv1a32" => Some(Self::Fnv1a32),
"fnv1a64" => Some(Self::Fnv1a64),
_ => None,
}
}
pub fn hash_hex(&self, input: &str) -> String {
match self {
Self::SipHash => {
let mut hasher = DefaultHasher::new();
input.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
Self::Fnv1a32 => format!("{:08x}", fnv1a_32(input.as_bytes())),
Self::Fnv1a64 => format!("{:016x}", fnv1a_64(input.as_bytes())),
}
}
}
fn fnv1a_32(bytes: &[u8]) -> u32 {
const FNV_OFFSET: u32 = 0x811c9dc5;
const FNV_PRIME: u32 = 0x01000193;
let mut hash = FNV_OFFSET;
for &byte in bytes {
hash ^= u32::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
fn fnv1a_64(bytes: &[u8]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x00000100000001b3;
let mut hash = FNV_OFFSET;
for &byte in bytes {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[derive(Debug, Clone)]
pub struct HashMasker {
algorithm: HashAlgorithm,
keep_prefix: usize,
suffix: String,
}
impl Default for HashMasker {
fn default() -> Self {
Self {
algorithm: HashAlgorithm::default(),
keep_prefix: 12,
suffix: "...".to_string(),
}
}
}
impl HashMasker {
pub fn new() -> Self {
Self::default()
}
pub fn with_algorithm(mut self, algo: HashAlgorithm) -> Self {
self.algorithm = algo;
self
}
pub fn with_keep_prefix(mut self, n: usize) -> Self {
self.keep_prefix = n;
self
}
pub fn with_suffix(mut self, suffix: &str) -> Self {
self.suffix = suffix.to_string();
self
}
pub fn algorithm(&self) -> HashAlgorithm {
self.algorithm
}
pub fn keep_prefix(&self) -> usize {
self.keep_prefix
}
pub fn suffix(&self) -> &str {
&self.suffix
}
pub fn mask(&self, value: &str) -> String {
if value.is_empty() {
return self.suffix.clone();
}
let hex = self.algorithm.hash_hex(value);
let prefix = if hex.len() <= self.keep_prefix {
hex.as_str()
} else {
&hex[..self.keep_prefix]
};
format!("{}{}", prefix, self.suffix)
}
pub fn mask_fields(
&self,
fields: &[String],
data: &HashMap<String, String>,
) -> HashMap<String, String> {
data.iter()
.map(|(k, v)| {
if fields.contains(k) {
(k.clone(), self.mask(v))
} else {
(k.clone(), v.clone())
}
})
.collect()
}
pub fn mask_json(&self, fields: &[String], json: &str) -> String {
let Ok(mut value) = serde_json::from_str::<serde_json::Value>(json) else {
return json.to_string();
};
let Some(obj) = value.as_object_mut() else {
return json.to_string();
};
for field in fields {
if let Some(serde_json::Value::String(s)) = obj.get_mut(field) {
*s = self.mask(s);
}
}
serde_json::to_string(&value).unwrap_or_else(|_| json.to_string())
}
pub fn mask_batch(&self, values: &[String]) -> Vec<String> {
values.iter().map(|v| self.mask(v)).collect()
}
}
#[derive(Debug, Clone)]
pub struct PartialDisplayMasker {
prefix_keep: usize,
suffix_keep: usize,
mask_char: char,
min_mask_length: usize,
fallback: String,
}
impl Default for PartialDisplayMasker {
fn default() -> Self {
Self {
prefix_keep: 3,
suffix_keep: 4,
mask_char: '*',
min_mask_length: 3,
fallback: "***".to_string(),
}
}
}
impl PartialDisplayMasker {
pub fn new() -> Self {
Self::default()
}
pub fn with_prefix(mut self, n: usize) -> Self {
self.prefix_keep = n;
self
}
pub fn with_suffix_keep(mut self, n: usize) -> Self {
self.suffix_keep = n;
self
}
pub fn with_mask_char(mut self, c: char) -> Self {
self.mask_char = c;
self
}
pub fn with_min_mask_length(mut self, n: usize) -> Self {
self.min_mask_length = n;
self
}
pub fn with_fallback(mut self, fallback: &str) -> Self {
self.fallback = fallback.to_string();
self
}
pub fn prefix_keep(&self) -> usize {
self.prefix_keep
}
pub fn suffix_keep(&self) -> usize {
self.suffix_keep
}
pub fn mask_char(&self) -> char {
self.mask_char
}
pub fn mask(&self, value: &str) -> String {
let chars: Vec<char> = value.chars().collect();
let len = chars.len();
if len == 0 {
return self.fallback.clone();
}
let need = self.prefix_keep + self.suffix_keep;
if len <= need {
return self.fallback.clone();
}
let hidden = len - need;
let mask_len = hidden.max(self.min_mask_length);
let mut out = String::with_capacity(len + mask_len);
for &c in &chars[..self.prefix_keep] {
out.push(c);
}
for _ in 0..mask_len {
out.push(self.mask_char);
}
for &c in &chars[len - self.suffix_keep..] {
out.push(c);
}
out
}
pub fn mask_fields(
&self,
fields: &[String],
data: &HashMap<String, String>,
) -> HashMap<String, String> {
data.iter()
.map(|(k, v)| {
if fields.contains(k) {
(k.clone(), self.mask(v))
} else {
(k.clone(), v.clone())
}
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct PatternMasker {
patterns: Vec<(String, crate::MaskingRule)>,
}
impl Default for PatternMasker {
fn default() -> Self {
Self {
patterns: Vec::new(),
}
}
}
impl PatternMasker {
pub fn new() -> Self {
Self::default()
}
pub fn add_pattern(mut self, pattern: &str, rule: crate::MaskingRule) -> Self {
self.patterns.push((pattern.to_string(), rule));
self
}
pub fn pattern_count(&self) -> usize {
self.patterns.len()
}
pub fn clear(&mut self) {
self.patterns.clear();
}
pub fn match_rule(&self, field: &str) -> Option<&crate::MaskingRule> {
self.patterns
.iter()
.find(|(pat, _)| wildcard_match(pat, field))
.map(|(_, rule)| rule)
}
pub fn mask_map(&self, data: &HashMap<String, String>) -> HashMap<String, String> {
data.iter()
.map(|(k, v)| match self.match_rule(k) {
Some(rule) => (k.clone(), crate::DataMasker::apply(rule, v)),
None => (k.clone(), v.clone()),
})
.collect()
}
pub fn mask_json(&self, json: &str) -> String {
let Ok(mut value) = serde_json::from_str::<serde_json::Value>(json) else {
return json.to_string();
};
let Some(obj) = value.as_object_mut() else {
return json.to_string();
};
let keys: Vec<String> = obj.keys().cloned().collect();
for key in keys {
if let Some(rule) = self.match_rule(&key) {
if let Some(serde_json::Value::String(s)) = obj.get_mut(&key) {
*s = crate::DataMasker::apply(rule, s);
}
}
}
serde_json::to_string(&value).unwrap_or_else(|_| json.to_string())
}
}
pub fn wildcard_match(pattern: &str, text: &str) -> bool {
let pat: Vec<char> = pattern.chars().collect();
let txt: Vec<char> = text.chars().collect();
let m = pat.len();
let n = txt.len();
let mut dp = vec![vec![false; n + 1]; m + 1];
dp[0][0] = true;
for i in 1..=m {
if pat[i - 1] == '*' {
dp[i][0] = dp[i - 1][0];
}
}
for i in 1..=m {
for j in 1..=n {
match pat[i - 1] {
'*' => dp[i][j] = dp[i - 1][j] || dp[i][j - 1],
'?' => dp[i][j] = dp[i - 1][j - 1],
c => dp[i][j] = dp[i - 1][j - 1] && c == txt[j - 1],
}
}
}
dp[m][n]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_algorithm_default_is_siphash() {
assert_eq!(HashAlgorithm::default(), HashAlgorithm::SipHash);
}
#[test]
fn hash_algorithm_as_str() {
assert_eq!(HashAlgorithm::SipHash.as_str(), "siphash");
assert_eq!(HashAlgorithm::Fnv1a32.as_str(), "fnv1a32");
assert_eq!(HashAlgorithm::Fnv1a64.as_str(), "fnv1a64");
}
#[test]
fn hash_algorithm_parse_name_valid() {
assert_eq!(
HashAlgorithm::parse_name("siphash"),
Some(HashAlgorithm::SipHash)
);
assert_eq!(
HashAlgorithm::parse_name("FNV1A32"),
Some(HashAlgorithm::Fnv1a32)
);
assert_eq!(
HashAlgorithm::parse_name("fnv1a64"),
Some(HashAlgorithm::Fnv1a64)
);
}
#[test]
fn hash_algorithm_parse_name_invalid() {
assert_eq!(HashAlgorithm::parse_name("md5"), None);
assert_eq!(HashAlgorithm::parse_name(""), None);
}
#[test]
fn hash_algorithm_siphash_deterministic() {
let a = HashAlgorithm::SipHash.hash_hex("hello");
let b = HashAlgorithm::SipHash.hash_hex("hello");
assert_eq!(a, b);
assert_eq!(a.len(), 16);
}
#[test]
fn hash_algorithm_fnv1a32_deterministic() {
let a = HashAlgorithm::Fnv1a32.hash_hex("test");
let b = HashAlgorithm::Fnv1a32.hash_hex("test");
assert_eq!(a, b);
assert_eq!(a.len(), 8);
}
#[test]
fn hash_algorithm_fnv1a64_deterministic() {
let a = HashAlgorithm::Fnv1a64.hash_hex("test");
let b = HashAlgorithm::Fnv1a64.hash_hex("test");
assert_eq!(a, b);
assert_eq!(a.len(), 16);
}
#[test]
fn hash_algorithm_different_inputs_different_hashes() {
let a = HashAlgorithm::SipHash.hash_hex("alice");
let b = HashAlgorithm::SipHash.hash_hex("bob");
assert_ne!(a, b);
}
#[test]
fn hash_algorithm_empty_input() {
let h = HashAlgorithm::SipHash.hash_hex("");
assert!(!h.is_empty());
}
#[test]
fn fnv1a_32_known_values() {
assert_eq!(fnv1a_32(b""), 0x811c9dc5);
}
#[test]
fn fnv1a_64_known_values() {
assert_eq!(fnv1a_64(b""), 0xcbf29ce484222325);
}
#[test]
fn fnv1a_32_single_byte() {
let h = fnv1a_32(b"a");
let expected = (0x811c9dc5u32 ^ 97).wrapping_mul(0x01000193);
assert_eq!(h, expected);
}
#[test]
fn hash_masker_default() {
let m = HashMasker::new();
assert_eq!(m.algorithm(), HashAlgorithm::SipHash);
assert_eq!(m.keep_prefix(), 12);
assert_eq!(m.suffix(), "...");
}
#[test]
fn hash_masker_mask_basic() {
let m = HashMasker::new();
let result = m.mask("13812345678");
assert!(result.ends_with("..."));
assert!(result.len() > 3);
}
#[test]
fn hash_masker_mask_empty() {
let m = HashMasker::new();
assert_eq!(m.mask(""), "...");
}
#[test]
fn hash_masker_deterministic() {
let m = HashMasker::new();
let a = m.mask("same_value");
let b = m.mask("same_value");
assert_eq!(a, b);
}
#[test]
fn hash_masker_different_values_different_output() {
let m = HashMasker::new();
let a = m.mask("alice");
let b = m.mask("bob");
assert_ne!(a, b);
}
#[test]
fn hash_masker_with_algorithm_fnv1a32() {
let m = HashMasker::new().with_algorithm(HashAlgorithm::Fnv1a32);
let result = m.mask("test");
assert!(result.ends_with("..."));
assert_eq!(m.algorithm(), HashAlgorithm::Fnv1a32);
}
#[test]
fn hash_masker_with_keep_prefix() {
let m = HashMasker::new().with_keep_prefix(4);
let result = m.mask("hello");
assert_eq!(result.len(), 7);
}
#[test]
fn hash_masker_with_suffix() {
let m = HashMasker::new().with_suffix("[hashed]");
let result = m.mask("value");
assert!(result.ends_with("[hashed]"));
}
#[test]
fn hash_masker_keep_prefix_exceeds_hash_len() {
let m = HashMasker::new().with_keep_prefix(100);
let result = m.mask("test");
assert_eq!(result.len(), 19);
}
#[test]
fn hash_masker_mask_fields() {
let m = HashMasker::new();
let mut data = HashMap::new();
data.insert("phone".to_string(), "13812345678".to_string());
data.insert("name".to_string(), "Alice".to_string());
let fields = vec!["phone".to_string()];
let result = m.mask_fields(&fields, &data);
assert_ne!(result["phone"], "13812345678");
assert_eq!(result["name"], "Alice");
}
#[test]
fn hash_masker_mask_json() {
let m = HashMasker::new();
let json = r#"{"phone":"13812345678","name":"Alice"}"#;
let fields = vec!["phone".to_string()];
let result = m.mask_json(&fields, json);
assert!(result.contains("Alice"));
assert!(!result.contains("13812345678"));
}
#[test]
fn hash_masker_mask_json_invalid() {
let m = HashMasker::new();
let result = m.mask_json(&["phone".to_string()], "not json");
assert_eq!(result, "not json");
}
#[test]
fn hash_masker_mask_batch() {
let m = HashMasker::new();
let values = vec!["a".to_string(), "b".to_string(), "c".to_string()];
let result = m.mask_batch(&values);
assert_eq!(result.len(), 3);
assert_ne!(result[0], result[1]);
}
#[test]
fn partial_display_default() {
let m = PartialDisplayMasker::new();
assert_eq!(m.prefix_keep(), 3);
assert_eq!(m.suffix_keep(), 4);
assert_eq!(m.mask_char(), '*');
}
#[test]
fn partial_display_mask_basic() {
let m = PartialDisplayMasker::new();
assert_eq!(m.mask("13812345678"), "138****5678");
}
#[test]
fn partial_display_mask_too_short() {
let m = PartialDisplayMasker::new();
assert_eq!(m.mask("123"), "***");
}
#[test]
fn partial_display_mask_empty() {
let m = PartialDisplayMasker::new();
assert_eq!(m.mask(""), "***");
}
#[test]
fn partial_display_custom_mask_char() {
let m = PartialDisplayMasker::new().with_mask_char('#');
assert_eq!(m.mask("13812345678"), "138####5678");
}
#[test]
fn partial_display_custom_prefix_suffix() {
let m = PartialDisplayMasker::new()
.with_prefix(2)
.with_suffix_keep(2);
assert_eq!(m.mask("abcdefgh"), "ab****gh");
}
#[test]
fn partial_display_min_mask_length() {
let m = PartialDisplayMasker::new()
.with_prefix(3)
.with_suffix_keep(4)
.with_min_mask_length(6);
assert_eq!(m.mask("12345678"), "123******5678");
}
#[test]
fn partial_display_custom_fallback() {
let m = PartialDisplayMasker::new().with_fallback("[hidden]");
assert_eq!(m.mask(""), "[hidden]");
assert_eq!(m.mask("ab"), "[hidden]");
}
#[test]
fn partial_display_unicode() {
let m = PartialDisplayMasker::new()
.with_prefix(1)
.with_suffix_keep(1);
assert_eq!(m.mask("张三李四王"), "张***王");
}
#[test]
fn partial_display_mask_fields() {
let m = PartialDisplayMasker::new();
let mut data = HashMap::new();
data.insert("phone".to_string(), "13812345678".to_string());
data.insert("name".to_string(), "Alice".to_string());
let fields = vec!["phone".to_string()];
let result = m.mask_fields(&fields, &data);
assert_eq!(result["phone"], "138****5678");
assert_eq!(result["name"], "Alice");
}
#[test]
fn partial_display_exact_boundary() {
let m = PartialDisplayMasker::new();
assert_eq!(m.mask("1234567"), "***");
}
#[test]
fn partial_display_one_past_boundary() {
let m = PartialDisplayMasker::new();
assert_eq!(m.mask("12345678"), "123***5678");
}
#[test]
fn wildcard_exact_match() {
assert!(wildcard_match("phone", "phone"));
}
#[test]
fn wildcard_no_match() {
assert!(!wildcard_match("phone", "email"));
}
#[test]
fn wildcard_star_match_prefix() {
assert!(wildcard_match("user_*", "user_name"));
assert!(wildcard_match("user_*", "user_id"));
}
#[test]
fn wildcard_star_match_suffix() {
assert!(wildcard_match("*_id", "user_id"));
assert!(wildcard_match("*_id", "order_id"));
}
#[test]
fn wildcard_star_match_entire() {
assert!(wildcard_match("*", "anything"));
assert!(wildcard_match("*", ""));
}
#[test]
fn wildcard_question_match_single() {
assert!(wildcard_match("user_?", "user_1"));
assert!(!wildcard_match("user_?", "user_12"));
}
#[test]
fn wildcard_combined_star_question() {
assert!(wildcard_match("u*r?", "user_"));
assert!(wildcard_match("?*?", "abc"));
}
#[test]
fn wildcard_empty_pattern() {
assert!(wildcard_match("", ""));
assert!(!wildcard_match("", "a"));
}
#[test]
fn wildcard_star_only() {
assert!(wildcard_match("***", "test"));
assert!(wildcard_match("***", ""));
}
#[test]
fn pattern_masker_default_empty() {
let m = PatternMasker::new();
assert_eq!(m.pattern_count(), 0);
}
#[test]
fn pattern_masker_add_pattern() {
let m = PatternMasker::new().add_pattern("phone_*", crate::MaskingRule::Phone);
assert_eq!(m.pattern_count(), 1);
}
#[test]
fn pattern_masker_match_rule() {
let m = PatternMasker::new()
.add_pattern("phone_*", crate::MaskingRule::Phone)
.add_pattern("*_email", crate::MaskingRule::Email);
assert!(m.match_rule("phone_primary").is_some());
assert!(m.match_rule("user_email").is_some());
assert!(m.match_rule("address").is_none());
}
#[test]
fn pattern_masker_clear() {
let mut m = PatternMasker::new().add_pattern("*", crate::MaskingRule::Phone);
m.clear();
assert_eq!(m.pattern_count(), 0);
}
#[test]
fn pattern_masker_mask_map() {
let m = PatternMasker::new()
.add_pattern("phone_*", crate::MaskingRule::Phone)
.add_pattern("*_email", crate::MaskingRule::Email);
let mut data = HashMap::new();
data.insert("phone_primary".to_string(), "13812345678".to_string());
data.insert("user_email".to_string(), "test@example.com".to_string());
data.insert("name".to_string(), "Alice".to_string());
let result = m.mask_map(&data);
assert_eq!(result["phone_primary"], "138****5678");
assert_eq!(result["user_email"], "t***@example.com");
assert_eq!(result["name"], "Alice");
}
#[test]
fn pattern_masker_mask_json() {
let m = PatternMasker::new().add_pattern("phone", crate::MaskingRule::Phone);
let json = r#"{"phone":"13812345678","name":"Alice"}"#;
let result = m.mask_json(json);
assert!(result.contains("138****5678"));
assert!(result.contains("Alice"));
}
#[test]
fn pattern_masker_mask_json_invalid() {
let m = PatternMasker::new().add_pattern("*", crate::MaskingRule::Phone);
assert_eq!(m.mask_json("not json"), "not json");
}
#[test]
fn pattern_masker_no_match_passthrough() {
let m = PatternMasker::new().add_pattern("secret_*", crate::MaskingRule::Password);
let mut data = HashMap::new();
data.insert("public_field".to_string(), "visible".to_string());
let result = m.mask_map(&data);
assert_eq!(result["public_field"], "visible");
}
}