1use crate::types::EntityType;
12
13pub fn validate_entity(entity_type: &EntityType, value: &str) -> f32 {
20 match entity_type {
21 EntityType::CreditCard => validate_credit_card(value),
22 EntityType::IbanCode | EntityType::Iban => validate_iban(value),
23 EntityType::UsSsn => validate_us_ssn(value),
24 EntityType::UkNino => validate_uk_nino(value),
25 EntityType::UkNhs => validate_uk_nhs(value),
26 EntityType::Isbn => validate_isbn(value),
27 EntityType::IpAddress => validate_ip_address(value),
28 EntityType::HttpBasicAuth => validate_http_basic_auth(value),
29 _ => 1.0, }
31}
32
33pub fn validate_http_basic_auth(value: &str) -> f32 {
38 let token = value
39 .strip_prefix("Basic ")
40 .or_else(|| value.strip_prefix("basic "))
41 .unwrap_or(value);
42 if !is_canonical_b64(token) {
43 return 0.0;
44 }
45 let Ok(bytes) = decode_base64(token) else {
46 return 0.0;
47 };
48 let Ok(plain) = std::str::from_utf8(&bytes) else {
49 return 0.0;
50 };
51 if plain.contains('\0') || !plain.chars().all(|c| c.is_ascii() && !c.is_ascii_control()) {
52 return 0.0;
53 }
54 let Some((user, password)) = plain.split_once(':') else {
55 return 0.0;
56 };
57 if user.is_empty() || password.is_empty() {
58 return 0.0;
59 }
60 1.0
61}
62
63fn is_canonical_b64(token: &str) -> bool {
64 if token.is_empty() || !token.len().is_multiple_of(4) {
65 return false;
66 }
67 let pad = token.bytes().rev().take_while(|&b| b == b'=').count();
68 if pad > 2 {
69 return false;
70 }
71 let body_len = token.len() - pad;
72 if !token.as_bytes()[..body_len]
73 .iter()
74 .all(|b| b.is_ascii_alphanumeric() || *b == b'+' || *b == b'/')
75 {
76 return false;
77 }
78 let Ok(bytes) = decode_base64(token) else {
80 return false;
81 };
82 encode_base64(&bytes) == token
83}
84
85fn encode_base64(bytes: &[u8]) -> String {
86 const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
87 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
88 let mut i = 0;
89 while i < bytes.len() {
90 let b0 = bytes[i];
91 let b1 = bytes.get(i + 1).copied();
92 let b2 = bytes.get(i + 2).copied();
93 out.push(TABLE[(b0 >> 2) as usize] as char);
94 match (b1, b2) {
95 (Some(b1), Some(b2)) => {
96 out.push(TABLE[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
97 out.push(TABLE[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
98 out.push(TABLE[(b2 & 0x3f) as usize] as char);
99 }
100 (Some(b1), None) => {
101 out.push(TABLE[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
102 out.push(TABLE[((b1 & 0x0f) << 2) as usize] as char);
103 out.push('=');
104 }
105 (None, _) => {
106 out.push(TABLE[((b0 & 0x03) << 4) as usize] as char);
107 out.push('=');
108 out.push('=');
109 }
110 }
111 i += 3;
112 }
113 out
114}
115
116fn decode_base64(token: &str) -> Result<Vec<u8>, ()> {
117 const TABLE: [i8; 256] = {
118 let mut t = [-1i8; 256];
119 let mut i = 0;
120 while i < 26 {
121 t[(b'A' + i as u8) as usize] = i;
122 t[(b'a' + i as u8) as usize] = 26 + i;
123 i += 1;
124 }
125 i = 0;
126 while i < 10 {
127 t[(b'0' + i as u8) as usize] = 52 + i;
128 i += 1;
129 }
130 t[b'+' as usize] = 62;
131 t[b'/' as usize] = 63;
132 t
133 };
134 let mut out = Vec::with_capacity(token.len() / 4 * 3);
135 let bytes = token.as_bytes();
136 let mut i = 0;
137 while i < bytes.len() {
138 let a = TABLE[bytes[i] as usize];
139 let b = TABLE[bytes[i + 1] as usize];
140 if a < 0 || b < 0 {
141 return Err(());
142 }
143 out.push(((a as u8) << 2) | ((b as u8) >> 4));
144 if bytes[i + 2] != b'=' {
145 let c = TABLE[bytes[i + 2] as usize];
146 if c < 0 {
147 return Err(());
148 }
149 out.push(((b as u8) << 4) | ((c as u8) >> 2));
150 if bytes[i + 3] != b'=' {
151 let d = TABLE[bytes[i + 3] as usize];
152 if d < 0 {
153 return Err(());
154 }
155 out.push(((c as u8) << 6) | (d as u8));
156 }
157 } else if bytes[i + 3] != b'=' {
158 return Err(());
159 }
160 i += 4;
161 }
162 Ok(out)
163}
164
165pub fn validate_credit_card(value: &str) -> f32 {
169 let digits: Vec<u32> = value
170 .chars()
171 .filter(|c| c.is_ascii_digit())
172 .filter_map(|c| c.to_digit(10))
173 .collect();
174
175 if digits.len() < 13 || digits.len() > 19 {
176 return 0.0;
177 }
178
179 if luhn_check(&digits) {
180 1.0
181 } else {
182 0.0
183 }
184}
185
186fn luhn_check(digits: &[u32]) -> bool {
188 let mut sum = 0;
189 let mut double = false;
190
191 for &digit in digits.iter().rev() {
192 let mut d = digit;
193 if double {
194 d *= 2;
195 if d > 9 {
196 d -= 9;
197 }
198 }
199 sum += d;
200 double = !double;
201 }
202
203 sum.is_multiple_of(10)
204}
205
206pub fn validate_iban(value: &str) -> f32 {
212 let cleaned: String = value.chars().filter(|c| c.is_alphanumeric()).collect();
213
214 if cleaned.len() < 15 || cleaned.len() > 34 {
215 return 0.0;
216 }
217
218 let country_code: String = cleaned.chars().take(2).collect();
220 if !country_code.chars().all(|c| c.is_ascii_alphabetic()) {
221 return 0.0;
222 }
223
224 let expected_length = get_iban_length(&country_code);
226 if expected_length > 0 && cleaned.len() != expected_length {
227 return 0.5; }
229
230 if validate_iban_checksum(&cleaned) {
232 1.0
233 } else {
234 0.0
235 }
236}
237
238fn get_iban_length(country_code: &str) -> usize {
240 match country_code.to_uppercase().as_str() {
241 "GB" => 22,
242 "DE" => 22,
243 "FR" => 27,
244 "ES" => 24,
245 "IT" => 27,
246 "NL" => 18,
247 "BE" => 16,
248 "AT" => 20,
249 "CH" => 21,
250 "IE" => 22,
251 "PL" => 28,
252 "PT" => 25,
253 "SE" => 24,
254 "NO" => 15,
255 "DK" => 18,
256 "FI" => 18,
257 _ => 0, }
259}
260
261fn validate_iban_checksum(iban: &str) -> bool {
263 let rearranged = format!("{}{}", &iban[4..], &iban[..4]);
265
266 let mut numeric = String::new();
268 for c in rearranged.chars() {
269 if c.is_ascii_digit() {
270 numeric.push(c);
271 } else if c.is_ascii_alphabetic() {
272 let val = c.to_ascii_uppercase() as u32 - 'A' as u32 + 10;
273 numeric.push_str(&val.to_string());
274 }
275 }
276
277 let mut remainder: u64 = 0;
279 for chunk in numeric.as_bytes().chunks(9) {
280 let chunk_str: String = std::str::from_utf8(chunk).unwrap_or("0").to_string();
281 let combined = format!("{}{}", remainder, chunk_str);
282 remainder = combined.parse::<u64>().unwrap_or(0) % 97;
283 }
284
285 remainder == 1
286}
287
288pub fn validate_us_ssn(value: &str) -> f32 {
295 let digits: String = value.chars().filter(|c| c.is_ascii_digit()).collect();
296
297 if digits.len() != 9 {
298 return 0.0;
299 }
300
301 let area: u32 = digits[0..3].parse().unwrap_or(0);
302 let group: u32 = digits[3..5].parse().unwrap_or(0);
303 let serial: u32 = digits[5..9].parse().unwrap_or(0);
304
305 if area == 0 || area == 666 || area >= 900 {
307 return 0.0;
308 }
309
310 if group == 0 || serial == 0 {
312 return 0.0;
313 }
314
315 1.0
316}
317
318pub fn validate_uk_nino(value: &str) -> f32 {
325 let cleaned: String = value
326 .chars()
327 .filter(|c| c.is_alphanumeric())
328 .collect::<String>()
329 .to_uppercase();
330
331 if cleaned.len() != 9 {
332 return 0.0;
333 }
334
335 let prefix: String = cleaned.chars().take(2).collect();
336 let suffix = cleaned.chars().last().unwrap_or('X');
337
338 let invalid_prefixes = ["BG", "GB", "NK", "KN", "TN", "NT", "ZZ"];
340 if invalid_prefixes.contains(&prefix.as_str()) {
341 return 0.0;
342 }
343
344 let first = prefix.chars().next().unwrap_or('X');
346 if "DFIQUV".contains(first) {
347 return 0.0;
348 }
349
350 let second = prefix.chars().nth(1).unwrap_or('X');
352 if "DFIOQUV".contains(second) {
353 return 0.0;
354 }
355
356 if !"ABCD".contains(suffix) {
358 return 0.0;
359 }
360
361 let middle: String = cleaned.chars().skip(2).take(6).collect();
363 if !middle.chars().all(|c| c.is_ascii_digit()) {
364 return 0.0;
365 }
366
367 1.0
368}
369
370pub fn validate_uk_nhs(value: &str) -> f32 {
372 let digits: Vec<u32> = value
373 .chars()
374 .filter(|c| c.is_ascii_digit())
375 .filter_map(|c| c.to_digit(10))
376 .collect();
377
378 if digits.len() != 10 {
379 return 0.0;
380 }
381
382 let weights = [10, 9, 8, 7, 6, 5, 4, 3, 2];
384 let sum: u32 = digits
385 .iter()
386 .take(9)
387 .zip(weights.iter())
388 .map(|(d, w)| d * w)
389 .sum();
390
391 let remainder = 11 - (sum % 11);
392 let check_digit = if remainder == 11 { 0 } else { remainder };
393
394 if check_digit == 10 {
395 return 0.0; }
397
398 if digits[9] == check_digit {
399 1.0
400 } else {
401 0.0
402 }
403}
404
405pub fn validate_isbn(value: &str) -> f32 {
407 let cleaned: String = value
408 .chars()
409 .filter(|c| c.is_ascii_digit() || *c == 'X' || *c == 'x')
410 .collect();
411
412 match cleaned.len() {
413 10 => validate_isbn10(&cleaned),
414 13 => validate_isbn13(&cleaned),
415 _ => 0.0,
416 }
417}
418
419fn validate_isbn10(isbn: &str) -> f32 {
420 let mut sum = 0;
421 for (i, c) in isbn.chars().enumerate() {
422 let digit = if c == 'X' || c == 'x' {
423 10
424 } else {
425 c.to_digit(10).unwrap_or(0)
426 };
427 sum += digit * (10 - i as u32);
428 }
429
430 if sum.is_multiple_of(11) {
431 1.0
432 } else {
433 0.0
434 }
435}
436
437fn validate_isbn13(isbn: &str) -> f32 {
438 let digits: Vec<u32> = isbn.chars().filter_map(|c| c.to_digit(10)).collect();
439
440 if digits.len() != 13 {
441 return 0.0;
442 }
443
444 let sum: u32 = digits
445 .iter()
446 .enumerate()
447 .map(|(i, &d)| if i % 2 == 0 { d } else { d * 3 })
448 .sum();
449
450 if sum.is_multiple_of(10) {
451 1.0
452 } else {
453 0.0
454 }
455}
456
457pub fn validate_ip_address(value: &str) -> f32 {
459 let octets: Vec<&str> = value.split('.').collect();
460
461 if octets.len() != 4 {
462 return 0.0;
463 }
464
465 for octet in octets {
466 match octet.parse::<u32>() {
467 Ok(n) if n <= 255 => continue,
468 _ => return 0.0,
469 }
470 }
471
472 1.0
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn test_luhn_valid_cards() {
481 assert_eq!(validate_credit_card("4532015112830366"), 1.0);
483 assert_eq!(validate_credit_card("5425233430109903"), 1.0);
484 assert_eq!(validate_credit_card("374245455400126"), 1.0);
485 }
486
487 #[test]
488 fn test_luhn_invalid_cards() {
489 assert_eq!(validate_credit_card("4532015112830367"), 0.0);
490 assert_eq!(validate_credit_card("1234567890123456"), 0.0);
491 }
492
493 #[test]
494 fn test_valid_ssn() {
495 assert_eq!(validate_us_ssn("123-45-6789"), 1.0);
496 assert_eq!(validate_us_ssn("123456789"), 1.0);
497 }
498
499 #[test]
500 fn test_invalid_ssn() {
501 assert_eq!(validate_us_ssn("000-12-3456"), 0.0); assert_eq!(validate_us_ssn("666-12-3456"), 0.0); assert_eq!(validate_us_ssn("900-12-3456"), 0.0); assert_eq!(validate_us_ssn("123-00-3456"), 0.0); assert_eq!(validate_us_ssn("123-45-0000"), 0.0); }
507
508 #[test]
509 fn test_valid_uk_nino() {
510 assert_eq!(validate_uk_nino("AB123456C"), 1.0);
511 assert_eq!(validate_uk_nino("JG103759A"), 1.0);
512 }
513
514 #[test]
515 fn test_invalid_uk_nino() {
516 assert_eq!(validate_uk_nino("BG123456A"), 0.0); assert_eq!(validate_uk_nino("DA123456A"), 0.0); assert_eq!(validate_uk_nino("AB123456E"), 0.0); }
520
521 #[test]
522 fn test_valid_iban() {
523 assert_eq!(validate_iban("GB82WEST12345698765432"), 1.0);
524 assert_eq!(validate_iban("DE89370400440532013000"), 1.0);
525 }
526
527 #[test]
528 fn test_invalid_iban() {
529 assert_eq!(validate_iban("GB82WEST12345698765433"), 0.0); assert_eq!(validate_iban("XX00000000000000"), 0.0);
531 }
532
533 #[test]
534 fn test_valid_isbn() {
535 assert_eq!(validate_isbn("0-306-40615-2"), 1.0); assert_eq!(validate_isbn("978-0-306-40615-7"), 1.0); }
538
539 #[test]
540 fn test_valid_ip() {
541 assert_eq!(validate_ip_address("192.168.1.1"), 1.0);
542 assert_eq!(validate_ip_address("0.0.0.0"), 1.0);
543 assert_eq!(validate_ip_address("255.255.255.255"), 1.0);
544 }
545
546 #[test]
547 fn test_invalid_ip() {
548 assert_eq!(validate_ip_address("256.1.1.1"), 0.0);
549 assert_eq!(validate_ip_address("1.1.1"), 0.0);
550 }
551
552 #[test]
553 fn test_http_basic_auth_requires_user_password() {
554 let ok = format!("{}{}", "dXNlcm5hbWU6", "cGFzc3dvcmQ=");
555 assert_eq!(validate_http_basic_auth(&ok), 1.0);
556 assert_eq!(validate_http_basic_auth("not-base64"), 0.0);
557 assert_eq!(validate_http_basic_auth("QQ=="), 0.0);
559 }
560
561 #[test]
562 fn test_http_basic_auth_rejects_noncanonical_unused_bits() {
563 assert_eq!(validate_http_basic_auth("dTpwYXM="), 1.0);
565 assert_eq!(validate_http_basic_auth("dTpwYXN="), 0.0);
566 assert_eq!(
567 encode_base64(&decode_base64("dTpwYXM=").unwrap()),
568 "dTpwYXM="
569 );
570 }
571}