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 }