iban.rs (11133B)
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 use compact_str::CompactString; 24 pub use registry::Country; 25 use registry::{IbanC, PatternErr, check_pattern, rng_pattern}; 26 use serde_with::{DeserializeFromStr, SerializeDisplay}; 27 28 use super::utils::InlineStr; 29 30 mod registry; 31 32 const MAX_IBAN_SIZE: usize = 34; 33 const MAX_BIC_SIZE: usize = 11; 34 35 /// Parse an IBAN, panic if malformed 36 pub fn iban(iban: impl AsRef<str>) -> IBAN { 37 iban.as_ref().parse().expect("invalid IBAN") 38 } 39 40 /// Parse an BIC, panic if malformed 41 pub fn bic(bic: impl AsRef<str>) -> BIC { 42 bic.as_ref().parse().expect("invalid BIC") 43 } 44 45 #[derive(Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 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 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"00") 72 .chain(bban.iter().copied()), 73 ) 74 .unwrap(); 75 // Compute check digit 76 let checksum = 98 - Self::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 IbanErrKind { 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 ParseIbanErr { 145 iban: CompactString, 146 pub kind: IbanErrKind, 147 } 148 149 impl FromStr for IBAN { 150 type Err = ParseIbanErr; 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(IbanErrKind::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(IbanErrKind::Underflow(encoded.len())) 166 } else if !IbanC::A.check(&encoded[0..2]) || !IbanC::N.check(&encoded[2..4]) { 167 Err(IbanErrKind::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) => IbanErrKind::Size(expected, got), 172 PatternErr::Malformed => IbanErrKind::Malformed, 173 }) 174 } else { 175 let checksum = Self::checksum(&encoded); 176 if checksum != 1 { 177 Err(IbanErrKind::Checksum(checksum)) 178 } else { 179 Ok(Self { country, encoded }) 180 } 181 } 182 } else { 183 Err(IbanErrKind::UnknownCountry( 184 encoded.as_ref()[..2].to_owned(), 185 )) 186 } 187 } else { 188 Err(IbanErrKind::Overflow(bytes.len())) 189 } 190 .map_err(|kind| ParseIbanErr { 191 iban: s.into(), 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 impl Debug for IBAN { 204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 205 Display::fmt(&self, f) 206 } 207 } 208 209 /// Bank Identifier Code (BIC) 210 #[derive(Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 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 BicErrKind { 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 got {0}")] 251 Size(usize), 252 } 253 254 #[derive(Debug, thiserror::Error)] 255 #[error("bic '{bic}' {kind}")] 256 pub struct ParseBicErr { 257 bic: CompactString, 258 pub kind: BicErrKind, 259 } 260 261 impl FromStr for BIC { 262 type Err = ParseBicErr; 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(BicErrKind::Size(len)) 269 } else if !bytes[0..4].iter().all(u8::is_ascii_alphabetic) { 270 Err(BicErrKind::BankCode) 271 } else if !bytes[4..6].iter().all(u8::is_ascii_alphabetic) { 272 Err(BicErrKind::CountryCode) 273 } else if !bytes[6..].iter().all(u8::is_ascii_alphanumeric) { 274 Err(BicErrKind::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| ParseBicErr { 282 bic: s.into(), 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 impl Debug for BIC { 295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 296 Display::fmt(&self, f) 297 } 298 } 299 300 #[test] 301 fn parse_iban() { 302 use registry::VALID_IBAN; 303 for (valid, bban) in VALID_IBAN { 304 // Parsing 305 let iban = IBAN::from_str(valid).unwrap(); 306 assert_eq!(iban.to_string(), valid); 307 // Roundtrip 308 let from_parts = IBAN::from_parts(iban.country(), iban.bban()); 309 assert_eq!(from_parts.to_string(), valid); 310 311 // BBAN 312 if let Some(bban) = bban { 313 assert_eq!(bban, iban.bban()); 314 } 315 316 // Random 317 let rand = IBAN::random(iban.country()); 318 let parsed = IBAN::from_str(rand.as_ref()).unwrap(); 319 assert_eq!(rand, parsed); 320 } 321 322 for (invalid, err) in [ 323 ("FR1420041@10050500013M02606", IbanErrKind::Invalid), 324 ("", IbanErrKind::Underflow(0)), 325 ("12345678901234567890123456", IbanErrKind::Malformed), 326 ("FR", IbanErrKind::Underflow(2)), 327 ("FRANCE123456", IbanErrKind::Malformed), 328 ("DE44500105175407324932", IbanErrKind::Checksum(28)), 329 ] { 330 let iban = IBAN::from_str(invalid).unwrap_err(); 331 assert_eq!(iban.kind, err); 332 } 333 } 334 335 #[test] 336 fn parse_bic() { 337 for (valid, parts) in [ 338 ("DEUTDEFF", ("DEUT", "DE", "FF", None)), // Deutsche Bank, Germany 339 ("NEDSZAJJ", ("NEDS", "ZA", "JJ", None)), // Nedbank, South Africa // codespell:ignore 340 ("BARCGB22", ("BARC", "GB", "22", None)), // Barclays, UK 341 ("CHASUS33XXX", ("CHAS", "US", "33", Some("XXX"))), // JP Morgan Chase, USA (branch) 342 ("BNPAFRPP", ("BNPA", "FR", "PP", None)), // BNP Paribas, France 343 ("INGBNL2A", ("INGB", "NL", "2A", None)), // ING Bank, Netherlands 344 ] { 345 let bic = BIC::from_str(valid).unwrap(); 346 assert_eq!( 347 ( 348 bic.bank_code(), 349 bic.country_code(), 350 bic.location_code(), 351 bic.branch_code() 352 ), 353 parts 354 ); 355 assert_eq!(bic.to_string(), valid); 356 } 357 358 for (invalid, err) in [ 359 ("DEU", BicErrKind::Size(3)), 360 ("DEUTDEFFA1BC", BicErrKind::Size(12)), 361 ("D3UTDEFF", BicErrKind::BankCode), 362 ("DEUTD3FF", BicErrKind::CountryCode), 363 ("DEUTDEFF@@1", BicErrKind::Invalid), 364 ] { 365 let bic = BIC::from_str(invalid).unwrap_err(); 366 assert_eq!(bic.kind, err, "{invalid}"); 367 } 368 }