maskify 0.1.0

Zero-dependency, blazing-fast Rust crate for masking sensitive data — emails, phones, cards, Aadhaar, PAN, IPs & more. Comes with trait extensions and a builder pattern.
Documentation
/// Masks an email address.
///
/// Keeps the first character of the local part and masks the rest.
/// Domain remains untouched.
///
/// # Examples
/// ```
/// use maskify::mask_email;
/// assert_eq!(mask_email("user@example.com"), "u***@example.com");
/// assert_eq!(mask_email("ab@test.com"), "a*@test.com");
/// assert_eq!(mask_email("a@test.com"), "*@test.com");
/// assert_eq!(mask_email(""), "");
/// ```
pub fn mask_email(input: &str) -> String {
    if input.is_empty() {
        return String::new();
    }

    match input.split_once('@') {
        Some((local, domain)) => {
            let chars: Vec<char> = local.chars().collect();
            let masked_local = match chars.len() {
                0 | 1 => String::from("*"),
                n @ 2..=4 => {
                    format!("{}{}", chars[0], "*".repeat(n - 1))
                }
                _ => {
                    format!("{}****", chars[0])
                }
            };
            format!("{}@{}", masked_local, domain)
        }
        None => mask_all(input),
    }
}

/// Masks a phone number.
///
/// Strips all non-digit characters first. Shows first 2 and last 2 digits.
///
/// # Examples
/// ```
/// use maskify::mask_phone;
/// assert_eq!(mask_phone("9876543210"), "98******10");
/// assert_eq!(mask_phone("+91-9876543210"), "91********10");
/// assert_eq!(mask_phone("123"), "***");
/// assert_eq!(mask_phone(""), "");
/// ```
pub fn mask_phone(input: &str) -> String {
    let digits: Vec<char> = input.chars().filter(|c| c.is_ascii_digit()).collect();
    let len = digits.len();

    if len == 0 {
        return String::new();
    }

    if len <= 4 {
        return "*".repeat(len);
    }

    let start: String = digits[..2].iter().collect();
    let end: String = digits[len - 2..].iter().collect();
    let middle = "*".repeat(len - 4);

    format!("{}{}{}", start, middle, end)
}

/// Masks a credit/debit card number.
///
/// Strips all non-digit characters first. Shows first 4 and last 4 digits.
///
/// # Examples
/// ```
/// use maskify::mask_card;
/// assert_eq!(mask_card("4111111111111111"), "4111********1111");
/// assert_eq!(mask_card("4111-1111-1111-1111"), "4111********1111");
/// assert_eq!(mask_card("1234"), "****");
/// assert_eq!(mask_card(""), "");
/// ```
pub fn mask_card(input: &str) -> String {
    let digits: Vec<char> = input.chars().filter(|c| c.is_ascii_digit()).collect();
    let len = digits.len();

    if len == 0 {
        return String::new();
    }

    if len <= 8 {
        return "*".repeat(len);
    }

    let start: String = digits[..4].iter().collect();
    let end: String = digits[len - 4..].iter().collect();
    let middle = "*".repeat(len - 8);

    format!("{}{}{}", start, middle, end)
}

/// Masks an entire string with asterisks.
///
/// # Examples
/// ```
/// use maskify::mask_all;
/// assert_eq!(mask_all("secret"), "******");
/// assert_eq!(mask_all(""), "");
/// ```
pub fn mask_all(input: &str) -> String {
    "*".repeat(input.chars().count())
}

/// Masks a name — keeps first letter of each word.
///
/// # Examples
/// ```
/// use maskify::mask_name;
/// assert_eq!(mask_name("John Doe"), "J*** D**");
/// assert_eq!(mask_name("Alice Bob Charlie"), "A**** B** C******");
/// assert_eq!(mask_name("A"), "A");
/// assert_eq!(mask_name(""), "");
/// ```
pub fn mask_name(input: &str) -> String {
    if input.is_empty() {
        return String::new();
    }

    input
        .split_whitespace()
        .map(|word| {
            let chars: Vec<char> = word.chars().collect();
            match chars.len() {
                0 => String::new(),
                1 => chars[0].to_string(),
                n => format!("{}{}", chars[0], "*".repeat(n - 1)),
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}

/// Masks an IP address — shows only the first octet (IPv4) or first group (IPv6).
///
/// # Examples
/// ```
/// use maskify::mask_ip;
/// assert_eq!(mask_ip("192.168.1.100"), "192.*.*.*");
/// assert_eq!(mask_ip("10.0.0.1"), "10.*.*.*");
/// assert_eq!(mask_ip(""), "");
/// ```
pub fn mask_ip(input: &str) -> String {
    if input.is_empty() {
        return String::new();
    }

    let parts: Vec<&str> = input.split('.').collect();
    if parts.len() == 4 {
        return format!("{}.*.*.*", parts[0]);
    }

    if input.contains(':') {
        let parts: Vec<&str> = input.split(':').collect();
        if parts.len() >= 2 {
            let masked_groups: Vec<&str> = std::iter::once(parts[0])
                .chain(std::iter::repeat("****").take(parts.len() - 1))
                .collect();
            return masked_groups.join(":");
        }
    }

    mask_custom(input, 3, 0, '*')
}

/// Masks an Aadhaar number — shows only last 4 digits in formatted output.
///
/// Input can contain spaces or dashes — only digits are considered.
/// If not exactly 12 digits, the entire input is masked.
///
/// # Examples
/// ```
/// use maskify::mask_aadhaar;
/// assert_eq!(mask_aadhaar("123456789012"), "XXXX-XXXX-9012");
/// assert_eq!(mask_aadhaar("1234 5678 9012"), "XXXX-XXXX-9012");
/// assert_eq!(mask_aadhaar("12345"), "*****");
/// ```
pub fn mask_aadhaar(input: &str) -> String {
    let digits: Vec<char> = input.chars().filter(|c| c.is_ascii_digit()).collect();

    if digits.len() != 12 {
        return mask_all(input);
    }

    let last4: String = digits[8..].iter().collect();
    format!("XXXX-XXXX-{}", last4)
}

/// Masks a PAN card number — keeps first and last character.
///
/// If not exactly 10 characters, the entire input is masked.
///
/// # Examples
/// ```
/// use maskify::mask_pan;
/// assert_eq!(mask_pan("ABCDE1234F"), "A********F");
/// assert_eq!(mask_pan("SHORT"), "*****");
/// ```
pub fn mask_pan(input: &str) -> String {
    let chars: Vec<char> = input.chars().collect();

    if chars.len() != 10 {
        return mask_all(input);
    }

    format!("{}{}{}", chars[0], "*".repeat(8), chars[9])
}

/// Custom masking — you decide how many characters to keep at the start and end.
///
/// # Arguments
/// * `input` — the string to mask
/// * `keep_start` — number of characters to keep at the start
/// * `keep_end` — number of characters to keep at the end
/// * `mask_char` — the character to use for masking
///
/// # Examples
/// ```
/// use maskify::mask_custom;
/// assert_eq!(mask_custom("HelloWorld", 2, 2, '#'), "He######ld");
/// assert_eq!(mask_custom("Short", 3, 3, '*'), "Short");
/// assert_eq!(mask_custom("ABCDEF", 1, 1, 'X'), "AXXXXF");
/// ```
pub fn mask_custom(input: &str, keep_start: usize, keep_end: usize, mask_char: char) -> String {
    let chars: Vec<char> = input.chars().collect();
    let len = chars.len();

    if len == 0 {
        return String::new();
    }

    if keep_start + keep_end >= len {
        return input.to_string();
    }

    let start: String = chars[..keep_start].iter().collect();
    let end: String = if keep_end > 0 {
        chars[len - keep_end..].iter().collect()
    } else {
        String::new()
    };
    let middle_len = len - keep_start - keep_end;
    let middle: String = std::iter::repeat(mask_char).take(middle_len).collect();

    format!("{}{}{}", start, middle, end)
}