use super::funejson::Value; use super::validation_iban_data::IBAN_LENGTHS; /// ISO 13616 allows 34 characters at most; Norway's 15 is the shortest issued. const MAX_IBAN: usize = 34; const MIN_IBAN: usize = 15; fn is_digit(ch: char) -> bool { ('0'..='9').contains(&ch) } fn is_upper_letter(ch: char) -> bool { ('A'..='Z').contains(&ch) } /// Strip spaces and fold to upper case, ASCII only, so that this agrees with /// the TypeScript and Python siblings whose built-in case folding is /// Unicode-aware. Nothing in an IBAN is non-ASCII. fn compact(value: &str) -> Vec { value .chars() // Only the ASCII space is stripped. IBANs are printed in groups of four // and pasted that way; hyphens and other punctuation are not a printing // convention, they are a sign the value came from somewhere unexpected. .filter(|ch| *ch != ' ') .map(|ch| ch.to_ascii_uppercase()) .collect() } /// The registered IBAN length for a country, or -1 if the country has none. pub fn iban_length(country: &str) -> i64 { let code: String = compact(country).into_iter().collect(); for row in IBAN_LENGTHS { if row.country == code { return row.length; } } -1 } /// Is this a structurally valid IBAN? /// /// Two checks, both necessary. The ISO 13616 mod-97-10 checksum catches /// mistyped and transposed characters, but on its own it would accept a /// correctly-checksummed string of any length; the country's registered length /// is what catches a truncated or padded account number that still happens to /// check out. /// /// Neither check proves the account exists. Only the bank can say that, and /// only a payment (or a confirmation-of-payee service) proves it belongs to /// the person you think it does. pub fn is_iban(value: &str) -> bool { let iban = compact(value); if iban.len() < MIN_IBAN || iban.len() > MAX_IBAN { return false; } // Positions 1-2 are the country, 3-4 the check digits. Testing this before // the table lookup means a lower-case or punctuated value fails here rather // than being reported as an unknown country. if !is_upper_letter(iban[0]) || !is_upper_letter(iban[1]) { return false; } if !is_digit(iban[2]) || !is_digit(iban[3]) { return false; } let country: String = iban[0..2].iter().collect(); let expected = iban_length(&country); if expected < 0 || iban.len() as i64 != expected { return false; } for ch in &iban[4..] { if !is_digit(*ch) && !is_upper_letter(*ch) { return false; } } mod97(&iban) == 1 } /// ISO 13616 mod-97-10: move the first four characters to the end, replace /// each letter with its position in the alphabet plus 9 (A=10 ... Z=35), and /// take the whole thing modulo 97. /// /// The expansion of a 34-character IBAN is up to 68 digits, which no 64-bit /// integer can hold, so the remainder is carried forward one character at a /// time. A letter contributes two digits, so it multiplies the running /// remainder by 100; the largest intermediate is 96 * 100 + 35 = 9635, nowhere /// near an overflow. fn mod97(iban: &[char]) -> i64 { let mut remainder: i64 = 0; let n = iban.len(); for i in 0..n { // Rotation without building a second string: read from position 4 // onwards, then wrap round to the first four characters. let ch = iban[(i + 4) % n]; if is_digit(ch) { remainder = (remainder * 10 + (ch as i64 - 48)) % 97; } else { remainder = (remainder * 100 + (ch as i64 - 55)) % 97; } } remainder } /// The IBAN in its printed form, groups of four separated by single spaces, /// or `None` if it does not validate. pub fn format_iban(value: &str) -> Option { if !is_iban(value) { return None; } let iban = compact(value); let groups: Vec = iban.chunks(4).map(|c| c.iter().collect()).collect(); Some(groups.join(" ")) } pub fn fune_vector(args: &[Value]) -> Value { // A non-string argument arrives here as an empty string, which is exactly // the answer TypeScript and Python give for a non-string: not an IBAN. Value::Bool(is_iban(args[0].as_str())) }