taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 44d4a83cfc1f306cbdb6d15d3e61913fd65885da
parent d59b23ae7725271a93afe3aba3bb0bff24512002
Author: Antoine A <>
Date:   Tue, 18 Feb 2025 11:26:46 +0100

common: IBAN country & pattern check

Diffstat:
Mcommon/taler-api/tests/common/db.rs | 2+-
Mcommon/taler-common/Cargo.toml | 5+++++
Acommon/taler-common/benches/iban.rs | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/taler-common/src/error_code.rs | 3+--
Mcommon/taler-common/src/types/iban.rs | 167++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Acommon/taler-common/src/types/iban/registry.rs | 1704+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtaler-magnet-bank/src/lib.rs | 12++++++------
7 files changed, 1880 insertions(+), 79 deletions(-)

diff --git a/common/taler-api/tests/common/db.rs b/common/taler-api/tests/common/db.rs @@ -46,7 +46,7 @@ pub async fn notification_listener( pub enum TransferResult { Success(TransferResponse), RequestUidReuse, - WtidReuse + WtidReuse, } pub async fn transfer(db: &PgPool, transfer: TransferRequest) -> sqlx::Result<TransferResult> { diff --git a/common/taler-common/Cargo.toml b/common/taler-common/Cargo.toml @@ -33,3 +33,7 @@ criterion.workspace = true [[bench]] name = "base32" harness = false + +[[bench]] +name = "iban" +harness = false +\ No newline at end of file diff --git a/common/taler-common/benches/iban.rs b/common/taler-common/benches/iban.rs @@ -0,0 +1,66 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use std::str::FromStr; + +use criterion::{criterion_group, criterion_main, BatchSize, Criterion}; +use taler_common::types::iban::{ + Country::{self, *}, + IBAN, +}; + +const COUNTRIES: [Country; 89] = [ + AD, AE, AL, AT, AZ, BA, BE, BG, BH, BI, BR, BY, CH, CR, CY, CZ, DE, DJ, DK, DO, EE, EG, ES, FI, + FK, FO, FR, GB, GE, GI, GL, GR, GT, HN, HR, HU, IE, IL, IQ, IS, IT, JO, KW, KZ, LB, LC, LI, LT, + LU, LV, LY, MC, MD, ME, MK, MN, MR, MT, MU, NI, NL, NO, OM, PK, PL, PS, PT, QA, RO, RS, RU, SA, + SC, SD, SE, SI, SK, SM, SO, ST, SV, TL, TN, TR, UA, VA, VG, XK, YE, +]; + +fn parser(c: &mut Criterion) { + c.bench_function("iban_random_all", |b| { + b.iter_batched( + || { + (0..fastrand::usize(0..40)) + .map(|_| fastrand::char(..)) + .collect::<String>() + }, + |case| IBAN::from_str(&case), + BatchSize::SmallInput, + ) + }); + c.bench_function("iban_random_alphanumeric", |b| { + b.iter_batched( + || { + (0..fastrand::usize(0..40)) + .map(|_| fastrand::alphanumeric()) + .collect::<String>() + }, + |case| IBAN::from_str(&case), + BatchSize::SmallInput, + ) + }); + + c.bench_function("iban_valid", |b| { + b.iter_batched( + || IBAN::random(fastrand::choice(COUNTRIES).unwrap()).to_string(), + |case| IBAN::from_str(&case).unwrap(), + BatchSize::SmallInput, + ) + }); +} + +criterion_group!(benches, parser); +criterion_main!(benches); diff --git a/common/taler-common/src/error_code.rs b/common/taler-common/src/error_code.rs @@ -2002,4 +2002,4 @@ impl ErrorCode { END => (0, "End of error code range."), } } -} -\ No newline at end of file +} diff --git a/common/taler-common/src/types/iban.rs b/common/taler-common/src/types/iban.rs @@ -20,8 +20,13 @@ use std::{ str::FromStr, }; +pub use registry::Country; +use registry::{rng_pattern, IbanC, PatternErr}; + use super::utils::InlineStr; +mod registry; + const MAX_IBAN_SIZE: usize = 34; const MAX_BIC_SIZE: usize = 11; @@ -39,73 +44,80 @@ pub fn bic(bic: impl AsRef<str>) -> BIC { Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, )] /// International Bank Account Number (IBAN) -pub struct IBAN(InlineStr<MAX_IBAN_SIZE>); +pub struct IBAN { + country: Country, + encoded: InlineStr<MAX_IBAN_SIZE>, +} impl IBAN { /// Compute IBAN checksum fn iban_checksum(s: &[u8]) -> u8 { - (s.iter() - .cycle() - .skip(4) - .take(s.len()) - .fold(0u32, |mut checksum, b| { - if b.is_ascii_digit() { - checksum = checksum * 10 + (b - b'0') as u32; - } else { - checksum = checksum * 100 + (b - b'A' + 10) as u32; - } - if checksum > 9_999_999 { - checksum %= 97 - } - checksum - }) - % 97) as u8 + (s.iter().cycle().skip(4).take(s.len()).fold(0u32, |sum, b| { + if b.is_ascii_digit() { + (sum * 10 + (b - b'0') as u32) % 97 + } else { + (sum * 100 + (b - b'A' + 10) as u32) % 97 + } + })) as u8 } - pub fn from_parts(country: &str, bban: &str) -> Self { - assert_eq!(country.len(), 2); + fn from_raw_parts(country: Country, bban: &[u8]) -> Self { // Create a iban with an empty digit check - let mut iban = InlineStr::try_from_iter( + let mut encoded = InlineStr::try_from_iter( country .as_bytes() - .iter() - .copied() - .chain("00".as_bytes().iter().copied()) - .chain(bban.as_bytes().iter().copied()), + .into_iter() + .chain([b'0', b'0']) + .chain(bban.iter().copied()), ) .unwrap(); // Compute check digit - let check_digit = 98 - Self::iban_checksum(iban.deref()); + let checksum = 98 - Self::iban_checksum(encoded.deref()); // And insert it unsafe { // SAFETY: we only insert ASCII digits - let buf = iban.deref_mut(); - buf[3] = check_digit % 10 + b'0'; - buf[2] = check_digit / 10 + b'0'; + let buf = encoded.deref_mut(); + buf[3] = checksum % 10 + b'0'; + buf[2] = checksum / 10 + b'0'; } - Self(iban) + + Self { country, encoded } } - pub fn country_code(&self) -> &str { - // SAFETY len >= 4 - unsafe { self.as_ref().get_unchecked(0..2) } + pub fn from_parts(country: Country, bban: &str) -> Self { + assert_eq!(bban.len(), country.bban_len()); // TODO return Result ? + country.bban_check(bban.as_bytes()).unwrap(); // TODO return Result + Self::from_raw_parts(country, bban.as_bytes()) + } + + pub fn random(country: Country) -> Self { + let mut bban = [0u8; MAX_IBAN_SIZE - 4]; + rng_pattern(&mut bban, country.bban_pattern()); + Self::from_raw_parts(country, &bban[..country.bban_len()]) } - pub fn check_digit(&self) -> &str { - // SAFETY len >= 4 - unsafe { self.as_ref().get_unchecked(2..4) } + pub fn country(&self) -> Country { + self.country } pub fn bban(&self) -> &str { // SAFETY len >= 5 unsafe { self.as_ref().get_unchecked(4..) } } + + pub fn bank_id(&self) -> &str { + &self.bban()[self.country.bank_id()] + } + + pub fn branch_id(&self) -> &str { + &self.bban()[self.country.branch_id()] + } } impl AsRef<str> for IBAN { fn as_ref(&self) -> &str { - self.0.as_ref() + self.encoded.as_ref() } } @@ -113,12 +125,16 @@ impl AsRef<str> for IBAN { pub enum IbanErrorKind { #[error("contains illegal characters (only 0-9A-Z allowed)")] Invalid, - #[error("contains invalid country code")] - CountryCode, - #[error("contains invalid check digit")] - CheckDigit, + #[error("contains invalid characters")] + Malformed, + #[error("unknown country {0}")] + UnknownCountry(String), #[error("too long expected max {MAX_IBAN_SIZE} chars got {0}")] - Big(usize), + Overflow(usize), + #[error("too short expected min 4 chars got {0}")] + Underflow(usize), + #[error("wrong size expected {0} chars got {1}")] + Size(u8, usize), #[error("checksum expected 1 got {0}")] Checksum(u8), } @@ -140,25 +156,36 @@ impl FromStr for IBAN { .all(|b| b.is_ascii_whitespace() || b.is_ascii_alphanumeric()) { Err(IbanErrorKind::Invalid) - } else if let Some(inlined) = InlineStr::try_from_iter( + } else if let Some(encoded) = InlineStr::try_from_iter( bytes .iter() .filter_map(|b| (!b.is_ascii_whitespace()).then_some(b.to_ascii_uppercase())), ) { - if inlined.len() < 2 || !inlined[0..2].iter().all(u8::is_ascii_uppercase) { - Err(IbanErrorKind::CountryCode) - } else if inlined.len() < 4 || !inlined[2..4].iter().all(u8::is_ascii_digit) { - Err(IbanErrorKind::CheckDigit) - } else { - let checksum = Self::iban_checksum(&inlined); - if checksum != 1 { - Err(IbanErrorKind::Checksum(checksum)) + if encoded.len() < 4 { + Err(IbanErrorKind::Underflow(encoded.len())) + } else if !IbanC::A.check(&encoded[0..2]) || !IbanC::N.check(&encoded[2..4]) { + Err(IbanErrorKind::Malformed) + } else if let Some(country) = Country::from_str(&encoded.as_ref()[..2]) { + if let Err(e) = country.bban_check(&encoded[4..]) { + Err(match e { + PatternErr::Len(expected, got) => IbanErrorKind::Size(expected, got), + PatternErr::Malformed => IbanErrorKind::Malformed, + }) } else { - Ok(Self(inlined)) + let checksum = Self::iban_checksum(&encoded); + if checksum != 1 { + Err(IbanErrorKind::Checksum(checksum)) + } else { + Ok(Self { country, encoded }) + } } + } else { + Err(IbanErrorKind::UnknownCountry( + encoded.as_ref()[..2].to_owned(), + )) } } else { - Err(IbanErrorKind::Big(bytes.len())) + Err(IbanErrorKind::Overflow(bytes.len())) } .map_err(|kind| ParseIbanError { iban: s.to_owned(), @@ -262,32 +289,32 @@ impl Display for BIC { #[test] fn parse_iban() { - for valid in [ - "DE44500105175407324931", // Germany - "GB82WEST12345698765432", // United Kingdom - "FR1420041010050500013M02606", // France - "ES9121000418450200051332", // Spain - "NL91ABNA0417164300", // Netherlands - "IT60X0542811101000000123456", // Italy - "BE68539007547034", // Belgium - "SE4550000000058398257466", // Sweden - "PL61109010140000071219812874", // Poland - "NO9386011117947", // Norway - ] { + use registry::VALID_IBAN; + for (valid, bban) in VALID_IBAN { // Parsing - let iban = IBAN::from_str(&valid).unwrap(); + let iban = IBAN::from_str(valid).unwrap(); assert_eq!(iban.to_string(), valid); // Roundtrip - let from_parts = IBAN::from_parts(iban.country_code(), iban.bban()); + let from_parts = IBAN::from_parts(iban.country(), iban.bban()); assert_eq!(from_parts.to_string(), valid); + + // BBAN + if let Some(bban) = bban { + assert_eq!(bban, iban.bban()); + } + + // Random + let rand = IBAN::random(iban.country()); + let parsed = IBAN::from_str(&rand.to_string()).unwrap(); + assert_eq!(rand, parsed); } for (invalid, err) in [ ("FR1420041@10050500013M02606", IbanErrorKind::Invalid), - ("", IbanErrorKind::CountryCode), - ("12345678901234567890123456", IbanErrorKind::CountryCode), - ("FR", IbanErrorKind::CheckDigit), - ("FRANCE123456", IbanErrorKind::CheckDigit), + ("", IbanErrorKind::Underflow(0)), + ("12345678901234567890123456", IbanErrorKind::Malformed), + ("FR", IbanErrorKind::Underflow(2)), + ("FRANCE123456", IbanErrorKind::Malformed), ("DE44500105175407324932", IbanErrorKind::Checksum(28)), ] { let iban = IBAN::from_str(&invalid).unwrap_err(); diff --git a/common/taler-common/src/types/iban/registry.rs b/common/taler-common/src/types/iban/registry.rs @@ -0,0 +1,1704 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use std::fmt::Display; + +use Country::*; +use IbanC::*; + +/// IBAN ASCII characters rules +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IbanC { + /// Digits (0-9) + N, + /// Uppercase (A-Z) + A, + /// Digits or uppercase (0-9 & A-Z) + C, +} + +impl IbanC { + /// Check if a valid IBAN slice follow a specific characters rules + pub fn check(self, iban_ascii: &[u8]) -> bool { + // IBAN are made of ASCII digits and uppercase + debug_assert!(iban_ascii + .iter() + .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())); + // As all characters are ASCII digits or uppercase + // we can use simple masks to check the character kind + const MASK_IS_UPPERCASE: u8 = 0b0100_0000; + const MASK_IS_DIGIT: u8 = 0b0010_0000; + + let mask = match self { + Self::N => MASK_IS_UPPERCASE, + Self::A => MASK_IS_DIGIT, + Self::C => return true, + }; + iban_ascii.iter().all(|b| (*b & mask) == 0) + } +} + +/// An IBAN pattern, an array of characters rules over a number of character +pub type Pattern = &'static [(u8, IbanC)]; + +#[derive(Debug)] +pub enum PatternErr { + Len(u8, usize), + Malformed, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Country { + AD, + AE, + AL, + AT, + AZ, + BA, + BE, + BG, + BH, + BI, + BR, + BY, + CH, + CR, + CY, + CZ, + DE, + DJ, + DK, + DO, + EE, + EG, + ES, + FI, + FK, + FO, + FR, + GB, + GE, + GI, + GL, + GR, + GT, + HN, + HR, + HU, + IE, + IL, + IQ, + IS, + IT, + JO, + KW, + KZ, + LB, + LC, + LI, + LT, + LU, + LV, + LY, + MC, + MD, + ME, + MK, + MN, + MR, + MT, + MU, + NI, + NL, + NO, + OM, + PK, + PL, + PS, + PT, + QA, + RO, + RS, + RU, + SA, + SC, + SD, + SE, + SI, + SK, + SM, + SO, + ST, + SV, + TL, + TN, + TR, + UA, + VA, + VG, + XK, + YE, +} + +impl Country { + pub fn from_str(country: &str) -> Option<Self> { + match country { + "AD" => Some(AD), + "AE" => Some(AE), + "AL" => Some(AL), + "AT" => Some(AT), + "AZ" => Some(AZ), + "BA" => Some(BA), + "BE" => Some(BE), + "BG" => Some(BG), + "BH" => Some(BH), + "BI" => Some(BI), + "BR" => Some(BR), + "BY" => Some(BY), + "CH" => Some(CH), + "CR" => Some(CR), + "CY" => Some(CY), + "CZ" => Some(CZ), + "DE" => Some(DE), + "DJ" => Some(DJ), + "DK" => Some(DK), + "DO" => Some(DO), + "EE" => Some(EE), + "EG" => Some(EG), + "ES" => Some(ES), + "FI" => Some(FI), + "FK" => Some(FK), + "FO" => Some(FO), + "FR" => Some(FR), + "GB" => Some(GB), + "GE" => Some(GE), + "GI" => Some(GI), + "GL" => Some(GL), + "GR" => Some(GR), + "GT" => Some(GT), + "HN" => Some(HN), + "HR" => Some(HR), + "HU" => Some(HU), + "IE" => Some(IE), + "IL" => Some(IL), + "IQ" => Some(IQ), + "IS" => Some(IS), + "IT" => Some(IT), + "JO" => Some(JO), + "KW" => Some(KW), + "KZ" => Some(KZ), + "LB" => Some(LB), + "LC" => Some(LC), + "LI" => Some(LI), + "LT" => Some(LT), + "LU" => Some(LU), + "LV" => Some(LV), + "LY" => Some(LY), + "MC" => Some(MC), + "MD" => Some(MD), + "ME" => Some(ME), + "MK" => Some(MK), + "MN" => Some(MN), + "MR" => Some(MR), + "MT" => Some(MT), + "MU" => Some(MU), + "NI" => Some(NI), + "NL" => Some(NL), + "NO" => Some(NO), + "OM" => Some(OM), + "PK" => Some(PK), + "PL" => Some(PL), + "PS" => Some(PS), + "PT" => Some(PT), + "QA" => Some(QA), + "RO" => Some(RO), + "RS" => Some(RS), + "RU" => Some(RU), + "SA" => Some(SA), + "SC" => Some(SC), + "SD" => Some(SD), + "SE" => Some(SE), + "SI" => Some(SI), + "SK" => Some(SK), + "SM" => Some(SM), + "SO" => Some(SO), + "ST" => Some(ST), + "SV" => Some(SV), + "TL" => Some(TL), + "TN" => Some(TN), + "TR" => Some(TR), + "UA" => Some(UA), + "VA" => Some(VA), + "VG" => Some(VG), + "XK" => Some(XK), + "YE" => Some(YE), + _ => None, + } + } + + pub const fn as_str(self) -> &'static str { + match self { + AD => "AD", + AE => "AE", + AL => "AL", + AT => "AT", + AZ => "AZ", + BA => "BA", + BE => "BE", + BG => "BG", + BH => "BH", + BI => "BI", + BR => "BR", + BY => "BY", + CH => "CH", + CR => "CR", + CY => "CY", + CZ => "CZ", + DE => "DE", + DJ => "DJ", + DK => "DK", + DO => "DO", + EE => "EE", + EG => "EG", + ES => "ES", + FI => "FI", + FK => "FK", + FO => "FO", + FR => "FR", + GB => "GB", + GE => "GE", + GI => "GI", + GL => "GL", + GR => "GR", + GT => "GT", + HN => "HN", + HR => "HR", + HU => "HU", + IE => "IE", + IL => "IL", + IQ => "IQ", + IS => "IS", + IT => "IT", + JO => "JO", + KW => "KW", + KZ => "KZ", + LB => "LB", + LC => "LC", + LI => "LI", + LT => "LT", + LU => "LU", + LV => "LV", + LY => "LY", + MC => "MC", + MD => "MD", + ME => "ME", + MK => "MK", + MN => "MN", + MR => "MR", + MT => "MT", + MU => "MU", + NI => "NI", + NL => "NL", + NO => "NO", + OM => "OM", + PK => "PK", + PL => "PL", + PS => "PS", + PT => "PT", + QA => "QA", + RO => "RO", + RS => "RS", + RU => "RU", + SA => "SA", + SC => "SC", + SD => "SD", + SE => "SE", + SI => "SI", + SK => "SK", + SM => "SM", + SO => "SO", + ST => "ST", + SV => "SV", + TL => "TL", + TN => "TN", + TR => "TR", + UA => "UA", + VA => "VA", + VG => "VG", + XK => "XK", + YE => "YE", + } + } + + pub const fn as_bytes(self) -> [u8; 2] { + match self { + AD => [b'A', b'D'], + AE => [b'A', b'E'], + AL => [b'A', b'L'], + AT => [b'A', b'T'], + AZ => [b'A', b'Z'], + BA => [b'B', b'A'], + BE => [b'B', b'E'], + BG => [b'B', b'G'], + BH => [b'B', b'H'], + BI => [b'B', b'I'], + BR => [b'B', b'R'], + BY => [b'B', b'Y'], + CH => [b'C', b'H'], + CR => [b'C', b'R'], + CY => [b'C', b'Y'], + CZ => [b'C', b'Z'], + DE => [b'D', b'E'], + DJ => [b'D', b'J'], + DK => [b'D', b'K'], + DO => [b'D', b'O'], + EE => [b'E', b'E'], + EG => [b'E', b'G'], + ES => [b'E', b'S'], + FI => [b'F', b'I'], + FK => [b'F', b'K'], + FO => [b'F', b'O'], + FR => [b'F', b'R'], + GB => [b'G', b'B'], + GE => [b'G', b'E'], + GI => [b'G', b'I'], + GL => [b'G', b'L'], + GR => [b'G', b'R'], + GT => [b'G', b'T'], + HN => [b'H', b'N'], + HR => [b'H', b'R'], + HU => [b'H', b'U'], + IE => [b'I', b'E'], + IL => [b'I', b'L'], + IQ => [b'I', b'Q'], + IS => [b'I', b'S'], + IT => [b'I', b'T'], + JO => [b'J', b'O'], + KW => [b'K', b'W'], + KZ => [b'K', b'Z'], + LB => [b'L', b'B'], + LC => [b'L', b'C'], + LI => [b'L', b'I'], + LT => [b'L', b'T'], + LU => [b'L', b'U'], + LV => [b'L', b'V'], + LY => [b'L', b'Y'], + MC => [b'M', b'C'], + MD => [b'M', b'D'], + ME => [b'M', b'E'], + MK => [b'M', b'K'], + MN => [b'M', b'N'], + MR => [b'M', b'R'], + MT => [b'M', b'T'], + MU => [b'M', b'U'], + NI => [b'N', b'I'], + NL => [b'N', b'L'], + NO => [b'N', b'O'], + OM => [b'O', b'M'], + PK => [b'P', b'K'], + PL => [b'P', b'L'], + PS => [b'P', b'S'], + PT => [b'P', b'T'], + QA => [b'Q', b'A'], + RO => [b'R', b'O'], + RS => [b'R', b'S'], + RU => [b'R', b'U'], + SA => [b'S', b'A'], + SC => [b'S', b'C'], + SD => [b'S', b'D'], + SE => [b'S', b'E'], + SI => [b'S', b'I'], + SK => [b'S', b'K'], + SM => [b'S', b'M'], + SO => [b'S', b'O'], + ST => [b'S', b'T'], + SV => [b'S', b'V'], + TL => [b'T', b'L'], + TN => [b'T', b'N'], + TR => [b'T', b'R'], + UA => [b'U', b'A'], + VA => [b'V', b'A'], + VG => [b'V', b'G'], + XK => [b'X', b'K'], + YE => [b'Y', b'E'], + } + } + + pub const fn iban_len(self) -> usize { + match self { + AD => 24, + AE => 23, + AL => 28, + AT => 20, + AZ => 28, + BA => 20, + BE => 16, + BG => 22, + BH => 22, + BI => 27, + BR => 29, + BY => 28, + CH => 21, + CR => 22, + CY => 28, + CZ => 24, + DE => 22, + DJ => 27, + DK => 18, + DO => 28, + EE => 20, + EG => 29, + ES => 24, + FI => 18, + FK => 18, + FO => 18, + FR => 27, + GB => 22, + GE => 22, + GI => 23, + GL => 18, + GR => 27, + GT => 28, + HN => 28, + HR => 21, + HU => 28, + IE => 22, + IL => 23, + IQ => 23, + IS => 26, + IT => 27, + JO => 30, + KW => 30, + KZ => 20, + LB => 28, + LC => 32, + LI => 21, + LT => 20, + LU => 20, + LV => 21, + LY => 25, + MC => 27, + MD => 24, + ME => 22, + MK => 19, + MN => 20, + MR => 27, + MT => 31, + MU => 30, + NI => 28, + NL => 18, + NO => 15, + OM => 23, + PK => 24, + PL => 28, + PS => 29, + PT => 25, + QA => 29, + RO => 24, + RS => 22, + RU => 33, + SA => 24, + SC => 31, + SD => 18, + SE => 24, + SI => 19, + SK => 24, + SM => 27, + SO => 23, + ST => 25, + SV => 28, + TL => 23, + TN => 24, + TR => 26, + UA => 29, + VA => 22, + VG => 24, + XK => 20, + YE => 30, + } + } + + pub const fn bban_len(self) -> usize { + self.iban_len() - 4 + } + + pub const fn bank_id(self) -> core::ops::Range<usize> { + match self { + AD => 0..4, + AE => 0..3, + AL => 0..3, + AT => 0..5, + AZ => 0..4, + BA => 0..3, + BE => 0..3, + BG => 0..4, + BH => 0..4, + BI => 0..5, + BR => 0..8, + BY => 0..4, + CH => 0..5, + CR => 0..4, + CY => 0..3, + CZ => 0..4, + DE => 0..8, + DJ => 0..5, + DK => 0..4, + DO => 0..4, + EE => 0..2, + EG => 0..4, + ES => 0..4, + FI => 0..3, + FK => 0..2, + FO => 0..4, + FR => 0..5, + GB => 0..4, + GE => 0..2, + GI => 0..4, + GL => 0..4, + GR => 0..3, + GT => 0..4, + HN => 0..4, + HR => 0..7, + HU => 0..3, + IE => 0..4, + IL => 0..3, + IQ => 0..4, + IS => 0..2, + IT => 1..6, + JO => 4..8, + KW => 0..4, + KZ => 0..3, + LB => 0..4, + LC => 0..4, + LI => 0..5, + LT => 0..5, + LU => 0..3, + LV => 0..4, + LY => 0..3, + MC => 0..5, + MD => 0..2, + ME => 0..3, + MK => 0..3, + MN => 0..4, + MR => 0..5, + MT => 0..4, + MU => 0..6, + NI => 0..4, + NL => 0..4, + NO => 0..4, + OM => 0..3, + PK => 0..4, + PL => 0..0, + PS => 0..4, + PT => 0..4, + QA => 0..4, + RO => 0..4, + RS => 0..3, + RU => 0..9, + SA => 0..2, + SC => 0..6, + SD => 0..2, + SE => 0..3, + SI => 0..5, + SK => 0..4, + SM => 1..6, + SO => 0..4, + ST => 0..4, + SV => 0..4, + TL => 0..3, + TN => 0..2, + TR => 0..5, + UA => 0..6, + VA => 0..3, + VG => 0..4, + XK => 0..2, + YE => 0..4, + } + } + + pub const fn branch_id(self) -> core::ops::Range<usize> { + match self { + AD => 4..8, + AE => 0..0, + AL => 3..8, + AT => 0..0, + AZ => 0..0, + BA => 3..6, + BE => 0..0, + BG => 4..8, + BH => 0..0, + BI => 5..10, + BR => 8..13, + BY => 0..0, + CH => 0..0, + CR => 0..0, + CY => 3..8, + CZ => 0..0, + DE => 0..0, + DJ => 5..10, + DK => 0..0, + DO => 0..0, + EE => 0..0, + EG => 4..8, + ES => 4..8, + FI => 0..0, + FK => 0..0, + FO => 0..0, + FR => 0..0, + GB => 4..10, + GE => 0..0, + GI => 0..0, + GL => 0..0, + GR => 3..7, + GT => 0..0, + HN => 0..0, + HR => 0..0, + HU => 3..7, + IE => 4..10, + IL => 3..6, + IQ => 4..7, + IS => 2..4, + IT => 6..11, + JO => 4..8, + KW => 0..0, + KZ => 0..0, + LB => 0..0, + LC => 0..0, + LI => 0..0, + LT => 0..0, + LU => 0..0, + LV => 0..0, + LY => 3..6, + MC => 5..10, + MD => 0..0, + ME => 0..0, + MK => 0..0, + MN => 0..0, + MR => 5..10, + MT => 4..9, + MU => 6..8, + NI => 0..0, + NL => 0..0, + NO => 0..0, + OM => 0..0, + PK => 0..0, + PL => 0..8, + PS => 0..0, + PT => 4..8, + QA => 0..0, + RO => 0..0, + RS => 0..0, + RU => 9..14, + SA => 0..0, + SC => 6..8, + SD => 0..0, + SE => 0..0, + SI => 0..0, + SK => 0..0, + SM => 6..11, + SO => 4..7, + ST => 4..8, + SV => 0..0, + TL => 0..0, + TN => 2..5, + TR => 0..0, + UA => 0..0, + VA => 0..0, + VG => 0..0, + XK => 2..4, + YE => 4..8, + } + } + + pub const fn bban_pattern(self) -> Pattern { + match self { + AD => &[(8, N), (12, C)], + AE => &[(19, N)], + AL => &[(8, N), (16, C)], + AT => &[(16, N)], + AZ => &[(4, A), (20, C)], + BA => &[(16, N)], + BE => &[(12, N)], + BG => &[(4, A), (6, N), (8, C)], + BH => &[(4, A), (14, C)], + BI => &[(23, N)], + BR => &[(23, N), (1, A), (1, C)], + BY => &[(4, C), (4, N), (16, C)], + CH => &[(5, N), (12, C)], + CR => &[(18, N)], + CY => &[(8, N), (16, C)], + CZ => &[(20, N)], + DE => &[(18, N)], + DJ => &[(23, N)], + DK => &[(14, N)], + DO => &[(4, C), (20, N)], + EE => &[(16, N)], + EG => &[(25, N)], + ES => &[(20, N)], + FI => &[(14, N)], + FK => &[(2, A), (12, N)], + FO => &[(14, N)], + FR => &[(10, N), (11, C), (2, N)], + GB => &[(4, A), (14, N)], + GE => &[(2, A), (16, N)], + GI => &[(4, A), (15, C)], + GL => &[(14, N)], + GR => &[(7, N), (16, C)], + GT => &[(24, C)], + HN => &[(4, A), (20, N)], + HR => &[(17, N)], + HU => &[(24, N)], + IE => &[(4, A), (14, N)], + IL => &[(19, N)], + IQ => &[(4, A), (15, N)], + IS => &[(22, N)], + IT => &[(1, A), (10, N), (12, C)], + JO => &[(4, A), (4, N), (18, C)], + KW => &[(4, A), (22, C)], + KZ => &[(3, N), (13, C)], + LB => &[(4, N), (20, C)], + LC => &[(4, A), (24, C)], + LI => &[(5, N), (12, C)], + LT => &[(16, N)], + LU => &[(3, N), (13, C)], + LV => &[(4, A), (13, C)], + LY => &[(21, N)], + MC => &[(10, N), (11, C), (2, N)], + MD => &[(20, C)], + ME => &[(18, N)], + MK => &[(3, N), (10, C), (2, N)], + MN => &[(16, N)], + MR => &[(23, N)], + MT => &[(4, A), (5, N), (18, C)], + MU => &[(4, A), (19, N), (3, A)], + NI => &[(4, A), (20, N)], + NL => &[(4, A), (10, N)], + NO => &[(11, N)], + OM => &[(3, N), (16, C)], + PK => &[(4, A), (16, C)], + PL => &[(24, N)], + PS => &[(4, A), (21, C)], + PT => &[(21, N)], + QA => &[(4, A), (21, C)], + RO => &[(4, A), (16, C)], + RS => &[(18, N)], + RU => &[(14, N), (15, C)], + SA => &[(2, N), (18, C)], + SC => &[(4, A), (20, N), (3, A)], + SD => &[(14, N)], + SE => &[(20, N)], + SI => &[(15, N)], + SK => &[(20, N)], + SM => &[(1, A), (10, N), (12, C)], + SO => &[(19, N)], + ST => &[(21, N)], + SV => &[(4, A), (20, N)], + TL => &[(19, N)], + TN => &[(20, N)], + TR => &[(6, N), (16, C)], + UA => &[(6, N), (19, C)], + VA => &[(18, N)], + VG => &[(4, A), (16, N)], + XK => &[(16, N)], + YE => &[(4, A), (4, N), (18, C)], + } + } + + pub fn bban_check(self, iban_ascii: &[u8]) -> Result<(), PatternErr> { + // IBAN are made of ASCII digits and uppercase + debug_assert!(iban_ascii + .iter() + .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())); + match self { + AD => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..8]) || !C.check(&iban_ascii[8..20]) { + return Err(PatternErr::Malformed); + } + } + AE => { + if iban_ascii.len() != 19 { + return Err(PatternErr::Len(19, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..19]) { + return Err(PatternErr::Malformed); + } + } + AL => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..8]) || !C.check(&iban_ascii[8..24]) { + return Err(PatternErr::Malformed); + } + } + AT => { + if iban_ascii.len() != 16 { + return Err(PatternErr::Len(16, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..16]) { + return Err(PatternErr::Malformed); + } + } + AZ => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..24]) { + return Err(PatternErr::Malformed); + } + } + BA => { + if iban_ascii.len() != 16 { + return Err(PatternErr::Len(16, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..16]) { + return Err(PatternErr::Malformed); + } + } + BE => { + if iban_ascii.len() != 12 { + return Err(PatternErr::Len(12, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..12]) { + return Err(PatternErr::Malformed); + } + } + BG => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) + || !N.check(&iban_ascii[4..10]) + || !C.check(&iban_ascii[10..18]) + { + return Err(PatternErr::Malformed); + } + } + BH => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..18]) { + return Err(PatternErr::Malformed); + } + } + BI => { + if iban_ascii.len() != 23 { + return Err(PatternErr::Len(23, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..23]) { + return Err(PatternErr::Malformed); + } + } + BR => { + if iban_ascii.len() != 25 { + return Err(PatternErr::Len(25, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..23]) + || !A.check(&iban_ascii[23..24]) + || !C.check(&iban_ascii[24..25]) + { + return Err(PatternErr::Malformed); + } + } + BY => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !C.check(&iban_ascii[0..4]) + || !N.check(&iban_ascii[4..8]) + || !C.check(&iban_ascii[8..24]) + { + return Err(PatternErr::Malformed); + } + } + CH => { + if iban_ascii.len() != 17 { + return Err(PatternErr::Len(17, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..5]) || !C.check(&iban_ascii[5..17]) { + return Err(PatternErr::Malformed); + } + } + CR => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..18]) { + return Err(PatternErr::Malformed); + } + } + CY => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..8]) || !C.check(&iban_ascii[8..24]) { + return Err(PatternErr::Malformed); + } + } + CZ => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..20]) { + return Err(PatternErr::Malformed); + } + } + DE => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..18]) { + return Err(PatternErr::Malformed); + } + } + DJ => { + if iban_ascii.len() != 23 { + return Err(PatternErr::Len(23, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..23]) { + return Err(PatternErr::Malformed); + } + } + DK => { + if iban_ascii.len() != 14 { + return Err(PatternErr::Len(14, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..14]) { + return Err(PatternErr::Malformed); + } + } + DO => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !C.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..24]) { + return Err(PatternErr::Malformed); + } + } + EE => { + if iban_ascii.len() != 16 { + return Err(PatternErr::Len(16, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..16]) { + return Err(PatternErr::Malformed); + } + } + EG => { + if iban_ascii.len() != 25 { + return Err(PatternErr::Len(25, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..25]) { + return Err(PatternErr::Malformed); + } + } + ES => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..20]) { + return Err(PatternErr::Malformed); + } + } + FI => { + if iban_ascii.len() != 14 { + return Err(PatternErr::Len(14, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..14]) { + return Err(PatternErr::Malformed); + } + } + FK => { + if iban_ascii.len() != 14 { + return Err(PatternErr::Len(14, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..2]) || !N.check(&iban_ascii[2..14]) { + return Err(PatternErr::Malformed); + } + } + FO => { + if iban_ascii.len() != 14 { + return Err(PatternErr::Len(14, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..14]) { + return Err(PatternErr::Malformed); + } + } + FR => { + if iban_ascii.len() != 23 { + return Err(PatternErr::Len(23, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..10]) + || !C.check(&iban_ascii[10..21]) + || !N.check(&iban_ascii[21..23]) + { + return Err(PatternErr::Malformed); + } + } + GB => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..18]) { + return Err(PatternErr::Malformed); + } + } + GE => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..2]) || !N.check(&iban_ascii[2..18]) { + return Err(PatternErr::Malformed); + } + } + GI => { + if iban_ascii.len() != 19 { + return Err(PatternErr::Len(19, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..19]) { + return Err(PatternErr::Malformed); + } + } + GL => { + if iban_ascii.len() != 14 { + return Err(PatternErr::Len(14, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..14]) { + return Err(PatternErr::Malformed); + } + } + GR => { + if iban_ascii.len() != 23 { + return Err(PatternErr::Len(23, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..7]) || !C.check(&iban_ascii[7..23]) { + return Err(PatternErr::Malformed); + } + } + GT => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !C.check(&iban_ascii[0..24]) { + return Err(PatternErr::Malformed); + } + } + HN => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..24]) { + return Err(PatternErr::Malformed); + } + } + HR => { + if iban_ascii.len() != 17 { + return Err(PatternErr::Len(17, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..17]) { + return Err(PatternErr::Malformed); + } + } + HU => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..24]) { + return Err(PatternErr::Malformed); + } + } + IE => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..18]) { + return Err(PatternErr::Malformed); + } + } + IL => { + if iban_ascii.len() != 19 { + return Err(PatternErr::Len(19, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..19]) { + return Err(PatternErr::Malformed); + } + } + IQ => { + if iban_ascii.len() != 19 { + return Err(PatternErr::Len(19, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..19]) { + return Err(PatternErr::Malformed); + } + } + IS => { + if iban_ascii.len() != 22 { + return Err(PatternErr::Len(22, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..22]) { + return Err(PatternErr::Malformed); + } + } + IT => { + if iban_ascii.len() != 23 { + return Err(PatternErr::Len(23, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..1]) + || !N.check(&iban_ascii[1..11]) + || !C.check(&iban_ascii[11..23]) + { + return Err(PatternErr::Malformed); + } + } + JO => { + if iban_ascii.len() != 26 { + return Err(PatternErr::Len(26, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) + || !N.check(&iban_ascii[4..8]) + || !C.check(&iban_ascii[8..26]) + { + return Err(PatternErr::Malformed); + } + } + KW => { + if iban_ascii.len() != 26 { + return Err(PatternErr::Len(26, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..26]) { + return Err(PatternErr::Malformed); + } + } + KZ => { + if iban_ascii.len() != 16 { + return Err(PatternErr::Len(16, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..3]) || !C.check(&iban_ascii[3..16]) { + return Err(PatternErr::Malformed); + } + } + LB => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..24]) { + return Err(PatternErr::Malformed); + } + } + LC => { + if iban_ascii.len() != 28 { + return Err(PatternErr::Len(28, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..28]) { + return Err(PatternErr::Malformed); + } + } + LI => { + if iban_ascii.len() != 17 { + return Err(PatternErr::Len(17, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..5]) || !C.check(&iban_ascii[5..17]) { + return Err(PatternErr::Malformed); + } + } + LT => { + if iban_ascii.len() != 16 { + return Err(PatternErr::Len(16, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..16]) { + return Err(PatternErr::Malformed); + } + } + LU => { + if iban_ascii.len() != 16 { + return Err(PatternErr::Len(16, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..3]) || !C.check(&iban_ascii[3..16]) { + return Err(PatternErr::Malformed); + } + } + LV => { + if iban_ascii.len() != 17 { + return Err(PatternErr::Len(17, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..17]) { + return Err(PatternErr::Malformed); + } + } + LY => { + if iban_ascii.len() != 21 { + return Err(PatternErr::Len(21, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..21]) { + return Err(PatternErr::Malformed); + } + } + MC => { + if iban_ascii.len() != 23 { + return Err(PatternErr::Len(23, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..10]) + || !C.check(&iban_ascii[10..21]) + || !N.check(&iban_ascii[21..23]) + { + return Err(PatternErr::Malformed); + } + } + MD => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !C.check(&iban_ascii[0..20]) { + return Err(PatternErr::Malformed); + } + } + ME => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..18]) { + return Err(PatternErr::Malformed); + } + } + MK => { + if iban_ascii.len() != 15 { + return Err(PatternErr::Len(15, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..3]) + || !C.check(&iban_ascii[3..13]) + || !N.check(&iban_ascii[13..15]) + { + return Err(PatternErr::Malformed); + } + } + MN => { + if iban_ascii.len() != 16 { + return Err(PatternErr::Len(16, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..16]) { + return Err(PatternErr::Malformed); + } + } + MR => { + if iban_ascii.len() != 23 { + return Err(PatternErr::Len(23, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..23]) { + return Err(PatternErr::Malformed); + } + } + MT => { + if iban_ascii.len() != 27 { + return Err(PatternErr::Len(27, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) + || !N.check(&iban_ascii[4..9]) + || !C.check(&iban_ascii[9..27]) + { + return Err(PatternErr::Malformed); + } + } + MU => { + if iban_ascii.len() != 26 { + return Err(PatternErr::Len(26, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) + || !N.check(&iban_ascii[4..23]) + || !A.check(&iban_ascii[23..26]) + { + return Err(PatternErr::Malformed); + } + } + NI => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..24]) { + return Err(PatternErr::Malformed); + } + } + NL => { + if iban_ascii.len() != 14 { + return Err(PatternErr::Len(14, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..14]) { + return Err(PatternErr::Malformed); + } + } + NO => { + if iban_ascii.len() != 11 { + return Err(PatternErr::Len(11, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..11]) { + return Err(PatternErr::Malformed); + } + } + OM => { + if iban_ascii.len() != 19 { + return Err(PatternErr::Len(19, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..3]) || !C.check(&iban_ascii[3..19]) { + return Err(PatternErr::Malformed); + } + } + PK => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..20]) { + return Err(PatternErr::Malformed); + } + } + PL => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..24]) { + return Err(PatternErr::Malformed); + } + } + PS => { + if iban_ascii.len() != 25 { + return Err(PatternErr::Len(25, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..25]) { + return Err(PatternErr::Malformed); + } + } + PT => { + if iban_ascii.len() != 21 { + return Err(PatternErr::Len(21, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..21]) { + return Err(PatternErr::Malformed); + } + } + QA => { + if iban_ascii.len() != 25 { + return Err(PatternErr::Len(25, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..25]) { + return Err(PatternErr::Malformed); + } + } + RO => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !C.check(&iban_ascii[4..20]) { + return Err(PatternErr::Malformed); + } + } + RS => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..18]) { + return Err(PatternErr::Malformed); + } + } + RU => { + if iban_ascii.len() != 29 { + return Err(PatternErr::Len(29, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..14]) || !C.check(&iban_ascii[14..29]) { + return Err(PatternErr::Malformed); + } + } + SA => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..2]) || !C.check(&iban_ascii[2..20]) { + return Err(PatternErr::Malformed); + } + } + SC => { + if iban_ascii.len() != 27 { + return Err(PatternErr::Len(27, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) + || !N.check(&iban_ascii[4..24]) + || !A.check(&iban_ascii[24..27]) + { + return Err(PatternErr::Malformed); + } + } + SD => { + if iban_ascii.len() != 14 { + return Err(PatternErr::Len(14, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..14]) { + return Err(PatternErr::Malformed); + } + } + SE => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..20]) { + return Err(PatternErr::Malformed); + } + } + SI => { + if iban_ascii.len() != 15 { + return Err(PatternErr::Len(15, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..15]) { + return Err(PatternErr::Malformed); + } + } + SK => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..20]) { + return Err(PatternErr::Malformed); + } + } + SM => { + if iban_ascii.len() != 23 { + return Err(PatternErr::Len(23, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..1]) + || !N.check(&iban_ascii[1..11]) + || !C.check(&iban_ascii[11..23]) + { + return Err(PatternErr::Malformed); + } + } + SO => { + if iban_ascii.len() != 19 { + return Err(PatternErr::Len(19, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..19]) { + return Err(PatternErr::Malformed); + } + } + ST => { + if iban_ascii.len() != 21 { + return Err(PatternErr::Len(21, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..21]) { + return Err(PatternErr::Malformed); + } + } + SV => { + if iban_ascii.len() != 24 { + return Err(PatternErr::Len(24, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..24]) { + return Err(PatternErr::Malformed); + } + } + TL => { + if iban_ascii.len() != 19 { + return Err(PatternErr::Len(19, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..19]) { + return Err(PatternErr::Malformed); + } + } + TN => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..20]) { + return Err(PatternErr::Malformed); + } + } + TR => { + if iban_ascii.len() != 22 { + return Err(PatternErr::Len(22, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..6]) || !C.check(&iban_ascii[6..22]) { + return Err(PatternErr::Malformed); + } + } + UA => { + if iban_ascii.len() != 25 { + return Err(PatternErr::Len(25, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..6]) || !C.check(&iban_ascii[6..25]) { + return Err(PatternErr::Malformed); + } + } + VA => { + if iban_ascii.len() != 18 { + return Err(PatternErr::Len(18, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..18]) { + return Err(PatternErr::Malformed); + } + } + VG => { + if iban_ascii.len() != 20 { + return Err(PatternErr::Len(20, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) || !N.check(&iban_ascii[4..20]) { + return Err(PatternErr::Malformed); + } + } + XK => { + if iban_ascii.len() != 16 { + return Err(PatternErr::Len(16, iban_ascii.len())); + } else if !N.check(&iban_ascii[0..16]) { + return Err(PatternErr::Malformed); + } + } + YE => { + if iban_ascii.len() != 26 { + return Err(PatternErr::Len(26, iban_ascii.len())); + } else if !A.check(&iban_ascii[0..4]) + || !N.check(&iban_ascii[4..8]) + || !C.check(&iban_ascii[8..26]) + { + return Err(PatternErr::Malformed); + } + } + } + Ok(()) + } +} + +impl Display for Country { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Generate random ASCII string following an IBAN pattern rules +pub fn rng_pattern(out: &mut [u8], pattern: Pattern) { + let mut cursor = 0; + for (len, rule) in pattern { + let alphabet = match rule { + IbanC::C => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + IbanC::N => "0123456789", + IbanC::A => "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + }; + for b in &mut out[cursor..cursor + *len as usize] { + *b = *fastrand::choice(alphabet.as_bytes()).unwrap(); + } + cursor += *len as usize + } +} + +#[cfg(test)] +pub const VALID_IBAN: [(&str, Option<&str>); 89] = [ + ("AD1200012030200359100100", Some("00012030200359100100")), + ("AE070331234567890123456", Some("0331234567890123456")), + ( + "AL47212110090000000235698741", + Some("212110090000000235698741"), + ), + ("AT611904300234573201", Some("1904300234573201")), + ( + "AZ21NABZ00000000137010001944", + Some("NABZ00000000137010001944"), + ), + ("BA391290079401028494", Some("1290079401028494")), + ("BE68539007547034", Some("539007547034")), + ("BG80BNBG96611020345678", Some("BNBG96611020345678")), + ("BH67BMAG00001299123456", Some("BMAG00001299123456")), + ( + "BI4210000100010000332045181", + Some("10000100010000332045181"), + ), + ( + "BR1800360305000010009795493C1", + Some("00360305000010009795493C1"), + ), + ( + "BY13NBRB3600900000002Z00AB00", + Some("NBRB3600900000002Z00AB00"), + ), + ("CH9300762011623852957", Some("00762011623852957")), + ("CR05015202001026284066", Some("015202001026284066")), + ( + "CY17002001280000001200527600", + Some("002001280000001200527600"), + ), + ("CZ6508000000192000145399", Some("08000000192000145399")), + ("DE89370400440532013000", Some("370400440532013000")), + ( + "DJ2100010000000154000100186", + Some("00010000000154000100186"), + ), + ("DK5000400440116243", Some("00400440116243")), + ( + "DO28BAGR00000001212453611324", + Some("BAGR00000001212453611324"), + ), + ("EE382200221020145685", Some("2200221020145685")), + ( + "EG380019000500000000263180002", + Some("0019000500000000263180002"), + ), + ("ES9121000418450200051332", Some("21000418450200051332")), + ("FI2112345600000785", None), + ("FK88SC123456789012", Some("SC123456789012")), + ("FO6264600001631634", Some("64600001631634")), + ( + "FR1420041010050500013M02606", + Some("20041010050500013M02606"), + ), + ("GB29NWBK60161331926819", Some("NWBK60161331926819")), + ("GE29NB0000000101904917", Some("NB0000000101904917")), + ("GI75NWBK000000007099453", Some("NWBK000000007099453")), + ("GL8964710001000206", Some("64710001000206")), + ( + "GR1601101250000000012300695", + Some("01101250000000012300695"), + ), + ( + "GT82TRAJ01020000001210029690", + Some("TRAJ01020000001210029690"), + ), + ( + "HN88CABF00000000000250005469", + Some("CABF00000000000250005469"), + ), + ("HR1210010051863000160", Some("10010051863000160")), + ( + "HU42117730161111101800000000", + Some("117730161111101800000000"), + ), + ("IE29AIBK93115212345678", Some("AIBK93115212345678")), + ("IL620108000000099999999", Some("0108000000099999999")), + ("IQ98NBIQ850123456789012", Some("NBIQ850123456789012")), + ("IS140159260076545510730339", Some("0159260076545510730339")), + ( + "IT60X0542811101000000123456", + Some("X0542811101000000123456"), + ), + ( + "JO94CBJO0010000000000131000302", + Some("CBJO0010000000000131000302"), + ), + ( + "KW81CBKU0000000000001234560101", + Some("CBKU0000000000001234560101"), + ), + ("KZ86125KZT5004100100", Some("125KZT5004100100")), + ( + "LB62099900000001001901229114", + Some("099900000001001901229114"), + ), + ( + "LC55HEMM000100010012001200023015", + Some("HEMM000100010012001200023015"), + ), + ("LI21088100002324013AA", Some("088100002324013AA")), + ("LT121000011101001000", Some("1000011101001000")), + ("LU280019400644750000", Some("0019400644750000")), + ("LV80BANK0000435195001", Some("BANK0000435195001")), + ("LY83002048000020100120361", Some("002048000020100120361")), + ( + "MC5811222000010123456789030", + Some("11222000010123456789030"), + ), + ("MD24AG000225100013104168", Some("AG000225100013104168")), + ("ME25505000012345678951", Some("505000012345678951")), + ("MK07250120000058984", Some("250120000058984")), + ("MN121234123456789123", Some("1234123456789123")), + ( + "MR1300020001010000123456753", + Some("00020001010000123456753"), + ), + ( + "MT84MALT011000012345MTLCAST001S", + Some("MALT011000012345MTLCAST001S"), + ), + ( + "MU17BOMM0101101030300200000MUR", + Some("BOMM0101101030300200000MUR"), + ), + ( + "NI45BAPR00000013000003558124", + Some("BAPR00000013000003558124"), + ), + ("NL91ABNA0417164300", Some("ABNA0417164300")), + ("NO9386011117947", Some("86011117947")), + ("OM810180000001299123456", Some("0180000001299123456")), + ("PK36SCBL0000001123456702", Some("SCBL0000001123456702")), + ( + "PL61109010140000071219812874", + Some("109010140000071219812874"), + ), + ( + "PS92PALS000000000400123456702", + Some("PALS000000000400123456702"), + ), + ("PT50000201231234567890154", Some("000201231234567890154")), + ( + "QA58DOHB00001234567890ABCDEFG", + Some("DOHB00001234567890ABCDEFG"), + ), + ("RO49AAAA1B31007593840000", Some("AAAA1B31007593840000")), + ("RS35260005601001611379", Some("260005601001611379")), + ( + "RU0304452522540817810538091310419", + Some("04452522540817810538091310419"), + ), + ("SA0380000000608010167519", Some("80000000608010167519")), + ( + "SC18SSCB11010000000000001497USD", + Some("SSCB11010000000000001497USD"), + ), + ("SD2129010501234001", Some("29010501234001")), + ("SE4550000000058398257466", Some("50000000058398257466")), + ("SI56263300012039086", Some("263300012039086")), + ("SK3112000000198742637541", Some("12000000198742637541")), + ( + "SM86U0322509800000000270100", + Some("U0322509800000000270100"), + ), + ("SO211000001001000100141", Some("1000001001000100141")), + ("ST23000100010051845310146", Some("000100010051845310146")), + ( + "SV62CENR00000000000000700025", + Some("CENR00000000000000700025"), + ), + ("TL380080012345678910157", Some("0080012345678910157")), + ("TN5910006035183598478831", Some("10006035183598478831")), + ("TR330006100519786457841326", Some("0006100519786457841326")), + ( + "UA213223130000026007233566001", + Some("3223130000026007233566001"), + ), + ("VA59001123000012345678", Some("001123000012345678")), + ("VG96VPVG0000012345678901", Some("VPVG0000012345678901")), + ("XK051212012345678906", Some("1212012345678906")), + ( + "YE15CBYE0001018861234567891234", + Some("CBYE0001018861234567891234"), + ), +]; diff --git a/taler-magnet-bank/src/lib.rs b/taler-magnet-bank/src/lib.rs @@ -17,7 +17,7 @@ use std::{borrow::Cow, str::FromStr}; use taler_common::types::{ - iban::{IbanErrorKind, ParseIbanError, IBAN}, + iban::{Country, IbanErrorKind, ParseIbanError, IBAN}, payto::{FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI, TransferPayto}, }; @@ -85,7 +85,7 @@ impl HuIban { } else { Cow::Borrowed(bban) }; - let iban = IBAN::from_parts("HU", &full_bban); + let iban = IBAN::from_parts(Country::HU, &full_bban); Ok(Self(iban)) } @@ -104,7 +104,7 @@ pub enum HuIbanErr { #[error("contains illegal characters (only 0-9 allowed)")] Invalid, #[error("expected an hungarian IBAN starting with HU got {0}")] - CountryCode(String), + Country(Country), #[error("invalid length expected 16 or 24 chars got {0}")] BbanSize(usize), #[error("invalid checksum for {0} expected {1} got {2}")] @@ -129,9 +129,9 @@ impl TryFrom<IBAN> for HuIban { type Error = HuIbanErr; fn try_from(iban: IBAN) -> Result<Self, Self::Error> { - let country_code = iban.country_code(); - if country_code != "HU" { - return Err(HuIbanErr::CountryCode(country_code.to_owned())); + let country = iban.country(); + if country != Country::HU { + return Err(HuIbanErr::Country(country)); } Self::check_bban(iban.bban())?;