taler-rust

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

iban.rs (10922B)


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