const MASK_CHAR: char = '*';
fn star_string(n: usize) -> String {
MASK_CHAR.to_string().repeat(n)
}
pub fn mask_phone(phone: &str) -> String {
if phone.len() == 11 && phone.chars().all(|c| c.is_ascii_digit()) {
format!("{}****{}", &phone[..3], &phone[7..])
} else {
mask_middle(phone, 1, 1)
}
}
pub fn mask_email(email: &str) -> String {
let Some(at) = email.find('@') else {
return mask_middle(email, 1, 0);
};
let (local, domain) = email.split_at(at);
if local.is_empty() {
return email.to_string();
}
let first = local.chars().next().unwrap();
format!("{first}***{domain}")
}
pub fn mask_id_card(id: &str) -> String {
let len = id.chars().count();
if !(len == 15 || len == 18) {
return mask_middle(id, 1, 1);
}
mask_middle(id, 6, 4)
}
pub fn mask_bank_card(card: &str) -> String {
if card.chars().count() < 9 {
return mask_middle(card, 1, 1);
}
mask_middle(card, 4, 4)
}
pub fn mask_token(token: &str) -> String {
if token.chars().count() < 9 {
return star_string(token.chars().count());
}
mask_middle(token, 4, 4)
}
pub fn mask_name(name: &str) -> String {
let chars: Vec<char> = name.chars().collect();
match chars.len() {
0 | 1 => name.to_string(),
2 => format!("{}{MASK_CHAR}", chars[0]),
n => {
let middle = star_string(n - 2);
format!("{}{middle}{}", chars[0], chars[n - 1])
}
}
}
pub fn mask_secret(value: Option<&str>, keep_chars: usize) -> String {
let s = match value {
None => return "<none>".to_string(),
Some("") => return "<empty>".to_string(),
Some(v) => v,
};
let chars: Vec<char> = s.chars().collect();
let len = chars.len();
if len <= keep_chars * 2 {
return s.to_string();
}
let head: String = chars[..keep_chars].iter().collect();
let tail: String = chars[len - keep_chars..].iter().collect();
format!("{head}***{tail}")
}
pub fn mask_middle(s: &str, prefix: usize, suffix: usize) -> String {
let chars: Vec<char> = s.chars().collect();
let total = chars.len();
if total <= prefix + suffix {
return star_string(total);
}
let head: String = chars[..prefix].iter().collect();
let tail: String = chars[total - suffix..].iter().collect();
let middle = star_string(total - prefix - suffix);
format!("{head}{middle}{tail}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn phone_well_formed() {
assert_eq!(mask_phone("13812345678"), "138****5678");
}
#[test]
fn phone_malformed_falls_back_to_generic() {
assert_eq!(mask_phone("12345"), "1***5");
assert_eq!(mask_phone("138-1234-5678"), "1***********8");
}
#[test]
fn phone_empty_returns_empty() {
assert_eq!(mask_phone(""), "");
}
#[test]
fn email_typical() {
assert_eq!(mask_email("alice@example.com"), "a***@example.com");
}
#[test]
fn email_single_char_local() {
assert_eq!(mask_email("a@b.com"), "a***@b.com");
}
#[test]
fn email_no_at_sign_falls_back() {
assert_eq!(mask_email("not-an-email"), "n***********");
}
#[test]
fn email_starts_with_at_returned_as_is() {
assert_eq!(mask_email("@example.com"), "@example.com");
}
#[test]
fn id_card_18_digits() {
assert_eq!(mask_id_card("110101199001011234"), "110101********1234");
}
#[test]
fn id_card_15_digits() {
assert_eq!(mask_id_card("123456789012345"), "123456*****2345");
}
#[test]
fn id_card_unexpected_length_falls_back() {
assert_eq!(mask_id_card("123"), "1*3");
assert_eq!(mask_id_card("12345"), "1***5");
}
#[test]
fn bank_card_typical() {
assert_eq!(mask_bank_card("6225881234567890"), "6225********7890");
}
#[test]
fn bank_card_short() {
assert_eq!(mask_bank_card("12345678"), "1******8");
}
#[test]
fn token_typical() {
assert_eq!(mask_token("sk-1234567890abcdef"), "sk-1***********cdef");
}
#[test]
fn token_short_is_fully_starred() {
assert_eq!(mask_token("abc12345"), "********");
assert_eq!(mask_token(""), "");
}
#[test]
fn name_one_char_unchanged() {
assert_eq!(mask_name("张"), "张");
}
#[test]
fn name_two_chars() {
assert_eq!(mask_name("张三"), "张*");
}
#[test]
fn name_three_chars() {
assert_eq!(mask_name("张三丰"), "张*丰");
}
#[test]
fn name_four_chars() {
assert_eq!(mask_name("欧阳修远"), "欧**远");
}
#[test]
fn mask_middle_too_short_stars_everything() {
assert_eq!(mask_middle("abc", 2, 2), "***");
assert_eq!(mask_middle("ab", 1, 1), "**");
}
#[test]
fn mask_middle_unicode_is_char_based_not_byte_based() {
assert_eq!(mask_middle("一二三四五", 1, 1), "一***五");
}
#[test]
fn mask_secret_typical() {
assert_eq!(mask_secret(Some("vault:AES256:abcXYZ"), 3), "vau***XYZ");
}
#[test]
fn mask_secret_uses_three_stars_regardless_of_length() {
let long_secret = "a".repeat(50) + "END12345";
let masked = mask_secret(Some(&long_secret), 4);
assert_eq!(masked, "aaaa***2345");
}
#[test]
fn mask_secret_short_returned_as_is() {
assert_eq!(mask_secret(Some("abcdef"), 3), "abcdef");
assert_eq!(mask_secret(Some("ab"), 3), "ab");
}
#[test]
fn mask_secret_sentinels_for_none_and_empty() {
assert_eq!(mask_secret(None, 3), "<none>");
assert_eq!(mask_secret(Some(""), 3), "<empty>");
}
#[test]
fn mask_secret_keep_zero_stars_everything() {
assert_eq!(mask_secret(Some("hello"), 0), "***");
}
#[test]
fn mask_secret_unicode_safe() {
assert_eq!(mask_secret(Some("一二三四五六七八"), 2), "一二***七八");
}
}