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 }