Skip to main content

cleansh_core/
validators.rs

1// File: cleansh-core/src/validators.rs
2//! Programmatic validation functions for specific sensitive data types.
3//!
4//! This module provides additional validation logic beyond regular expression matching
5//! for sensitive information such as SSN and UK NINO. These functions help reduce
6//! false positives by applying structural and known invalid pattern checks.
7//!
8//! License: MIT OR APACHE 2.0
9
10use std::borrow::Cow;
11use std::collections::HashSet;
12use once_cell::sync::Lazy;
13
14/// Helper function to validate SSN based on US Social Security Administration rules.
15///
16/// This implementation aims for a robust programmatic check without external data.
17/// It validates the structural components against known invalid patterns.
18///
19/// # Arguments
20///
21/// * `ssn` - The SSN string slice to validate. Expected format "XXX-XX-XXXX".
22///
23/// # Returns
24///
25/// `true` if the SSN passes basic structural and invalid pattern checks, `false` otherwise.
26pub fn is_valid_ssn_programmatically(ssn: &str) -> bool {
27    let mut parts = ssn.split('-');
28
29    // Use a single pattern match to validate the structure and extract parts.
30    // This is more concise and less error-prone than a series of `if let` checks.
31    let (Some(area), Some(group), Some(serial), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
32        return false;
33    };
34
35    if area.len() != 3 || group.len() != 2 || serial.len() != 4 {
36        return false;
37    }
38
39    let Some(area_num) = area.parse::<u16>().ok() else { return false; };
40    let Some(group_num) = group.parse::<u8>().ok() else { return false; };
41    let Some(serial_num) = serial.parse::<u16>().ok() else { return false; };
42
43    // Check for invalid SSN patterns based on historical and current rules.
44    let invalid_area = (area_num == 0) || (area_num == 666) || (area_num >= 800);
45    let invalid_group = group_num == 0;
46    let invalid_serial = serial_num == 0;
47    
48    !(invalid_area || invalid_group || invalid_serial)
49}
50
51// Use a `once_cell` to create a static HashSet for efficient lookups.
52static INVALID_NINO_PREFIXES: Lazy<HashSet<&'static str>> = Lazy::new(|| {
53    let mut set = HashSet::new();
54    set.extend(["BF", "BG", "EH", "GB", "JE", "NK", "KN", "LI", "NT", "TN", "ZZ"]);
55    set
56});
57
58static INVALID_NINO_PREFIX_CHARS: Lazy<HashSet<char>> = Lazy::new(|| {
59    let mut set = HashSet::new();
60    set.extend(['D', 'F', 'I', 'Q', 'U', 'V', 'O']);
61    set
62});
63
64static VALID_NINO_SUFFIX_CHARS: Lazy<HashSet<char>> = Lazy::new(|| {
65    let mut set = HashSet::new();
66    set.extend(['A', 'B', 'C', 'D']);
67    set
68});
69
70/// Helper function to validate UK National Insurance Number (NINO) based on HMRC rules.
71///
72/// This implementation aims for a robust programmatic check without external data.
73/// It validates the structural components against known invalid patterns and characters.
74///
75/// # Arguments
76///
77/// * `nino` - The NINO string slice to validate. Expected format "AA######A" (where # are digits).
78///
79/// # Returns
80///
81/// `true` if the NINO passes basic structural and invalid pattern checks, `false` otherwise.
82pub fn is_valid_uk_nino_programmatically(nino: &str) -> bool {
83    const NINO_LENGTH: usize = 9;
84
85    let nino_normalized: Cow<str> = if nino.chars().any(|c: char| c.is_ascii_lowercase()) {
86        Cow::Owned(nino.to_uppercase())
87    } else {
88        Cow::Borrowed(nino)
89    };
90    
91    let nino_no_spaces = nino_normalized.chars().filter(|c| !c.is_whitespace()).collect::<String>();
92    
93    if nino_no_spaces.len() != NINO_LENGTH {
94        return false;
95    }
96
97    let mut chars = nino_no_spaces.chars();
98    
99    // Check prefix chars using iterators for efficiency
100    let (Some(prefix_char1), Some(prefix_char2)) = (chars.next(), chars.next()) else { return false; };
101    if !prefix_char1.is_ascii_alphabetic() || !prefix_char2.is_ascii_alphabetic() {
102        return false;
103    }
104    
105    // Check for invalid prefix characters and combinations
106    let prefix_str = &nino_no_spaces[0..2];
107    if INVALID_NINO_PREFIXES.contains(prefix_str) {
108        return false;
109    }
110    if INVALID_NINO_PREFIX_CHARS.contains(&prefix_char1) || INVALID_NINO_PREFIX_CHARS.contains(&prefix_char2) {
111        return false;
112    }
113
114    // Check that the middle 6 characters are digits
115    if !chars.by_ref().take(6).all(|c| c.is_ascii_digit()) {
116        return false;
117    }
118
119    // Check suffix character
120    let Some(suffix_char) = chars.next() else { return false; };
121    if !VALID_NINO_SUFFIX_CHARS.contains(&suffix_char) {
122        return false;
123    }
124
125    // Check if there are any remaining characters
126    if chars.next().is_some() {
127        return false;
128    }
129
130    true
131}
132
133/// Validates a number using the Luhn algorithm.
134///
135/// The Luhn algorithm, also known as the Mod 10 algorithm, is a simple checksum
136/// formula used to validate a variety of identification numbers, such as
137/// credit card numbers.
138///
139/// # Arguments
140///
141/// * `num_str` - A string slice containing only digits.
142///
143/// # Returns
144///
145/// `true` if the number is valid according to the Luhn algorithm, `false` otherwise.
146pub fn is_valid_luhn(num_str: &str) -> bool {
147    let mut sum = 0;
148    let mut alternate = false;
149
150    for c in num_str.chars().rev() {
151        let Some(mut digit) = c.to_digit(10) else { return false; };
152
153        if alternate {
154            digit *= 2;
155            if digit > 9 {
156                digit -= 9;
157            }
158        }
159        sum += digit;
160        alternate = !alternate;
161    }
162
163    sum % 10 == 0
164}
165
166/// Helper function to validate credit card numbers based on the Luhn algorithm.
167///
168/// This function first strips all non-digit characters from the input string
169/// and then applies the Luhn algorithm to the resulting digit string.
170///
171/// # Arguments
172///
173/// * `cc_number` - The credit card number string slice to validate.
174///
175/// # Returns
176///
177/// `true` if the number is valid according to the Luhn algorithm, `false` otherwise.
178pub fn is_valid_credit_card_programmatically(cc_number: &str) -> bool {
179    let digits: String = cc_number.chars().filter(|c| c.is_ascii_digit()).collect();
180    if digits.is_empty() {
181        return false;
182    }
183    is_valid_luhn(&digits)
184}