taler-rust

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

iban.rs (11042B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{
     18     fmt::{Debug, Display},
     19     ops::Deref,
     20     str::FromStr,
     21 };
     22 
     23 pub use registry::Country;
     24 use registry::{IbanC, PatternErr, check_pattern, rng_pattern};
     25 
     26 use super::utils::InlineStr;
     27 
     28 mod registry;
     29 
     30 const MAX_IBAN_SIZE: usize = 34;
     31 const MAX_BIC_SIZE: usize = 11;
     32 
     33 /// Parse an IBAN, panic if malformed
     34 pub fn iban(iban: impl AsRef<str>) -> IBAN {
     35     iban.as_ref().parse().expect("invalid IBAN")
     36 }
     37 
     38 /// Parse an BIC, panic if malformed
     39 pub fn bic(bic: impl AsRef<str>) -> BIC {
     40     bic.as_ref().parse().expect("invalid BIC")
     41 }
     42 
     43 #[derive(Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay)]
     44 /// International Bank Account Number (IBAN)
     45 pub struct IBAN {
     46     country: Country,
     47     encoded: InlineStr<MAX_IBAN_SIZE>,
     48 }
     49 
     50 impl IBAN {
     51     /// Compute IBAN checksum
     52     fn iban_checksum(s: &[u8]) -> u8 {
     53         (s.iter().cycle().skip(4).take(s.len()).fold(0u32, |sum, b| {
     54             if b.is_ascii_digit() {
     55                 (sum * 10 + (b - b'0') as u32) % 97
     56             } else {
     57                 (sum * 100 + (b - b'A' + 10) as u32) % 97
     58             }
     59         })) as u8
     60     }
     61 
     62     fn from_raw_parts(country: Country, bban: &[u8]) -> Self {
     63         // Create an iban with an empty digit check
     64         let mut encoded = InlineStr::try_from_iter(
     65             country
     66                 .iso_bytes()
     67                 .iter()
     68                 .copied()
     69                 .chain([b'0', b'0'])
     70                 .chain(bban.iter().copied()),
     71         )
     72         .unwrap();
     73         // Compute check digit
     74         let checksum = 98 - Self::iban_checksum(encoded.deref());
     75 
     76         // And insert it
     77         unsafe {
     78             // SAFETY: we only insert ASCII digits
     79             let buf = encoded.deref_mut();
     80             buf[3] = checksum % 10 + b'0';
     81             buf[2] = checksum / 10 + b'0';
     82         }
     83 
     84         Self { country, encoded }
     85     }
     86 
     87     pub fn from_parts(country: Country, bban: &str) -> Self {
     88         check_pattern(bban.as_bytes(), country.bban_pattern()).unwrap(); // TODO  return Result
     89         Self::from_raw_parts(country, bban.as_bytes())
     90     }
     91 
     92     pub fn random(country: Country) -> Self {
     93         let mut bban = [0u8; MAX_IBAN_SIZE - 4];
     94         rng_pattern(&mut bban, country.bban_pattern());
     95         Self::from_raw_parts(country, &bban[..country.bban_len()])
     96     }
     97 
     98     pub fn country(&self) -> Country {
     99         self.country
    100     }
    101 
    102     pub fn bban(&self) -> &str {
    103         // SAFETY len >= 5
    104         unsafe { self.as_ref().get_unchecked(4..) }
    105     }
    106 
    107     pub fn bank_id(&self) -> &str {
    108         &self.bban()[self.country.bank_id()]
    109     }
    110 
    111     pub fn branch_id(&self) -> &str {
    112         &self.bban()[self.country.branch_id()]
    113     }
    114 }
    115 
    116 impl AsRef<str> for IBAN {
    117     fn as_ref(&self) -> &str {
    118         self.encoded.as_ref()
    119     }
    120 }
    121 
    122 #[derive(Debug, PartialEq, Eq, thiserror::Error)]
    123 pub enum IbanErrorKind {
    124     #[error("contains illegal characters (only 0-9A-Z allowed)")]
    125     Invalid,
    126     #[error("contains invalid characters")]
    127     Malformed,
    128     #[error("unknown country {0}")]
    129     UnknownCountry(String),
    130     #[error("too long expected max {MAX_IBAN_SIZE} chars got {0}")]
    131     Overflow(usize),
    132     #[error("too short expected min 4 chars got {0}")]
    133     Underflow(usize),
    134     #[error("wrong size expected {0} chars got {1}")]
    135     Size(u8, usize),
    136     #[error("checksum expected 1 got {0}")]
    137     Checksum(u8),
    138 }
    139 
    140 #[derive(Debug, thiserror::Error)]
    141 #[error("iban '{iban}' {kind}")]
    142 pub struct ParseIbanError {
    143     iban: String,
    144     pub kind: IbanErrorKind,
    145 }
    146 
    147 impl FromStr for IBAN {
    148     type Err = ParseIbanError;
    149 
    150     fn from_str(s: &str) -> Result<Self, Self::Err> {
    151         let bytes: &[u8] = s.as_bytes();
    152         if !bytes
    153             .iter()
    154             .all(|b| b.is_ascii_whitespace() || b.is_ascii_alphanumeric())
    155         {
    156             Err(IbanErrorKind::Invalid)
    157         } else if let Some(encoded) = InlineStr::try_from_iter(
    158             bytes
    159                 .iter()
    160                 .filter_map(|b| (!b.is_ascii_whitespace()).then_some(b.to_ascii_uppercase())),
    161         ) {
    162             if encoded.len() < 4 {
    163                 Err(IbanErrorKind::Underflow(encoded.len()))
    164             } else if !IbanC::A.check(&encoded[0..2]) || !IbanC::N.check(&encoded[2..4]) {
    165                 Err(IbanErrorKind::Malformed)
    166             } else if let Some(country) = Country::from_iso(&encoded.as_ref()[..2]) {
    167                 if let Err(e) = check_pattern(&encoded[4..], country.bban_pattern()) {
    168                     Err(match e {
    169                         PatternErr::Len(expected, got) => IbanErrorKind::Size(expected, got),
    170                         PatternErr::Malformed => IbanErrorKind::Malformed,
    171                     })
    172                 } else {
    173                     let checksum = Self::iban_checksum(&encoded);
    174                     if checksum != 1 {
    175                         Err(IbanErrorKind::Checksum(checksum))
    176                     } else {
    177                         Ok(Self { country, encoded })
    178                     }
    179                 }
    180             } else {
    181                 Err(IbanErrorKind::UnknownCountry(
    182                     encoded.as_ref()[..2].to_owned(),
    183                 ))
    184             }
    185         } else {
    186             Err(IbanErrorKind::Overflow(bytes.len()))
    187         }
    188         .map_err(|kind| ParseIbanError {
    189             iban: s.to_owned(),
    190             kind,
    191         })
    192     }
    193 }
    194 
    195 impl Display for IBAN {
    196     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    197         Display::fmt(&self.as_ref(), f)
    198     }
    199 }
    200 
    201 impl Debug for IBAN {
    202     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    203         Display::fmt(&self, f)
    204     }
    205 }
    206 
    207 /// Bank Identifier Code (BIC)
    208 #[derive(
    209     Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
    210 )]
    211 pub struct BIC(InlineStr<MAX_BIC_SIZE>);
    212 
    213 impl BIC {
    214     pub fn bank_code(&self) -> &str {
    215         // SAFETY len >= 8
    216         unsafe { self.as_ref().get_unchecked(0..4) }
    217     }
    218 
    219     pub fn country_code(&self) -> &str {
    220         // SAFETY len >= 8
    221         unsafe { self.as_ref().get_unchecked(4..6) }
    222     }
    223 
    224     pub fn location_code(&self) -> &str {
    225         // SAFETY len >= 8
    226         unsafe { self.as_ref().get_unchecked(6..8) }
    227     }
    228 
    229     pub fn branch_code(&self) -> Option<&str> {
    230         // SAFETY len >= 8
    231         let s = unsafe { self.as_ref().get_unchecked(8..) };
    232         (!s.is_empty()).then_some(s)
    233     }
    234 }
    235 
    236 impl AsRef<str> for BIC {
    237     fn as_ref(&self) -> &str {
    238         self.0.as_ref()
    239     }
    240 }
    241 
    242 #[derive(Debug, PartialEq, Eq, thiserror::Error)]
    243 pub enum BicErrorKind {
    244     #[error("contains illegal characters (only 0-9A-Z allowed)")]
    245     Invalid,
    246     #[error("invalid check digit")]
    247     BankCode,
    248     #[error("invalid country code")]
    249     CountryCode,
    250     #[error("bad size expected 8 or {MAX_BIC_SIZE} chars for {0}")]
    251     Size(usize),
    252 }
    253 
    254 #[derive(Debug, thiserror::Error)]
    255 #[error("bic '{bic}' {kind}")]
    256 pub struct ParseBicError {
    257     bic: String,
    258     pub kind: BicErrorKind,
    259 }
    260 
    261 impl FromStr for BIC {
    262     type Err = ParseBicError;
    263 
    264     fn from_str(s: &str) -> Result<Self, Self::Err> {
    265         let bytes: &[u8] = s.as_bytes();
    266         let len = bytes.len();
    267         if len != 8 && len != MAX_BIC_SIZE {
    268             Err(BicErrorKind::Size(len))
    269         } else if !bytes[0..4].iter().all(u8::is_ascii_alphabetic) {
    270             Err(BicErrorKind::BankCode)
    271         } else if !bytes[4..6].iter().all(u8::is_ascii_alphabetic) {
    272             Err(BicErrorKind::CountryCode)
    273         } else if !bytes[6..].iter().all(u8::is_ascii_alphanumeric) {
    274             Err(BicErrorKind::Invalid)
    275         } else {
    276             Ok(Self(
    277                 InlineStr::try_from_iter(bytes.iter().copied().map(|b| b.to_ascii_uppercase()))
    278                     .unwrap(),
    279             ))
    280         }
    281         .map_err(|kind| ParseBicError {
    282             bic: s.to_owned(),
    283             kind,
    284         })
    285     }
    286 }
    287 
    288 impl Display for BIC {
    289     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    290         Display::fmt(&self.as_ref(), f)
    291     }
    292 }
    293 
    294 #[test]
    295 fn parse_iban() {
    296     use registry::VALID_IBAN;
    297     for (valid, bban) in VALID_IBAN {
    298         // Parsing
    299         let iban = IBAN::from_str(valid).unwrap();
    300         assert_eq!(iban.to_string(), valid);
    301         // Roundtrip
    302         let from_parts = IBAN::from_parts(iban.country(), iban.bban());
    303         assert_eq!(from_parts.to_string(), valid);
    304 
    305         // BBAN
    306         if let Some(bban) = bban {
    307             assert_eq!(bban, iban.bban());
    308         }
    309 
    310         // Random
    311         let rand = IBAN::random(iban.country());
    312         let parsed = IBAN::from_str(rand.as_ref()).unwrap();
    313         assert_eq!(rand, parsed);
    314     }
    315 
    316     for (invalid, err) in [
    317         ("FR1420041@10050500013M02606", IbanErrorKind::Invalid),
    318         ("", IbanErrorKind::Underflow(0)),
    319         ("12345678901234567890123456", IbanErrorKind::Malformed),
    320         ("FR", IbanErrorKind::Underflow(2)),
    321         ("FRANCE123456", IbanErrorKind::Malformed),
    322         ("DE44500105175407324932", IbanErrorKind::Checksum(28)),
    323     ] {
    324         let iban = IBAN::from_str(invalid).unwrap_err();
    325         assert_eq!(iban.kind, err);
    326     }
    327 }
    328 
    329 #[test]
    330 fn parse_bic() {
    331     for (valid, parts) in [
    332         ("DEUTDEFF", ("DEUT", "DE", "FF", None)), // Deutsche Bank, Germany
    333         ("NEDSZAJJ", ("NEDS", "ZA", "JJ", None)), // Nedbank, South Africa // codespell:ignore
    334         ("BARCGB22", ("BARC", "GB", "22", None)), // Barclays, UK
    335         ("CHASUS33XXX", ("CHAS", "US", "33", Some("XXX"))), // JP Morgan Chase, USA (branch)
    336         ("BNPAFRPP", ("BNPA", "FR", "PP", None)), // BNP Paribas, France
    337         ("INGBNL2A", ("INGB", "NL", "2A", None)), // ING Bank, Netherlands
    338     ] {
    339         let bic = BIC::from_str(valid).unwrap();
    340         assert_eq!(
    341             (
    342                 bic.bank_code(),
    343                 bic.country_code(),
    344                 bic.location_code(),
    345                 bic.branch_code()
    346             ),
    347             parts
    348         );
    349         assert_eq!(bic.to_string(), valid);
    350     }
    351 
    352     for (invalid, err) in [
    353         ("DEU", BicErrorKind::Size(3)),
    354         ("DEUTDEFFA1BC", BicErrorKind::Size(12)),
    355         ("D3UTDEFF", BicErrorKind::BankCode),
    356         ("DEUTD3FF", BicErrorKind::CountryCode),
    357         ("DEUTDEFF@@1", BicErrorKind::Invalid),
    358     ] {
    359         let bic = BIC::from_str(invalid).unwrap_err();
    360         assert_eq!(bic.kind, err, "{invalid}");
    361     }
    362 }