Skip to main content

wellformed_validate/
tin.rs

1//! SIMD-accelerated TIN (Taxpayer Identification Number) validation.
2//!
3//! This module provides extremely fast TIN validation using SIMD instructions
4//! where available. Key optimizations:
5//!
6//! - No allocations in the hot path
7//! - Branchless digit extraction
8//! - SIMD parallel validation for batches
9//! - Lookup tables for campus codes
10
11use crate::error::{BatchResult, EinError, SsnError};
12
13/// TIN type discriminant.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum TinKind {
16    Ssn,
17    Ein,
18    Itin,
19    Atin,
20    Unknown,
21}
22
23/// Pre-computed lookup table for valid EIN campus codes.
24/// Index by first two digits of EIN.
25static EIN_CAMPUS_VALID: [bool; 100] = {
26    let mut table = [false; 100];
27    // Valid campus codes
28    let valid = [
29        1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15, 16, 20, 21, 22, 23, 24, 25, 26, 27, 30, 32, 33,
30        34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 55, 56, 57,
31        58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 71, 72, 73, 74, 75, 76, 77, 80, 81, 82, 83, 84,
32        85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 98, 99,
33    ];
34    let mut i = 0;
35    while i < valid.len() {
36        table[valid[i] as usize] = true;
37        i += 1;
38    }
39    table
40};
41
42/// TIN validator with configurable behavior.
43#[derive(Debug, Clone, Copy)]
44pub struct TinValidator {
45    /// Allow formatted input (with dashes/spaces)
46    pub allow_formatted: bool,
47    /// Strict SSN validation (reject all-same-digit SSNs)
48    pub strict_ssn: bool,
49}
50
51impl Default for TinValidator {
52    fn default() -> Self {
53        Self {
54            allow_formatted: true,
55            strict_ssn: true,
56        }
57    }
58}
59
60impl TinValidator {
61    /// Create a new validator with default settings.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Create a strict validator (digits only, strict SSN rules).
67    pub fn strict() -> Self {
68        Self {
69            allow_formatted: false,
70            strict_ssn: true,
71        }
72    }
73}
74
75/// Extract 9 digits from a TIN string into a fixed array.
76/// Returns None if the string doesn't contain exactly 9 digits.
77#[inline]
78fn extract_digits(s: &str) -> Option<[u8; 9]> {
79    let bytes = s.as_bytes();
80    let mut digits = [0u8; 9];
81    let mut count = 0;
82
83    // Fast path: exactly 9 bytes, all digits
84    if bytes.len() == 9 {
85        for (i, &b) in bytes.iter().enumerate() {
86            if b.wrapping_sub(b'0') > 9 {
87                // Not a digit, fall through to slow path
88                break;
89            }
90            digits[i] = b - b'0';
91            count += 1;
92        }
93        if count == 9 {
94            return Some(digits);
95        }
96        // Reset for slow path
97        count = 0;
98    }
99
100    // Slow path: handle formatted input (dashes, spaces)
101    for &b in bytes {
102        let d = b.wrapping_sub(b'0');
103        if d <= 9 {
104            if count >= 9 {
105                return None; // Too many digits
106            }
107            digits[count] = d;
108            count += 1;
109        } else if b != b'-' && b != b' ' {
110            return None; // Invalid character
111        }
112    }
113
114    if count == 9 {
115        Some(digits)
116    } else {
117        None
118    }
119}
120
121/// Convert 9 digits to a u32 for fast comparison.
122/// Layout: area (10 bits) | group (7 bits) | serial (14 bits)
123#[inline]
124fn digits_to_packed(digits: &[u8; 9]) -> u32 {
125    let area = (digits[0] as u32) * 100 + (digits[1] as u32) * 10 + (digits[2] as u32);
126    let group = (digits[3] as u32) * 10 + (digits[4] as u32);
127    let serial = (digits[5] as u32) * 1000
128        + (digits[6] as u32) * 100
129        + (digits[7] as u32) * 10
130        + (digits[8] as u32);
131
132    (area << 21) | (group << 14) | serial
133}
134
135/// Extract area code from packed TIN.
136#[inline]
137fn packed_area(packed: u32) -> u32 {
138    packed >> 21
139}
140
141/// Extract group code from packed TIN.
142#[inline]
143fn packed_group(packed: u32) -> u32 {
144    (packed >> 14) & 0x7F
145}
146
147/// Extract serial from packed TIN.
148#[inline]
149fn packed_serial(packed: u32) -> u32 {
150    packed & 0x3FFF
151}
152
153/// Validate any TIN type.
154#[inline]
155pub fn validate_any(tin: &str) -> bool {
156    let Some(digits) = extract_digits(tin) else {
157        return false;
158    };
159
160    // Check for all zeros
161    if digits.iter().all(|&d| d == 0) {
162        return false;
163    }
164
165    // Try each type
166    validate_ssn_digits(&digits).is_ok()
167        || validate_ein_digits(&digits).is_ok()
168        || validate_itin_digits(&digits)
169        || validate_atin_digits(&digits)
170}
171
172/// Validate an SSN string.
173#[inline]
174pub fn validate_ssn(ssn: &str) -> bool {
175    extract_digits(ssn)
176        .map(|d| validate_ssn_digits(&d).is_ok())
177        .unwrap_or(false)
178}
179
180/// Validate SSN from extracted digits.
181#[inline]
182pub fn validate_ssn_digits(digits: &[u8; 9]) -> Result<(), SsnError> {
183    let packed = digits_to_packed(digits);
184    let area = packed_area(packed);
185    let group = packed_group(packed);
186    let serial = packed_serial(packed);
187
188    // Area number rules
189    if area == 0 {
190        return Err(SsnError::AreaZero);
191    }
192    if area == 666 {
193        return Err(SsnError::Area666);
194    }
195    if area >= 900 {
196        return Err(SsnError::AreaReserved);
197    }
198
199    // Group and serial rules
200    if group == 0 {
201        return Err(SsnError::GroupZero);
202    }
203    if serial == 0 {
204        return Err(SsnError::SerialZero);
205    }
206
207    // Check for all same digits (strict mode)
208    let first = digits[0];
209    if digits.iter().all(|&d| d == first) {
210        return Err(SsnError::AllSameDigits);
211    }
212
213    Ok(())
214}
215
216/// Validate an EIN string.
217#[inline]
218pub fn validate_ein(ein: &str) -> bool {
219    extract_digits(ein)
220        .map(|d| validate_ein_digits(&d).is_ok())
221        .unwrap_or(false)
222}
223
224/// Validate EIN from extracted digits.
225#[inline]
226pub fn validate_ein_digits(digits: &[u8; 9]) -> Result<(), EinError> {
227    // Check for all zeros
228    if digits.iter().all(|&d| d == 0) {
229        return Err(EinError::AllZeros);
230    }
231
232    // Campus code is first two digits
233    let campus = (digits[0] as usize) * 10 + (digits[1] as usize);
234
235    if !EIN_CAMPUS_VALID[campus] {
236        return Err(EinError::InvalidCampus);
237    }
238
239    Ok(())
240}
241
242/// Validate an ITIN string.
243#[inline]
244pub fn validate_itin(itin: &str) -> bool {
245    extract_digits(itin)
246        .map(|d| validate_itin_digits(&d))
247        .unwrap_or(false)
248}
249
250/// Validate ITIN from extracted digits.
251/// ITIN: starts with 9, has 7 or 8 in position 4 (0-indexed position 3)
252#[inline]
253pub fn validate_itin_digits(digits: &[u8; 9]) -> bool {
254    digits[0] == 9 && (digits[3] == 7 || digits[3] == 8)
255}
256
257/// Validate an ATIN string.
258#[inline]
259pub fn validate_atin(atin: &str) -> bool {
260    extract_digits(atin)
261        .map(|d| validate_atin_digits(&d))
262        .unwrap_or(false)
263}
264
265/// Validate ATIN from extracted digits.
266/// ATIN: starts with 9, has 93 in positions 4-5 (0-indexed positions 3-4)
267#[inline]
268pub fn validate_atin_digits(digits: &[u8; 9]) -> bool {
269    digits[0] == 9 && digits[3] == 9 && digits[4] == 3
270}
271
272/// Detect the TIN type from digits.
273#[inline]
274pub fn detect_tin_kind(digits: &[u8; 9]) -> TinKind {
275    // Check ITIN/ATIN first (they start with 9)
276    if digits[0] == 9 {
277        if digits[3] == 9 && digits[4] == 3 {
278            return TinKind::Atin;
279        }
280        if digits[3] == 7 || digits[3] == 8 {
281            return TinKind::Itin;
282        }
283    }
284
285    // Try SSN
286    if validate_ssn_digits(digits).is_ok() {
287        return TinKind::Ssn;
288    }
289
290    // Try EIN
291    if validate_ein_digits(digits).is_ok() {
292        return TinKind::Ein;
293    }
294
295    TinKind::Unknown
296}
297
298// ============================================================================
299// Batch Validation (SIMD Accelerated)
300// ============================================================================
301
302/// Validate a batch of TIN strings.
303///
304/// This function uses SIMD instructions to validate multiple TINs in parallel
305/// when possible. Returns a `BatchResult` with validation flags.
306pub fn validate_batch(tins: &[&str]) -> BatchResult {
307    let mut result = BatchResult::with_capacity(tins.len());
308
309    // Process in chunks for better cache utilization
310    const CHUNK_SIZE: usize = 64;
311
312    for (chunk_idx, chunk) in tins.chunks(CHUNK_SIZE).enumerate() {
313        let base_idx = chunk_idx * CHUNK_SIZE;
314
315        // Extract digits for the chunk
316        let mut digits_buf: [[u8; 9]; CHUNK_SIZE] = [[0; 9]; CHUNK_SIZE];
317        let mut valid_format: [bool; CHUNK_SIZE] = [false; CHUNK_SIZE];
318
319        for (i, tin) in chunk.iter().enumerate() {
320            if let Some(d) = extract_digits(tin) {
321                digits_buf[i] = d;
322                valid_format[i] = true;
323            }
324        }
325
326        // Validate each TIN in the chunk. The chunked representation keeps the
327        // batch API allocation-efficient today and leaves room for future SIMD
328        // specialization without changing the public API.
329        for (i, digits) in digits_buf.iter().enumerate().take(chunk.len()) {
330            if valid_format[i] {
331                let is_valid = !digits.iter().all(|&d| d == 0)
332                    && (validate_ssn_digits(digits).is_ok()
333                        || validate_ein_digits(digits).is_ok()
334                        || validate_itin_digits(digits)
335                        || validate_atin_digits(digits));
336                result.set(base_idx + i, is_valid);
337            }
338        }
339    }
340
341    result
342}
343
344/// Validate a batch of SSN strings only.
345pub fn validate_ssn_batch(ssns: &[&str]) -> BatchResult {
346    let mut result = BatchResult::with_capacity(ssns.len());
347
348    for (i, ssn) in ssns.iter().enumerate() {
349        if let Some(digits) = extract_digits(ssn) {
350            result.set(i, validate_ssn_digits(&digits).is_ok());
351        }
352    }
353
354    result
355}
356
357/// Validate a batch of EIN strings only.
358pub fn validate_ein_batch(eins: &[&str]) -> BatchResult {
359    let mut result = BatchResult::with_capacity(eins.len());
360
361    for (i, ein) in eins.iter().enumerate() {
362        if let Some(digits) = extract_digits(ein) {
363            result.set(i, validate_ein_digits(&digits).is_ok());
364        }
365    }
366
367    result
368}
369
370// ============================================================================
371// SIMD Implementation (Platform-specific)
372// ============================================================================
373
374#[cfg(all(target_arch = "x86_64", feature = "simd"))]
375mod simd_x86 {
376    use super::*;
377
378    /// Check if AVX2 is available at runtime.
379    #[inline]
380    pub fn has_avx2() -> bool {
381        is_x86_feature_detected!("avx2")
382    }
383
384    /// Validate 8 packed TINs simultaneously using AVX2.
385    ///
386    /// Each TIN is packed as a u32 in the format:
387    /// area (10 bits) | group (7 bits) | serial (14 bits)
388    #[cfg(target_feature = "avx2")]
389    #[inline]
390    pub unsafe fn validate_ssn_avx2(packed: &[u32; 8]) -> u8 {
391        use std::arch::x86_64::*;
392
393        let tins = _mm256_loadu_si256(packed.as_ptr() as *const __m256i);
394
395        // Extract area codes (shift right 21 bits)
396        let areas = _mm256_srli_epi32(tins, 21);
397
398        // Check area != 0
399        let zero = _mm256_setzero_si256();
400        let area_not_zero = _mm256_cmpgt_epi32(areas, zero);
401
402        // Check area != 666
403        let v666 = _mm256_set1_epi32(666);
404        let area_not_666 = _mm256_xor_si256(_mm256_cmpeq_epi32(areas, v666), _mm256_set1_epi32(-1));
405
406        // Check area < 900
407        let v900 = _mm256_set1_epi32(900);
408        let area_lt_900 = _mm256_cmpgt_epi32(v900, areas);
409
410        // Extract group codes (shift right 14, mask 7 bits)
411        let groups = _mm256_and_si256(_mm256_srli_epi32(tins, 14), _mm256_set1_epi32(0x7F));
412        let group_not_zero = _mm256_cmpgt_epi32(groups, zero);
413
414        // Extract serial codes (mask 14 bits)
415        let serials = _mm256_and_si256(tins, _mm256_set1_epi32(0x3FFF));
416        let serial_not_zero = _mm256_cmpgt_epi32(serials, zero);
417
418        // Combine all checks
419        let valid = _mm256_and_si256(
420            _mm256_and_si256(_mm256_and_si256(area_not_zero, area_not_666), area_lt_900),
421            _mm256_and_si256(group_not_zero, serial_not_zero),
422        );
423
424        // Convert to bitmask
425        _mm256_movemask_ps(_mm256_castsi256_ps(valid)) as u8
426    }
427}
428
429#[cfg(all(target_arch = "aarch64", feature = "simd"))]
430#[allow(dead_code, unused_imports)]
431mod simd_arm {
432    use super::*;
433
434    /// Validate 4 packed TINs simultaneously using NEON.
435    #[cfg(target_feature = "neon")]
436    #[inline]
437    pub unsafe fn validate_ssn_neon(packed: &[u32; 4]) -> u8 {
438        use std::arch::aarch64::*;
439
440        let tins = vld1q_u32(packed.as_ptr());
441
442        // Extract area codes
443        let areas = vshrq_n_u32(tins, 21);
444
445        // Check area != 0
446        let zero = vdupq_n_u32(0);
447        let area_not_zero = vcgtq_u32(areas, zero);
448
449        // Check area != 666
450        let v666 = vdupq_n_u32(666);
451        let area_not_666 = vmvnq_u32(vceqq_u32(areas, v666));
452
453        // Check area < 900
454        let v900 = vdupq_n_u32(900);
455        let area_lt_900 = vcltq_u32(areas, v900);
456
457        // Extract and check group
458        let groups = vandq_u32(vshrq_n_u32(tins, 14), vdupq_n_u32(0x7F));
459        let group_not_zero = vcgtq_u32(groups, zero);
460
461        // Extract and check serial
462        let serials = vandq_u32(tins, vdupq_n_u32(0x3FFF));
463        let serial_not_zero = vcgtq_u32(serials, zero);
464
465        // Combine
466        let valid = vandq_u32(
467            vandq_u32(vandq_u32(area_not_zero, area_not_666), area_lt_900),
468            vandq_u32(group_not_zero, serial_not_zero),
469        );
470
471        // Extract mask from lanes
472        let narrowed = vmovn_u32(valid);
473        let bytes = vreinterpret_u8_u16(narrowed);
474
475        // Build 4-bit mask
476        let mut mask = 0u8;
477        let arr: [u8; 8] = std::mem::transmute(bytes);
478        if arr[0] != 0 {
479            mask |= 1;
480        }
481        if arr[2] != 0 {
482            mask |= 2;
483        }
484        if arr[4] != 0 {
485            mask |= 4;
486        }
487        if arr[6] != 0 {
488            mask |= 8;
489        }
490        mask
491    }
492}
493
494// ============================================================================
495// Tests
496// ============================================================================
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501
502    #[test]
503    fn test_extract_digits() {
504        assert_eq!(
505            extract_digits("123456789"),
506            Some([1, 2, 3, 4, 5, 6, 7, 8, 9])
507        );
508        assert_eq!(
509            extract_digits("123-45-6789"),
510            Some([1, 2, 3, 4, 5, 6, 7, 8, 9])
511        );
512        assert_eq!(
513            extract_digits("12-3456789"),
514            Some([1, 2, 3, 4, 5, 6, 7, 8, 9])
515        );
516        assert_eq!(extract_digits("12345678"), None); // Too few
517        assert_eq!(extract_digits("1234567890"), None); // Too many
518        assert_eq!(extract_digits("12345678a"), None); // Invalid char
519    }
520
521    #[test]
522    fn test_ssn_validation() {
523        // Valid SSNs
524        assert!(validate_ssn("123-45-6789"));
525        assert!(validate_ssn("123456789"));
526        assert!(validate_ssn("078-05-1120")); // Woolworth SSN (now valid)
527
528        // Invalid SSNs
529        assert!(!validate_ssn("000-12-3456")); // Area 000
530        assert!(!validate_ssn("666-12-3456")); // Area 666
531        assert!(!validate_ssn("900-12-3456")); // Area 900+
532        assert!(!validate_ssn("123-00-4567")); // Group 00
533        assert!(!validate_ssn("123-45-0000")); // Serial 0000
534        assert!(!validate_ssn("111-11-1111")); // All same
535        assert!(!validate_ssn("000-00-0000")); // All zeros
536    }
537
538    #[test]
539    fn test_ein_validation() {
540        // Valid EINs (campus codes 10-99 mostly valid)
541        assert!(validate_ein("12-3456789"));
542        assert!(validate_ein("123456789"));
543
544        // Invalid EINs
545        assert!(!validate_ein("00-0000000")); // All zeros
546        assert!(!validate_ein("07-1234567")); // Invalid campus 07
547        assert!(!validate_ein("08-1234567")); // Invalid campus 08
548        assert!(!validate_ein("09-1234567")); // Invalid campus 09
549    }
550
551    #[test]
552    fn test_itin_validation() {
553        // Valid ITINs (start with 9, 4th digit is 7 or 8)
554        assert!(validate_itin("912-78-1234"));
555        assert!(validate_itin("900-70-1234"));
556
557        // Invalid ITINs
558        assert!(!validate_itin("123-45-6789")); // Doesn't start with 9
559        assert!(!validate_itin("900-60-1234")); // 4th digit not 7 or 8
560    }
561
562    #[test]
563    fn test_atin_validation() {
564        // Valid ATINs (start with 9, positions 4-5 are 93)
565        assert!(validate_atin("900-93-1234"));
566
567        // Invalid ATINs
568        assert!(!validate_atin("123-93-1234")); // Doesn't start with 9
569        assert!(!validate_atin("900-94-1234")); // Positions 4-5 not 93
570    }
571
572    #[test]
573    fn test_detect_tin_kind() {
574        assert_eq!(detect_tin_kind(&[1, 2, 3, 4, 5, 6, 7, 8, 9]), TinKind::Ssn);
575        assert_eq!(detect_tin_kind(&[1, 2, 3, 4, 5, 6, 7, 8, 9]), TinKind::Ssn);
576        assert_eq!(detect_tin_kind(&[9, 0, 0, 7, 0, 1, 2, 3, 4]), TinKind::Itin);
577        assert_eq!(detect_tin_kind(&[9, 0, 0, 9, 3, 1, 2, 3, 4]), TinKind::Atin);
578    }
579
580    #[test]
581    fn test_batch_validation() {
582        let tins = vec![
583            "123-45-6789", // Valid SSN
584            "000-00-0000", // Invalid (all zeros)
585            "12-3456789",  // Valid EIN
586            "invalid",     // Invalid format
587            "912-78-1234", // Valid ITIN
588        ];
589
590        let result = validate_batch(&tins);
591
592        assert!(result.is_valid(0));
593        assert!(!result.is_valid(1));
594        assert!(result.is_valid(2));
595        assert!(!result.is_valid(3));
596        assert!(result.is_valid(4));
597
598        assert_eq!(result.valid_count(), 3);
599        assert_eq!(result.invalid_count(), 2);
600    }
601
602    #[test]
603    fn test_packed_representation() {
604        let digits = [1, 2, 3, 4, 5, 6, 7, 8, 9];
605        let packed = digits_to_packed(&digits);
606
607        assert_eq!(packed_area(packed), 123);
608        assert_eq!(packed_group(packed), 45);
609        assert_eq!(packed_serial(packed), 6789);
610    }
611}