1use crate::countries::country_iban_length;
2use crate::error::ValidationError;
3use std::borrow::Cow;
4use std::fmt;
5use std::str::FromStr;
6
7const MAX_INPUT_LENGTH: usize = 256;
10
11pub fn validate(iban: &str) -> Result<(), ValidationError> {
25 validate_cow(iban).map(|_| ())
26}
27
28fn validate_cow(iban: &str) -> Result<Cow<'_, str>, ValidationError> {
34 if iban.len() > MAX_INPUT_LENGTH {
36 return Err(ValidationError::InvalidLength {
37 expected: MAX_INPUT_LENGTH,
38 found: iban.len(),
39 });
40 }
41
42 let needs_sanitization = iban
44 .chars()
45 .any(|c| c.is_whitespace() || c.is_ascii_lowercase());
46
47 let cow: Cow<'_, str> = if needs_sanitization {
48 let normalized: String = iban
49 .chars()
50 .filter(|c| !c.is_whitespace())
51 .map(|c| c.to_ascii_uppercase())
52 .collect();
53 Cow::Owned(normalized)
54 } else {
55 Cow::Borrowed(iban)
56 };
57
58 let s = cow.as_ref();
59
60 if s.is_empty() {
62 return Err(ValidationError::Empty);
63 }
64
65 if s.len() < 5 {
67 return Err(ValidationError::InvalidLength {
68 expected: 5,
69 found: s.len(),
70 });
71 }
72
73 let country_code = &s[0..2];
75 let expected_length = country_iban_length(country_code)
76 .ok_or(ValidationError::InvalidCountryCode)?;
77
78 if s.len() != expected_length {
80 return Err(ValidationError::InvalidLength {
81 expected: expected_length,
82 found: s.len(),
83 });
84 }
85
86 let bytes = s.as_bytes();
87
88 for (i, &b) in bytes.iter().enumerate() {
90 if !b.is_ascii_alphanumeric() {
91 return Err(ValidationError::InvalidCharacter {
92 character: b as char,
93 position: i,
94 });
95 }
96 }
97
98 let mut remainder = 0u32;
101
102 for &b in bytes[4..].iter().chain(&bytes[0..4]) {
103 if b.is_ascii_digit() {
104 let digit = (b - b'0') as u32;
105 remainder = (remainder * 10 + digit) % 97;
106 } else {
107 let value = (b - b'A' + 10) as u32;
109 let tens = value / 10;
110 let ones = value % 10;
111 remainder = (remainder * 10 + tens) % 97;
112 remainder = (remainder * 10 + ones) % 97;
113 }
114 }
115
116 if remainder != 1 {
117 return Err(ValidationError::InvalidChecksum);
118 }
119
120 Ok(cow)
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash)]
128pub struct Iban(String);
129
130impl Iban {
131 pub fn new(iban: &str) -> Result<Self, ValidationError> {
141 validate_cow(iban).map(|cow| Iban(cow.into_owned()))
142 }
143
144 pub fn country_code(&self) -> &str {
146 &self.0[0..2]
148 }
149
150 pub fn check_digits(&self) -> &str {
152 &self.0[2..4]
154 }
155
156 pub fn bban(&self) -> &str {
158 &self.0[4..]
160 }
161
162 pub fn as_str(&self) -> &str {
164 &self.0
165 }
166}
167
168impl fmt::Display for Iban {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 write!(f, "{}", self.0)
171 }
172}
173
174impl FromStr for Iban {
175 type Err = ValidationError;
176
177 fn from_str(s: &str) -> Result<Self, Self::Err> {
178 Self::new(s)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
188 fn test_valid_ibans() {
189 assert!(validate("DE89370400440532013000").is_ok()); assert!(validate("GB82WEST12345698765432").is_ok()); assert!(validate("FR1420041010050500013M02606").is_ok()); assert!(validate("AL35202111090000000001234567").is_ok()); assert!(validate("NO9386011117947").is_ok()); }
195
196 #[test]
198 fn test_invalid_checksum() {
199 assert_eq!(
200 validate("DE89370400440532013001"),
201 Err(ValidationError::InvalidChecksum)
202 );
203 assert_eq!(
204 validate("GB82WEST12345698765433"),
205 Err(ValidationError::InvalidChecksum)
206 );
207 }
208
209 #[test]
211 fn test_wrong_length() {
212 assert_eq!(
213 validate("DE8937040044053201300"),
214 Err(ValidationError::InvalidLength {
215 expected: 22,
216 found: 21,
217 })
218 );
219 assert_eq!(
220 validate("GB82WEST1234569876543"),
221 Err(ValidationError::InvalidLength {
222 expected: 22,
223 found: 21,
224 })
225 );
226 assert_eq!(
227 validate("FR1420041010050500013M0260"),
228 Err(ValidationError::InvalidLength {
229 expected: 27,
230 found: 26,
231 })
232 );
233 }
234
235 #[test]
237 fn test_unknown_country() {
238 assert_eq!(
239 validate("XX89370400440532013000"),
240 Err(ValidationError::InvalidCountryCode)
241 );
242 assert_eq!(
243 validate("ZZ1420041010050500013M02606"),
244 Err(ValidationError::InvalidCountryCode)
245 );
246 }
247
248 #[test]
250 fn test_with_whitespace() {
251 assert!(validate("DE89 3704 0044 0532 0130 00").is_ok());
253 }
254
255 #[test]
256 fn test_invalid_characters() {
257 assert_eq!(
258 validate("DE89.37040044053201300"),
259 Err(ValidationError::InvalidCharacter {
260 character: '.',
261 position: 4,
262 })
263 );
264 }
265
266 #[test]
267 fn test_invalid_characters_later() {
268 assert_eq!(
269 validate("GB82WEST1234569876543!"),
270 Err(ValidationError::InvalidCharacter {
271 character: '!',
272 position: 21,
273 })
274 );
275 }
276
277 #[test]
279 fn test_lowercase() {
280 assert!(validate("de89370400440532013000").is_ok());
281 }
282
283 #[test]
284 fn test_mixed_case() {
285 assert!(validate("De89370400440532013000").is_ok());
286 assert!(validate("gB82WEST12345698765432").is_ok());
287 }
288
289 #[test]
291 fn test_leading_whitespace() {
292 assert!(validate(" DE89370400440532013000").is_ok());
293 }
294
295 #[test]
296 fn test_trailing_whitespace() {
297 assert!(validate("DE89370400440532013000\n").is_ok());
298 }
299
300 #[test]
301 fn test_tabs_and_newlines() {
302 assert!(validate("DE89\t3704\n0044 0532\t0130\n00").is_ok());
303 }
304
305 #[test]
307 fn test_empty() {
308 assert_eq!(validate(""), Err(ValidationError::Empty));
309 }
310
311 #[test]
312 fn test_whitespace_only() {
313 assert_eq!(validate(" "), Err(ValidationError::Empty));
314 }
315
316 #[test]
317 fn test_tabs_only() {
318 assert_eq!(validate("\t\n"), Err(ValidationError::Empty));
319 }
320
321 #[test]
323 fn test_single_char() {
324 assert_eq!(
325 validate("D"),
326 Err(ValidationError::InvalidLength {
327 expected: 5,
328 found: 1,
329 })
330 );
331 }
332
333 #[test]
334 fn test_three_chars() {
335 assert_eq!(
336 validate("DE8"),
337 Err(ValidationError::InvalidLength {
338 expected: 5,
339 found: 3,
340 })
341 );
342 }
343
344 #[test]
345 fn test_country_code_only() {
346 assert_eq!(
347 validate("DE"),
348 Err(ValidationError::InvalidLength {
349 expected: 5,
350 found: 2,
351 })
352 );
353 }
354
355 #[test]
356 fn test_four_chars() {
357 assert_eq!(
358 validate("DE89"),
359 Err(ValidationError::InvalidLength {
360 expected: 5,
361 found: 4,
362 })
363 );
364 }
365
366 #[test]
368 fn test_iban_new_valid() {
369 let iban = Iban::new("DE89370400440532013000").unwrap();
370 assert_eq!(iban.as_str(), "DE89370400440532013000");
371 assert_eq!(iban.country_code(), "DE");
372 assert_eq!(iban.check_digits(), "89");
373 assert_eq!(iban.bban(), "370400440532013000");
374 }
375
376 #[test]
377 fn test_iban_new_invalid() {
378 assert!(Iban::new("DE89").is_err());
379 assert!(Iban::new("XX89370400440532013000").is_err());
380 assert!(Iban::new("DE89370400440532013001").is_err());
381 }
382
383 #[test]
384 fn test_iban_from_str() {
385 let iban: Iban = "DE89370400440532013000".parse().unwrap();
386 assert_eq!(iban.as_str(), "DE89370400440532013000");
387 }
388
389 #[test]
390 fn test_iban_display() {
391 let iban = Iban::new("DE89370400440532013000").unwrap();
392 assert_eq!(format!("{}", iban), "DE89370400440532013000");
393 }
394
395 #[test]
396 fn test_iban_equality() {
397 let iban1 = Iban::new("DE89370400440532013000").unwrap();
398 let iban2 = Iban::new("de89370400440532013000").unwrap();
399 assert_eq!(iban1, iban2);
400 }
401
402 #[test]
405 fn test_input_too_long() {
406 let long_input = "A".repeat(257);
407 assert_eq!(
408 validate(&long_input),
409 Err(ValidationError::InvalidLength {
410 expected: MAX_INPUT_LENGTH,
411 found: 257,
412 })
413 );
414 }
415
416 #[test]
417 fn test_input_at_max_length_with_whitespace() {
418 let iban = "DE89370400440532013000";
421 let with_ws = format!("{} {}", iban, " ".repeat(200));
422 assert!(validate(&with_ws).is_ok());
423 }
424
425 #[test]
426 fn test_clone_validation_error() {
427 let err = ValidationError::InvalidChecksum;
428 let cloned = err.clone();
429 assert_eq!(err, cloned);
430 }
431
432 #[test]
433 fn test_iban_clone() {
434 let iban1 = Iban::new("DE89370400440532013000").unwrap();
435 let iban2 = iban1.clone();
436 assert_eq!(iban1, iban2);
437 }
438
439 #[test]
440 fn test_all_countries_basic() {
441 assert!(validate("DE89370400440532013000").is_ok());
443 assert!(validate("GB82WEST12345698765432").is_ok());
444 assert!(validate("FR1420041010050500013M02606").is_ok());
445 assert!(validate("CH9300762011623852957").is_ok());
446 assert!(validate("NL91ABNA0417164300").is_ok());
447 assert!(validate("BE71096123456769").is_ok());
448 }
449}