use crate::types::EntityType;
pub fn validate_entity(entity_type: &EntityType, value: &str) -> f32 {
match entity_type {
EntityType::CreditCard => validate_credit_card(value),
EntityType::IbanCode | EntityType::Iban => validate_iban(value),
EntityType::UsSsn => validate_us_ssn(value),
EntityType::UkNino => validate_uk_nino(value),
EntityType::UkNhs => validate_uk_nhs(value),
EntityType::Isbn => validate_isbn(value),
EntityType::IpAddress => validate_ip_address(value),
EntityType::HttpBasicAuth => validate_http_basic_auth(value),
_ => 1.0, }
}
pub fn validate_http_basic_auth(value: &str) -> f32 {
let token = value
.strip_prefix("Basic ")
.or_else(|| value.strip_prefix("basic "))
.unwrap_or(value);
if !is_canonical_b64(token) {
return 0.0;
}
let Ok(bytes) = decode_base64(token) else {
return 0.0;
};
let Ok(plain) = std::str::from_utf8(&bytes) else {
return 0.0;
};
if plain.contains('\0') || !plain.chars().all(|c| c.is_ascii() && !c.is_ascii_control()) {
return 0.0;
}
let Some((user, password)) = plain.split_once(':') else {
return 0.0;
};
if user.is_empty() || password.is_empty() {
return 0.0;
}
1.0
}
fn is_canonical_b64(token: &str) -> bool {
if token.is_empty() || !token.len().is_multiple_of(4) {
return false;
}
let pad = token.bytes().rev().take_while(|&b| b == b'=').count();
if pad > 2 {
return false;
}
let body_len = token.len() - pad;
if !token.as_bytes()[..body_len]
.iter()
.all(|b| b.is_ascii_alphanumeric() || *b == b'+' || *b == b'/')
{
return false;
}
let Ok(bytes) = decode_base64(token) else {
return false;
};
encode_base64(&bytes) == token
}
fn encode_base64(bytes: &[u8]) -> String {
const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
let mut i = 0;
while i < bytes.len() {
let b0 = bytes[i];
let b1 = bytes.get(i + 1).copied();
let b2 = bytes.get(i + 2).copied();
out.push(TABLE[(b0 >> 2) as usize] as char);
match (b1, b2) {
(Some(b1), Some(b2)) => {
out.push(TABLE[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
out.push(TABLE[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
out.push(TABLE[(b2 & 0x3f) as usize] as char);
}
(Some(b1), None) => {
out.push(TABLE[(((b0 & 0x03) << 4) | (b1 >> 4)) as usize] as char);
out.push(TABLE[((b1 & 0x0f) << 2) as usize] as char);
out.push('=');
}
(None, _) => {
out.push(TABLE[((b0 & 0x03) << 4) as usize] as char);
out.push('=');
out.push('=');
}
}
i += 3;
}
out
}
fn decode_base64(token: &str) -> Result<Vec<u8>, ()> {
const TABLE: [i8; 256] = {
let mut t = [-1i8; 256];
let mut i = 0;
while i < 26 {
t[(b'A' + i as u8) as usize] = i;
t[(b'a' + i as u8) as usize] = 26 + i;
i += 1;
}
i = 0;
while i < 10 {
t[(b'0' + i as u8) as usize] = 52 + i;
i += 1;
}
t[b'+' as usize] = 62;
t[b'/' as usize] = 63;
t
};
let mut out = Vec::with_capacity(token.len() / 4 * 3);
let bytes = token.as_bytes();
let mut i = 0;
while i < bytes.len() {
let a = TABLE[bytes[i] as usize];
let b = TABLE[bytes[i + 1] as usize];
if a < 0 || b < 0 {
return Err(());
}
out.push(((a as u8) << 2) | ((b as u8) >> 4));
if bytes[i + 2] != b'=' {
let c = TABLE[bytes[i + 2] as usize];
if c < 0 {
return Err(());
}
out.push(((b as u8) << 4) | ((c as u8) >> 2));
if bytes[i + 3] != b'=' {
let d = TABLE[bytes[i + 3] as usize];
if d < 0 {
return Err(());
}
out.push(((c as u8) << 6) | (d as u8));
}
} else if bytes[i + 3] != b'=' {
return Err(());
}
i += 4;
}
Ok(out)
}
pub fn validate_credit_card(value: &str) -> f32 {
let digits: Vec<u32> = value
.chars()
.filter(|c| c.is_ascii_digit())
.filter_map(|c| c.to_digit(10))
.collect();
if digits.len() < 13 || digits.len() > 19 {
return 0.0;
}
if luhn_check(&digits) {
1.0
} else {
0.0
}
}
fn luhn_check(digits: &[u32]) -> bool {
let mut sum = 0;
let mut double = false;
for &digit in digits.iter().rev() {
let mut d = digit;
if double {
d *= 2;
if d > 9 {
d -= 9;
}
}
sum += d;
double = !double;
}
sum.is_multiple_of(10)
}
pub fn validate_iban(value: &str) -> f32 {
let cleaned: String = value.chars().filter(|c| c.is_alphanumeric()).collect();
if cleaned.len() < 15 || cleaned.len() > 34 {
return 0.0;
}
let country_code: String = cleaned.chars().take(2).collect();
if !country_code.chars().all(|c| c.is_ascii_alphabetic()) {
return 0.0;
}
let expected_length = get_iban_length(&country_code);
if expected_length > 0 && cleaned.len() != expected_length {
return 0.5; }
if validate_iban_checksum(&cleaned) {
1.0
} else {
0.0
}
}
fn get_iban_length(country_code: &str) -> usize {
match country_code.to_uppercase().as_str() {
"GB" => 22,
"DE" => 22,
"FR" => 27,
"ES" => 24,
"IT" => 27,
"NL" => 18,
"BE" => 16,
"AT" => 20,
"CH" => 21,
"IE" => 22,
"PL" => 28,
"PT" => 25,
"SE" => 24,
"NO" => 15,
"DK" => 18,
"FI" => 18,
_ => 0, }
}
fn validate_iban_checksum(iban: &str) -> bool {
let rearranged = format!("{}{}", &iban[4..], &iban[..4]);
let mut numeric = String::new();
for c in rearranged.chars() {
if c.is_ascii_digit() {
numeric.push(c);
} else if c.is_ascii_alphabetic() {
let val = c.to_ascii_uppercase() as u32 - 'A' as u32 + 10;
numeric.push_str(&val.to_string());
}
}
let mut remainder: u64 = 0;
for chunk in numeric.as_bytes().chunks(9) {
let chunk_str: String = std::str::from_utf8(chunk).unwrap_or("0").to_string();
let combined = format!("{}{}", remainder, chunk_str);
remainder = combined.parse::<u64>().unwrap_or(0) % 97;
}
remainder == 1
}
pub fn validate_us_ssn(value: &str) -> f32 {
let digits: String = value.chars().filter(|c| c.is_ascii_digit()).collect();
if digits.len() != 9 {
return 0.0;
}
let area: u32 = digits[0..3].parse().unwrap_or(0);
let group: u32 = digits[3..5].parse().unwrap_or(0);
let serial: u32 = digits[5..9].parse().unwrap_or(0);
if area == 0 || area == 666 || area >= 900 {
return 0.0;
}
if group == 0 || serial == 0 {
return 0.0;
}
1.0
}
pub fn validate_uk_nino(value: &str) -> f32 {
let cleaned: String = value
.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>()
.to_uppercase();
if cleaned.len() != 9 {
return 0.0;
}
let prefix: String = cleaned.chars().take(2).collect();
let suffix = cleaned.chars().last().unwrap_or('X');
let invalid_prefixes = ["BG", "GB", "NK", "KN", "TN", "NT", "ZZ"];
if invalid_prefixes.contains(&prefix.as_str()) {
return 0.0;
}
let first = prefix.chars().next().unwrap_or('X');
if "DFIQUV".contains(first) {
return 0.0;
}
let second = prefix.chars().nth(1).unwrap_or('X');
if "DFIOQUV".contains(second) {
return 0.0;
}
if !"ABCD".contains(suffix) {
return 0.0;
}
let middle: String = cleaned.chars().skip(2).take(6).collect();
if !middle.chars().all(|c| c.is_ascii_digit()) {
return 0.0;
}
1.0
}
pub fn validate_uk_nhs(value: &str) -> f32 {
let digits: Vec<u32> = value
.chars()
.filter(|c| c.is_ascii_digit())
.filter_map(|c| c.to_digit(10))
.collect();
if digits.len() != 10 {
return 0.0;
}
let weights = [10, 9, 8, 7, 6, 5, 4, 3, 2];
let sum: u32 = digits
.iter()
.take(9)
.zip(weights.iter())
.map(|(d, w)| d * w)
.sum();
let remainder = 11 - (sum % 11);
let check_digit = if remainder == 11 { 0 } else { remainder };
if check_digit == 10 {
return 0.0; }
if digits[9] == check_digit {
1.0
} else {
0.0
}
}
pub fn validate_isbn(value: &str) -> f32 {
let cleaned: String = value
.chars()
.filter(|c| c.is_ascii_digit() || *c == 'X' || *c == 'x')
.collect();
match cleaned.len() {
10 => validate_isbn10(&cleaned),
13 => validate_isbn13(&cleaned),
_ => 0.0,
}
}
fn validate_isbn10(isbn: &str) -> f32 {
let mut sum = 0;
for (i, c) in isbn.chars().enumerate() {
let digit = if c == 'X' || c == 'x' {
10
} else {
c.to_digit(10).unwrap_or(0)
};
sum += digit * (10 - i as u32);
}
if sum.is_multiple_of(11) {
1.0
} else {
0.0
}
}
fn validate_isbn13(isbn: &str) -> f32 {
let digits: Vec<u32> = isbn.chars().filter_map(|c| c.to_digit(10)).collect();
if digits.len() != 13 {
return 0.0;
}
let sum: u32 = digits
.iter()
.enumerate()
.map(|(i, &d)| if i % 2 == 0 { d } else { d * 3 })
.sum();
if sum.is_multiple_of(10) {
1.0
} else {
0.0
}
}
pub fn validate_ip_address(value: &str) -> f32 {
let octets: Vec<&str> = value.split('.').collect();
if octets.len() != 4 {
return 0.0;
}
for octet in octets {
match octet.parse::<u32>() {
Ok(n) if n <= 255 => continue,
_ => return 0.0,
}
}
1.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_luhn_valid_cards() {
assert_eq!(validate_credit_card("4532015112830366"), 1.0);
assert_eq!(validate_credit_card("5425233430109903"), 1.0);
assert_eq!(validate_credit_card("374245455400126"), 1.0);
assert_eq!(validate_credit_card("3782 822463 10005"), 1.0);
assert_eq!(validate_credit_card("4532-0151-1283-0366"), 1.0);
}
#[test]
fn test_luhn_invalid_cards() {
assert_eq!(validate_credit_card("4532015112830367"), 0.0);
assert_eq!(validate_credit_card("1234567890123456"), 0.0);
}
#[test]
fn test_valid_ssn() {
assert_eq!(validate_us_ssn("123-45-6789"), 1.0);
assert_eq!(validate_us_ssn("123456789"), 1.0);
}
#[test]
fn test_invalid_ssn() {
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); }
#[test]
fn test_valid_uk_nino() {
assert_eq!(validate_uk_nino("AB123456C"), 1.0);
assert_eq!(validate_uk_nino("JG103759A"), 1.0);
}
#[test]
fn test_invalid_uk_nino() {
assert_eq!(validate_uk_nino("BG123456A"), 0.0); assert_eq!(validate_uk_nino("DA123456A"), 0.0); assert_eq!(validate_uk_nino("AB123456E"), 0.0); }
#[test]
fn test_valid_iban() {
assert_eq!(validate_iban("GB82WEST12345698765432"), 1.0);
assert_eq!(validate_iban("DE89370400440532013000"), 1.0);
assert_eq!(validate_iban("GB82 WEST 1234 5698 7654 32"), 1.0);
assert_eq!(validate_iban("DE89 3704 0044 0532 0130 00"), 1.0);
}
#[test]
fn test_invalid_iban() {
assert_eq!(validate_iban("GB82WEST12345698765433"), 0.0); assert_eq!(validate_iban("XX00000000000000"), 0.0);
}
#[test]
fn test_valid_isbn() {
assert_eq!(validate_isbn("0-306-40615-2"), 1.0); assert_eq!(validate_isbn("978-0-306-40615-7"), 1.0); }
#[test]
fn test_valid_ip() {
assert_eq!(validate_ip_address("192.168.1.1"), 1.0);
assert_eq!(validate_ip_address("0.0.0.0"), 1.0);
assert_eq!(validate_ip_address("255.255.255.255"), 1.0);
}
#[test]
fn test_invalid_ip() {
assert_eq!(validate_ip_address("256.1.1.1"), 0.0);
assert_eq!(validate_ip_address("1.1.1"), 0.0);
}
#[test]
fn test_http_basic_auth_requires_user_password() {
let ok = format!("{}{}", "dXNlcm5hbWU6", "cGFzc3dvcmQ=");
assert_eq!(validate_http_basic_auth(&ok), 1.0);
assert_eq!(validate_http_basic_auth("not-base64"), 0.0);
assert_eq!(validate_http_basic_auth("QQ=="), 0.0);
}
#[test]
fn test_http_basic_auth_rejects_noncanonical_unused_bits() {
assert_eq!(validate_http_basic_auth("dTpwYXM="), 1.0);
assert_eq!(validate_http_basic_auth("dTpwYXN="), 0.0);
assert_eq!(
encode_base64(&decode_base64("dTpwYXM=").unwrap()),
"dTpwYXM="
);
}
}