use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MaskingRule {
Phone,
Email,
IdCard,
BankCard,
Name,
Address,
Ip,
Imei,
Plate,
Custom(String),
}
pub struct DataMasker;
impl DataMasker {
pub fn apply(rule: &MaskingRule, value: &str) -> String {
match rule {
MaskingRule::Phone => mask_prefix_suffix(value, 3, 4),
MaskingRule::Email => mask_email(value),
MaskingRule::IdCard => mask_prefix_suffix(value, 4, 4),
MaskingRule::BankCard => mask_prefix_suffix(value, 4, 4),
MaskingRule::Name => mask_name(value),
MaskingRule::Address => mask_address(value, 6),
MaskingRule::Ip => mask_ip(value),
MaskingRule::Imei => mask_imei(value),
MaskingRule::Plate => mask_plate(value),
MaskingRule::Custom(spec) => mask_custom(value, spec),
}
}
}
fn mask_prefix_suffix(value: &str, prefix: usize, suffix: usize) -> String {
let chars: Vec<char> = value.chars().collect();
let len = chars.len();
if len == 0 {
return "***".to_string();
}
if len <= prefix + suffix {
return "***".to_string();
}
let hidden = len - prefix - suffix;
let mut out = String::with_capacity(len);
for &c in &chars[..prefix] {
out.push(c);
}
for _ in 0..hidden {
out.push('*');
}
for &c in &chars[len - suffix..] {
out.push(c);
}
out
}
fn mask_email(value: &str) -> String {
let parts: Vec<&str> = value.splitn(2, '@').collect();
if parts.len() != 2 {
return "***".to_string();
}
let local = parts[0];
let domain = parts[1];
let local_chars: Vec<char> = local.chars().collect();
if local_chars.is_empty() {
return "***".to_string();
}
let mut out = String::with_capacity(value.len());
out.push(local_chars[0]);
for _ in 1..local_chars.len() {
out.push('*');
}
out.push('@');
out.push_str(domain);
out
}
fn mask_name(value: &str) -> String {
let chars: Vec<char> = value.chars().collect();
if chars.is_empty() {
return String::new();
}
let mut out = String::with_capacity(chars.len());
out.push(chars[0]);
for _ in 1..chars.len() {
out.push('*');
}
out
}
fn mask_address(value: &str, keep: usize) -> String {
let chars: Vec<char> = value.chars().collect();
if chars.is_empty() {
return String::new();
}
if chars.len() <= keep {
return "*".repeat(chars.len());
}
let hidden = chars.len() - keep;
let mut out = String::with_capacity(chars.len());
for &c in &chars[..keep] {
out.push(c);
}
for _ in 0..hidden {
out.push('*');
}
out
}
fn mask_ip(ip: &str) -> String {
if let Some(last_dot) = ip.rfind('.') {
format!("{}.*", &ip[..last_dot])
} else if let Some(last_colon) = ip.rfind(':') {
format!("{}:*", &ip[..last_colon])
} else {
ip.to_string()
}
}
fn mask_imei(imei: &str) -> String {
let chars: Vec<char> = imei.chars().collect();
if chars.len() < 7 {
return "*".repeat(chars.len());
}
let mut out = String::with_capacity(chars.len() + 4);
for &c in &chars[..6] {
out.push(c);
}
out.push_str("****");
out.push(chars[chars.len() - 1]);
out
}
fn mask_plate(plate: &str) -> String {
let chars: Vec<char> = plate.chars().collect();
let len = chars.len();
if len < 4 {
return "*".repeat(len);
}
let mut out = String::with_capacity(len + 2);
for &c in &chars[..len - 2] {
out.push(c);
}
out.push_str("**");
for &c in &chars[len - 2..] {
out.push(c);
}
out
}
fn mask_custom(value: &str, spec: &str) -> String {
let (prefix, suffix) = match parse_custom_spec(spec) {
Some(parsed) => parsed,
None => return "***".to_string(),
};
mask_prefix_suffix(value, prefix, suffix)
}
fn parse_custom_spec(spec: &str) -> Option<(usize, usize)> {
let parts: Vec<&str> = spec.split(',').collect();
if parts.len() != 2 {
return None;
}
let prefix: usize = parts[0].trim().parse().ok()?;
let suffix: usize = parts[1].trim().parse().ok()?;
Some((prefix, suffix))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phone_standard() {
let result = DataMasker::apply(&MaskingRule::Phone, "13812345678");
assert_eq!(result, "138****5678");
}
#[test]
fn test_phone_too_short() {
assert_eq!(DataMasker::apply(&MaskingRule::Phone, "12345"), "***");
assert_eq!(DataMasker::apply(&MaskingRule::Phone, "1234567"), "***");
}
#[test]
fn test_phone_boundary_seven_plus_one() {
assert_eq!(
DataMasker::apply(&MaskingRule::Phone, "12345678"),
"123*5678"
);
}
#[test]
fn test_phone_empty() {
assert_eq!(DataMasker::apply(&MaskingRule::Phone, ""), "***");
}
#[test]
fn test_email_standard() {
assert_eq!(
DataMasker::apply(&MaskingRule::Email, "test@example.com"),
"t***@example.com"
);
}
#[test]
fn test_email_single_char_local() {
assert_eq!(
DataMasker::apply(&MaskingRule::Email, "a@example.com"),
"a@example.com"
);
}
#[test]
fn test_email_no_at() {
assert_eq!(DataMasker::apply(&MaskingRule::Email, "notanemail"), "***");
}
#[test]
fn test_email_empty_local() {
assert_eq!(
DataMasker::apply(&MaskingRule::Email, "@example.com"),
"***"
);
}
#[test]
fn test_idcard_standard_18() {
let id = "110101199001012345";
let masked = DataMasker::apply(&MaskingRule::IdCard, id);
assert_eq!(masked, "1101**********2345");
assert_eq!(masked.len(), id.len());
}
#[test]
fn test_idcard_too_short() {
assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "1234567"), "***");
assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "12345678"), "***");
}
#[test]
fn test_idcard_empty() {
assert_eq!(DataMasker::apply(&MaskingRule::IdCard, ""), "***");
}
#[test]
fn test_bankcard_standard_16() {
let card = "6222020200112345";
let masked = DataMasker::apply(&MaskingRule::BankCard, card);
assert_eq!(masked, "6222********2345");
}
#[test]
fn test_bankcard_too_short() {
assert_eq!(DataMasker::apply(&MaskingRule::BankCard, "1234567"), "***");
}
#[test]
fn test_bankcard_empty() {
assert_eq!(DataMasker::apply(&MaskingRule::BankCard, ""), "***");
}
#[test]
fn test_name_chinese_two_chars() {
assert_eq!(DataMasker::apply(&MaskingRule::Name, "张三"), "张*");
}
#[test]
fn test_name_chinese_three_chars() {
assert_eq!(DataMasker::apply(&MaskingRule::Name, "诸葛亮"), "诸**");
}
#[test]
fn test_name_single_char() {
assert_eq!(DataMasker::apply(&MaskingRule::Name, "李"), "李");
}
#[test]
fn test_name_empty() {
assert_eq!(DataMasker::apply(&MaskingRule::Name, ""), "");
}
#[test]
fn test_name_english() {
assert_eq!(DataMasker::apply(&MaskingRule::Name, "Alice"), "A****");
}
#[test]
fn test_address_standard() {
let addr = "北京市海淀区中关村大街1号";
let masked = DataMasker::apply(&MaskingRule::Address, addr);
let expected = "北京市海淀区*******";
assert_eq!(masked, expected);
assert_eq!(masked.chars().count(), addr.chars().count());
}
#[test]
fn test_address_exactly_six_chars() {
let addr = "北京市海淀区";
assert_eq!(DataMasker::apply(&MaskingRule::Address, addr), "******");
}
#[test]
fn test_address_short() {
assert_eq!(DataMasker::apply(&MaskingRule::Address, "北京"), "**");
}
#[test]
fn test_address_empty() {
assert_eq!(DataMasker::apply(&MaskingRule::Address, ""), "");
}
#[test]
fn test_custom_prefix_suffix() {
let rule = MaskingRule::Custom("3,2".to_string());
assert_eq!(DataMasker::apply(&rule, "ABCDEFGHIJ"), "ABC*****IJ");
}
#[test]
fn test_custom_too_short() {
let rule = MaskingRule::Custom("4,4".to_string());
assert_eq!(DataMasker::apply(&rule, "ABC"), "***");
}
#[test]
fn test_custom_invalid_spec() {
let rule = MaskingRule::Custom("not_a_number".to_string());
assert_eq!(DataMasker::apply(&rule, "ABCDEF"), "***");
}
#[test]
fn test_custom_invalid_spec_two_parts() {
let rule = MaskingRule::Custom("1,2,3".to_string());
assert_eq!(DataMasker::apply(&rule, "ABCDEF"), "***");
}
#[test]
fn test_custom_empty_value() {
let rule = MaskingRule::Custom("2,2".to_string());
assert_eq!(DataMasker::apply(&rule, ""), "***");
}
#[test]
fn test_unicode_no_panic() {
let value = "你好🌍世界AB";
let masked = DataMasker::apply(&MaskingRule::Address, value);
assert!(masked.contains('*'));
}
#[test]
fn test_long_string() {
let value = "1".repeat(10000);
let masked = DataMasker::apply(&MaskingRule::Phone, &value);
assert!(masked.starts_with("111"));
assert!(masked.ends_with("1111"));
assert_eq!(masked.matches('*').count(), 10000 - 7);
}
#[test]
fn test_single_char_inputs() {
assert_eq!(DataMasker::apply(&MaskingRule::Phone, "1"), "***");
assert_eq!(DataMasker::apply(&MaskingRule::IdCard, "1"), "***");
assert_eq!(DataMasker::apply(&MaskingRule::BankCard, "1"), "***");
assert_eq!(DataMasker::apply(&MaskingRule::Name, "张"), "张");
assert_eq!(DataMasker::apply(&MaskingRule::Address, "张"), "*");
}
#[test]
fn test_ip_v4_standard() {
assert_eq!(
DataMasker::apply(&MaskingRule::Ip, "192.168.1.100"),
"192.168.1.*"
);
}
#[test]
fn test_ip_v4_loopback() {
assert_eq!(
DataMasker::apply(&MaskingRule::Ip, "127.0.0.1"),
"127.0.0.*"
);
}
#[test]
fn test_ip_v6_standard() {
assert_eq!(
DataMasker::apply(&MaskingRule::Ip, "2001:db8::1"),
"2001:db8::*"
);
}
#[test]
fn test_ip_no_separator() {
assert_eq!(
DataMasker::apply(&MaskingRule::Ip, "localhost"),
"localhost"
);
}
#[test]
fn test_ip_empty() {
assert_eq!(DataMasker::apply(&MaskingRule::Ip, ""), "");
}
#[test]
fn test_imei_standard_15() {
assert_eq!(
DataMasker::apply(&MaskingRule::Imei, "123456789012345"),
"123456****5"
);
}
#[test]
fn test_imei_too_short() {
assert_eq!(DataMasker::apply(&MaskingRule::Imei, "123456"), "******");
assert_eq!(DataMasker::apply(&MaskingRule::Imei, "123"), "***");
}
#[test]
fn test_imei_empty() {
assert_eq!(DataMasker::apply(&MaskingRule::Imei, ""), "");
}
#[test]
fn test_plate_chinese_standard() {
assert_eq!(
DataMasker::apply(&MaskingRule::Plate, "京A12345"),
"京A123**45"
);
}
#[test]
fn test_plate_with_separator() {
assert_eq!(
DataMasker::apply(&MaskingRule::Plate, "京A·12345"),
"京A·123**45"
);
}
#[test]
fn test_plate_too_short() {
assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京A"), "**");
assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京"), "*");
assert_eq!(DataMasker::apply(&MaskingRule::Plate, "京A1"), "***");
}
#[test]
fn test_plate_empty() {
assert_eq!(DataMasker::apply(&MaskingRule::Plate, ""), "");
}
#[test]
fn test_plate_boundary_four_chars() {
assert_eq!(DataMasker::apply(&MaskingRule::Plate, "ABCD"), "AB**CD");
}
}