use thiserror::Error;
#[derive(Debug, Error)]
pub enum SensitiveError {
#[error("Masking failed: {0}")]
MaskingFailed(String),
#[error("Encryption failed: {0}")]
EncryptionFailed(String),
#[error("Decryption failed: {0}")]
DecryptionFailed(String),
#[error("Invalid key: {0}")]
InvalidKey(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
}
pub type SensitiveResult<T> = Result<T, SensitiveError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaskType {
Phone,
Email,
IdCard,
BankCard,
Name,
Address,
Custom {
keep_prefix: usize,
keep_suffix: usize,
},
}
pub struct SensitiveMasker;
impl SensitiveMasker {
pub fn new() -> Self {
Self
}
pub fn mask(data: &str, mask_type: MaskType) -> SensitiveResult<String> {
let data = data.trim();
match mask_type {
MaskType::Phone => Self::mask_phone(data),
MaskType::Email => Self::mask_email(data),
MaskType::IdCard => Self::mask_id_card(data),
MaskType::BankCard => Self::mask_bank_card(data),
MaskType::Name => Self::mask_name(data),
MaskType::Address => Self::mask_address(data),
MaskType::Custom {
keep_prefix,
keep_suffix,
} => Self::mask_custom(data, keep_prefix, keep_suffix),
}
}
fn mask_phone(phone: &str) -> SensitiveResult<String> {
let cleaned: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
if cleaned.len() < 7 {
return Err(SensitiveError::InvalidInput("Phone number too short".to_string()));
}
let chars: Vec<char> = cleaned.chars().collect();
let len = chars.len();
let prefix: String = chars[..3].iter().collect();
let suffix: String = chars[len - 4..].iter().collect();
let mask_count = len - 7;
Ok(format!("{}{}{}", prefix, "*".repeat(mask_count), suffix))
}
fn mask_email(email: &str) -> SensitiveResult<String> {
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return Err(SensitiveError::InvalidInput("Invalid email format".to_string()));
}
let local = parts[0];
let domain = parts[1];
if local.is_empty() {
return Err(SensitiveError::InvalidInput("Email local part is empty".to_string()));
}
let prefix_len = local.len().min(2);
let prefix = &local[..prefix_len];
let mask_count = local.len() - prefix_len;
Ok(format!("{}{}@{}", prefix, "*".repeat(mask_count.max(3)), domain))
}
fn mask_id_card(id: &str) -> SensitiveResult<String> {
let cleaned: String = id
.chars()
.filter(|c| c.is_ascii_digit() || *c == 'X' || *c == 'x')
.collect();
if cleaned.len() != 15 && cleaned.len() != 18 {
return Err(SensitiveError::InvalidInput("Invalid ID card length".to_string()));
}
let chars: Vec<char> = cleaned.chars().collect();
let len = chars.len();
let prefix: String = chars[..4].iter().collect();
let suffix: String = chars[len - 4..].iter().collect();
let mask_count = len - 8;
Ok(format!("{}{}{}", prefix, "*".repeat(mask_count), suffix))
}
fn mask_bank_card(card: &str) -> SensitiveResult<String> {
let cleaned: String = card.chars().filter(|c| c.is_ascii_digit()).collect();
if cleaned.len() < 8 {
return Err(SensitiveError::InvalidInput("Bank card number too short".to_string()));
}
let chars: Vec<char> = cleaned.chars().collect();
let len = chars.len();
let prefix: String = chars[..4].iter().collect();
let suffix: String = chars[len - 4..].iter().collect();
let mask_count = len - 8;
Ok(format!("{}{}{}", prefix, "*".repeat(mask_count), suffix))
}
fn mask_name(name: &str) -> SensitiveResult<String> {
let trimmed = name.trim();
if trimmed.is_empty() {
return Err(SensitiveError::InvalidInput("Name is empty".to_string()));
}
let chars: Vec<char> = trimmed.chars().collect();
let len = chars.len();
let surname: String = chars[..1].iter().collect();
let mask_count = len - 1;
Ok(format!("{}{}", surname, "*".repeat(mask_count)))
}
fn mask_address(address: &str) -> SensitiveResult<String> {
let trimmed = address.trim();
if trimmed.is_empty() {
return Err(SensitiveError::InvalidInput("Address is empty".to_string()));
}
let keywords = ["省", "市", "区", "县", "镇"];
let mut last_keyword_pos = 0;
for keyword in &keywords {
if let Some(pos) = trimmed.find(keyword) {
last_keyword_pos = last_keyword_pos.max(pos + keyword.len());
}
}
if last_keyword_pos > 0 && last_keyword_pos < trimmed.len() {
let prefix = &trimmed[..last_keyword_pos];
let mask_len = trimmed.len() - last_keyword_pos;
return Ok(format!("{}{}", prefix, "*".repeat(mask_len)));
}
let chars: Vec<char> = trimmed.chars().collect();
let prefix_len = chars.len().min(6);
let prefix: String = chars[..prefix_len].iter().collect();
let mask_len = chars.len() - prefix_len;
Ok(format!("{}{}", prefix, "*".repeat(mask_len)))
}
fn mask_custom(data: &str, keep_prefix: usize, keep_suffix: usize) -> SensitiveResult<String> {
if data.is_empty() {
return Err(SensitiveError::InvalidInput("Data is empty".to_string()));
}
let chars: Vec<char> = data.chars().collect();
let len = chars.len();
if keep_prefix + keep_suffix >= len {
return Ok(data.to_string());
}
let prefix: String = chars[..keep_prefix].iter().collect();
let suffix: String = chars[len - keep_suffix..].iter().collect();
let mask_count = len - keep_prefix - keep_suffix;
Ok(format!("{}{}{}", prefix, "*".repeat(mask_count), suffix))
}
}
impl Default for SensitiveMasker {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mask_phone() {
assert_eq!(
SensitiveMasker::mask("13812345678", MaskType::Phone).unwrap(),
"138****5678"
);
assert_eq!(
SensitiveMasker::mask("138 1234 5678", MaskType::Phone).unwrap(),
"138****5678"
);
assert_eq!(
SensitiveMasker::mask("138-1234-5678", MaskType::Phone).unwrap(),
"138****5678"
);
}
#[test]
fn test_mask_email() {
let result = SensitiveMasker::mask("test@example.com", MaskType::Email).unwrap();
assert!(result.starts_with("te"));
assert!(result.ends_with("@example.com"));
assert!(result.contains('*'));
let result = SensitiveMasker::mask("a@b.com", MaskType::Email).unwrap();
assert!(result.contains('*'));
}
#[test]
fn test_mask_id_card() {
let result = SensitiveMasker::mask("110101199001011234", MaskType::IdCard).unwrap();
assert_eq!(&result[..4], "1101");
assert_eq!(&result[result.len() - 4..], "1234");
assert!(result.contains('*'));
let result = SensitiveMasker::mask("110101900101123", MaskType::IdCard).unwrap();
assert_eq!(&result[..4], "1101");
assert_eq!(&result[result.len() - 4..], "1123");
}
#[test]
fn test_mask_bank_card() {
let result = SensitiveMasker::mask("6222021234567890", MaskType::BankCard).unwrap();
assert_eq!(&result[..4], "6222");
assert_eq!(&result[result.len() - 4..], "7890");
assert!(result.contains('*'));
let result = SensitiveMasker::mask("6222021234567890123", MaskType::BankCard).unwrap();
assert_eq!(&result[..4], "6222");
assert_eq!(&result[result.len() - 4..], "0123");
}
#[test]
fn test_mask_name() {
assert_eq!(SensitiveMasker::mask("张三", MaskType::Name).unwrap(), "张*");
assert_eq!(SensitiveMasker::mask("李某某", MaskType::Name).unwrap(), "李**");
assert_eq!(SensitiveMasker::mask("欧阳明月", MaskType::Name).unwrap(), "欧***");
}
#[test]
fn test_mask_address() {
let result = SensitiveMasker::mask("北京市朝阳区某某街道123号", MaskType::Address).unwrap();
assert!(result.starts_with("北京市朝阳区"));
assert!(result.contains('*'));
let result = SensitiveMasker::mask("某某街道123号", MaskType::Address).unwrap();
assert!(!result.is_empty());
}
#[test]
fn test_mask_custom() {
assert_eq!(
SensitiveMasker::mask(
"1234567890",
MaskType::Custom {
keep_prefix: 2,
keep_suffix: 2
}
)
.unwrap(),
"12******90"
);
assert_eq!(
SensitiveMasker::mask(
"123",
MaskType::Custom {
keep_prefix: 2,
keep_suffix: 2
}
)
.unwrap(),
"123"
);
}
#[test]
fn test_invalid_inputs() {
assert!(SensitiveMasker::mask("123", MaskType::Phone).is_err());
assert!(SensitiveMasker::mask("invalid", MaskType::Email).is_err());
assert!(SensitiveMasker::mask("12345", MaskType::IdCard).is_err());
assert!(SensitiveMasker::mask("1234567", MaskType::BankCard).is_err());
}
}