taler-rust

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

iban.rs (11055B)


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