use crate::countries::country_iban_length;
use crate::error::ValidationError;
use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
const MAX_INPUT_LENGTH: usize = 256;
pub fn validate(iban: &str) -> Result<(), ValidationError> {
validate_cow(iban).map(|_| ())
}
fn validate_cow(iban: &str) -> Result<Cow<'_, str>, ValidationError> {
if iban.len() > MAX_INPUT_LENGTH {
return Err(ValidationError::InvalidLength {
expected: MAX_INPUT_LENGTH,
found: iban.len(),
});
}
let needs_sanitization = iban
.chars()
.any(|c| c.is_whitespace() || c.is_ascii_lowercase());
let cow: Cow<'_, str> = if needs_sanitization {
let normalized: String = iban
.chars()
.filter(|c| !c.is_whitespace())
.map(|c| c.to_ascii_uppercase())
.collect();
Cow::Owned(normalized)
} else {
Cow::Borrowed(iban)
};
let s = cow.as_ref();
if s.is_empty() {
return Err(ValidationError::Empty);
}
if s.len() < 5 {
return Err(ValidationError::InvalidLength {
expected: 5,
found: s.len(),
});
}
let country_code = &s[0..2];
let expected_length = country_iban_length(country_code)
.ok_or(ValidationError::InvalidCountryCode)?;
if s.len() != expected_length {
return Err(ValidationError::InvalidLength {
expected: expected_length,
found: s.len(),
});
}
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if !b.is_ascii_alphanumeric() {
return Err(ValidationError::InvalidCharacter {
character: b as char,
position: i,
});
}
}
let mut remainder = 0u32;
for &b in bytes[4..].iter().chain(&bytes[0..4]) {
if b.is_ascii_digit() {
let digit = (b - b'0') as u32;
remainder = (remainder * 10 + digit) % 97;
} else {
let value = (b - b'A' + 10) as u32;
let tens = value / 10;
let ones = value % 10;
remainder = (remainder * 10 + tens) % 97;
remainder = (remainder * 10 + ones) % 97;
}
}
if remainder != 1 {
return Err(ValidationError::InvalidChecksum);
}
Ok(cow)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Iban(String);
impl Iban {
pub fn new(iban: &str) -> Result<Self, ValidationError> {
validate_cow(iban).map(|cow| Iban(cow.into_owned()))
}
pub fn country_code(&self) -> &str {
&self.0[0..2]
}
pub fn check_digits(&self) -> &str {
&self.0[2..4]
}
pub fn bban(&self) -> &str {
&self.0[4..]
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Iban {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for Iban {
type Err = ValidationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_ibans() {
assert!(validate("DE89370400440532013000").is_ok()); assert!(validate("GB82WEST12345698765432").is_ok()); assert!(validate("FR1420041010050500013M02606").is_ok()); assert!(validate("AL35202111090000000001234567").is_ok()); assert!(validate("NO9386011117947").is_ok()); }
#[test]
fn test_invalid_checksum() {
assert_eq!(
validate("DE89370400440532013001"),
Err(ValidationError::InvalidChecksum)
);
assert_eq!(
validate("GB82WEST12345698765433"),
Err(ValidationError::InvalidChecksum)
);
}
#[test]
fn test_wrong_length() {
assert_eq!(
validate("DE8937040044053201300"),
Err(ValidationError::InvalidLength {
expected: 22,
found: 21,
})
);
assert_eq!(
validate("GB82WEST1234569876543"),
Err(ValidationError::InvalidLength {
expected: 22,
found: 21,
})
);
assert_eq!(
validate("FR1420041010050500013M0260"),
Err(ValidationError::InvalidLength {
expected: 27,
found: 26,
})
);
}
#[test]
fn test_unknown_country() {
assert_eq!(
validate("XX89370400440532013000"),
Err(ValidationError::InvalidCountryCode)
);
assert_eq!(
validate("ZZ1420041010050500013M02606"),
Err(ValidationError::InvalidCountryCode)
);
}
#[test]
fn test_with_whitespace() {
assert!(validate("DE89 3704 0044 0532 0130 00").is_ok());
}
#[test]
fn test_invalid_characters() {
assert_eq!(
validate("DE89.37040044053201300"),
Err(ValidationError::InvalidCharacter {
character: '.',
position: 4,
})
);
}
#[test]
fn test_invalid_characters_later() {
assert_eq!(
validate("GB82WEST1234569876543!"),
Err(ValidationError::InvalidCharacter {
character: '!',
position: 21,
})
);
}
#[test]
fn test_lowercase() {
assert!(validate("de89370400440532013000").is_ok());
}
#[test]
fn test_mixed_case() {
assert!(validate("De89370400440532013000").is_ok());
assert!(validate("gB82WEST12345698765432").is_ok());
}
#[test]
fn test_leading_whitespace() {
assert!(validate(" DE89370400440532013000").is_ok());
}
#[test]
fn test_trailing_whitespace() {
assert!(validate("DE89370400440532013000\n").is_ok());
}
#[test]
fn test_tabs_and_newlines() {
assert!(validate("DE89\t3704\n0044 0532\t0130\n00").is_ok());
}
#[test]
fn test_empty() {
assert_eq!(validate(""), Err(ValidationError::Empty));
}
#[test]
fn test_whitespace_only() {
assert_eq!(validate(" "), Err(ValidationError::Empty));
}
#[test]
fn test_tabs_only() {
assert_eq!(validate("\t\n"), Err(ValidationError::Empty));
}
#[test]
fn test_single_char() {
assert_eq!(
validate("D"),
Err(ValidationError::InvalidLength {
expected: 5,
found: 1,
})
);
}
#[test]
fn test_three_chars() {
assert_eq!(
validate("DE8"),
Err(ValidationError::InvalidLength {
expected: 5,
found: 3,
})
);
}
#[test]
fn test_country_code_only() {
assert_eq!(
validate("DE"),
Err(ValidationError::InvalidLength {
expected: 5,
found: 2,
})
);
}
#[test]
fn test_four_chars() {
assert_eq!(
validate("DE89"),
Err(ValidationError::InvalidLength {
expected: 5,
found: 4,
})
);
}
#[test]
fn test_iban_new_valid() {
let iban = Iban::new("DE89370400440532013000").unwrap();
assert_eq!(iban.as_str(), "DE89370400440532013000");
assert_eq!(iban.country_code(), "DE");
assert_eq!(iban.check_digits(), "89");
assert_eq!(iban.bban(), "370400440532013000");
}
#[test]
fn test_iban_new_invalid() {
assert!(Iban::new("DE89").is_err());
assert!(Iban::new("XX89370400440532013000").is_err());
assert!(Iban::new("DE89370400440532013001").is_err());
}
#[test]
fn test_iban_from_str() {
let iban: Iban = "DE89370400440532013000".parse().unwrap();
assert_eq!(iban.as_str(), "DE89370400440532013000");
}
#[test]
fn test_iban_display() {
let iban = Iban::new("DE89370400440532013000").unwrap();
assert_eq!(format!("{}", iban), "DE89370400440532013000");
}
#[test]
fn test_iban_equality() {
let iban1 = Iban::new("DE89370400440532013000").unwrap();
let iban2 = Iban::new("de89370400440532013000").unwrap();
assert_eq!(iban1, iban2);
}
#[test]
fn test_input_too_long() {
let long_input = "A".repeat(257);
assert_eq!(
validate(&long_input),
Err(ValidationError::InvalidLength {
expected: MAX_INPUT_LENGTH,
found: 257,
})
);
}
#[test]
fn test_input_at_max_length_with_whitespace() {
let iban = "DE89370400440532013000";
let with_ws = format!("{} {}", iban, " ".repeat(200));
assert!(validate(&with_ws).is_ok());
}
#[test]
fn test_clone_validation_error() {
let err = ValidationError::InvalidChecksum;
let cloned = err.clone();
assert_eq!(err, cloned);
}
#[test]
fn test_iban_clone() {
let iban1 = Iban::new("DE89370400440532013000").unwrap();
let iban2 = iban1.clone();
assert_eq!(iban1, iban2);
}
#[test]
fn test_all_countries_basic() {
assert!(validate("DE89370400440532013000").is_ok());
assert!(validate("GB82WEST12345698765432").is_ok());
assert!(validate("FR1420041010050500013M02606").is_ok());
assert!(validate("CH9300762011623852957").is_ok());
assert!(validate("NL91ABNA0417164300").is_ok());
assert!(validate("BE71096123456769").is_ok());
}
}