1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! [![ci-badge][]][ci] [![docs-badge][]][docs] [![crate-version]][crate-link]
//!
//! # schwifty
//!
//! A simple IBAN validation library inspired by Python's `schwifty`.
//!
//! ## Sample Usage
//! ```rust
//!assert!(schwifty::validate("GB82 WEST 1234 5698 7654 32").is_ok());
//! ```
//!
//! [ci]: https://github.com/Elinvynia/schwifty/actions?query=workflow%3ARust
//! [ci-badge]: https://img.shields.io/github/workflow/status/Elinvynia/schwifty/Rust/master?style=flat-square
//! [docs]: https://docs.rs/schwifty
//! [docs-badge]: https://img.shields.io/badge/docs-online-5023dd.svg?style=flat-square
//! [crate-link]: https://crates.io/crates/schwifty
//! [crate-version]: https://img.shields.io/crates/v/schwifty.svg?style=flat-square

#![forbid(unsafe_code)]
#![warn(missing_debug_implementations)]
#![warn(missing_docs)]

pub use crate::country::Country;
pub use crate::error::ValidationError;
use std::str::FromStr;

pub mod country;
pub(crate) mod country_specific;
pub mod error;

#[allow(clippy::all)]
pub(crate) mod u256 {
    uint::construct_uint! {
        pub(crate) struct U256(4);
    }
}

/// Represents an IBAN and provides helpful methods.
#[derive(Debug)]
#[non_exhaustive]
pub struct Iban {
    /// The country of this IBAN.
    pub country: Country,
    pub(crate) raw: String,
}

impl Iban {
    /// Returns the account number of the IBAN.
    pub fn account_number(&self) -> String {
        self.country.account_number(&self.raw)
    }

    /// Returns the national bank code of the IBAN.
    pub fn bank_code(&self) -> String {
        self.country.bank_code(&self.raw)
    }

    /// Returns the country code as a String, for example "GB".
    pub fn country_code(&self) -> String {
        self.country.to_string()
    }

    /// Access the raw String this IBAN was made from.
    pub fn raw(&self) -> &str {
        &self.raw
    }
}

/// Checks if the provided string is a valid IBAN, or tells you why it isn't.
pub fn validate<I: AsRef<str>>(input: I) -> Result<Iban, ValidationError> {
    let input = input.as_ref();

    // Remove the whitespace.
    let input: String = input.split_whitespace().collect();

    // IBAN can be at most 34 characters(bytes) long.
    if input.len() > 34 {
        return Err(ValidationError::TooLong);
    };

    // All of the characters must be alphanumeric.
    if !input.chars().all(|ch| ch.is_alphanumeric()) {
        return Err(ValidationError::InvalidChar);
    };

    // IBAN must have at least 2 characters to match a country code.
    if input.len() < 2 {
        return Err(ValidationError::InvalidCountryCode);
    };

    // See if it is a valid Country
    let country_code = &input[0..2];
    let country = match Country::from_str(country_code) {
        Ok(c) => c,
        Err(_) => return Err(ValidationError::InvalidCountryCode),
    };

    // Since it is a valid country, check if it is the proper length.
    if input.len() != country.length() {
        return Err(ValidationError::InvalidLength);
    }

    // Also check if the format matches
    if !country.format().is_match(&input) {
        return Err(ValidationError::InvalidFormat);
    }

    // Do country-specifich checks.
    if !country.custom_validation(&input) {
        return Err(ValidationError::CountryCheckFailed);
    }

    // Put the country code to the end of the string.
    let (start, rest) = input.split_at(4);
    let mut rearranged = String::with_capacity(34);
    rearranged.push_str(rest);
    rearranged.push_str(start);

    // Convert ASCII letters to their code, don't modify numbers.
    let mut integer_string = String::with_capacity(34);
    for ch in rearranged.chars() {
        if ch.is_numeric() {
            integer_string.push(ch);
        } else {
            // This will not panic as we are guaranteed A-Z, a-z
            let x = ch.to_digit(36).unwrap().to_string();
            integer_string.push_str(&x)
        }
    }

    // This will not panic as u256 can hold any IBAN.
    let integer = u256::U256::from_dec_str(&integer_string).unwrap();

    // Make sure that the remainder is one.
    if integer % 97 != 1.into() {
        return Err(ValidationError::InvalidIban);
    }

    Ok(Iban {
        country,
        raw: input,
    })
}