iban/base_iban.rs
1use crate::IbanLike;
2#[cfg(doc)]
3use crate::{Iban, ParseIbanError};
4use arrayvec::ArrayString;
5use core::fmt::{self, Debug, Display};
6use core::str::FromStr;
7use core::{convert::TryFrom, error::Error};
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Deserializer, Serialize, Serializer};
10
11/// The size of a group of characters in the paper format.
12const PAPER_GROUP_SIZE: usize = 4;
13
14/// The maximum length an IBAN can be, according to the spec. This variable is
15/// used for the capacity of the arrayvec, which in turn determines how long a
16/// valid IBAN can be.
17const MAX_IBAN_LEN: usize = 34;
18
19/// Represents an IBAN that passed basic checks, but not necessarily the BBAN
20/// validation. This corresponds to the validation as described in ISO 13616-1.
21///
22/// To be exact, the IBAN...
23/// - must start with two uppercase ASCII letters, followed
24/// by two digits, followed by any number of digits and ASCII
25/// letters.
26/// - must have a valid checksum.
27/// - must contain no whitespace, or be in the paper format, where
28/// characters are in space-separated groups of four.
29///
30/// Note that most useful methods are supplied by the trait [`IbanLike`](crate::IbanLike). The [`Display`](fmt::Display) trait provides pretty
31/// print formatting.
32///
33/// A [`BaseIban`] does not enforce the country specific BBAN format as
34/// described in the Swift registry. In most cases, you probably want to use
35/// an [`Iban`] instead, which additionally does country specific validation.
36/// When parsing an [`Iban`] fails, the [`ParseIbanError`] will contain the
37/// [`BaseIban`] if it was valid.
38///
39/// # Examples
40/// An example of parsing and using a correct IBAN:
41/// ```rust
42/// use iban::{BaseIban, IbanLike};
43/// # use iban::ParseBaseIbanError;
44///
45/// let iban: BaseIban = "MR13 0002 0001 0100 0012 3456 753".parse()?;
46/// assert_eq!(iban.electronic_str(), "MR1300020001010000123456753");
47/// // The pretty print 'paper' format
48/// assert_eq!(iban.to_string(), "MR13 0002 0001 0100 0012 3456 753");
49/// assert_eq!(iban.country_code(), "MR");
50/// assert_eq!(iban.check_digits_str(), "13");
51/// assert_eq!(iban.check_digits(), 13);
52/// assert_eq!(iban.bban_unchecked(), "00020001010000123456753");
53/// # Ok::<(), ParseBaseIbanError>(())
54/// ```
55///
56/// An example of parsing invalid IBANs:
57/// ```rust
58/// use iban::{BaseIban, ParseBaseIbanError};
59///
60/// assert_eq!(
61/// "MR$$".parse::<BaseIban>(),
62/// Err(ParseBaseIbanError::InvalidFormat)
63/// );
64///
65/// assert_eq!(
66/// "MR0000020001010000123456754".parse::<BaseIban>(),
67/// Err(ParseBaseIbanError::InvalidChecksum)
68/// );
69/// ```
70///
71/// ## Formatting
72/// The IBAN specification describes two formats: an electronic format without
73/// whitespace and a paper format which seperates the IBAN in groups of
74/// four characters. Both will be parsed correctly by this crate. When
75/// formatting, [`Debug`] can be used to output the former and [`Display`] for
76/// the latter. This is true for a [`BaseIban`] as well as an [`Iban`].
77/// Alternatively, you can use [`IbanLike::electronic_str`] to obtain the
78/// electronic format as a string slice.
79/// ```
80/// # use iban::ParseBaseIbanError;
81/// let iban: iban::BaseIban = "RO66BACX0000001234567890".parse()?;
82/// // Use Debug for the electronic format.
83/// assert_eq!(&format!("{:?}", iban), "RO66BACX0000001234567890");
84/// // Use Display for the paper format.
85/// assert_eq!(&format!("{}", iban), "RO66 BACX 0000 0012 3456 7890");
86/// # Ok::<(), ParseBaseIbanError>(())
87/// ```
88#[derive(Copy, Clone, Eq, PartialEq, Hash)]
89pub struct BaseIban {
90 /// The string representing the IBAN. The string contains only uppercase
91 /// ASCII and digits and no whitespace. It starts with two letters followed
92 /// by two digits.
93 s: ArrayString<MAX_IBAN_LEN>,
94}
95
96#[cfg(feature = "serde")]
97impl Serialize for BaseIban {
98 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
99 if serializer.is_human_readable() {
100 serializer.collect_str(self)
101 } else {
102 serializer.serialize_str(self.electronic_str())
103 }
104 }
105}
106
107#[cfg(feature = "serde")]
108impl<'de> Deserialize<'de> for BaseIban {
109 #[must_use]
110 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
111 struct IbanStringVisitor;
112 use serde::de;
113
114 impl<'vi> de::Visitor<'vi> for IbanStringVisitor {
115 type Value = BaseIban;
116
117 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(formatter, "an IBAN string")
119 }
120
121 fn visit_str<E: de::Error>(self, value: &str) -> Result<BaseIban, E> {
122 value.parse::<BaseIban>().map_err(E::custom)
123 }
124 }
125
126 deserializer.deserialize_str(IbanStringVisitor)
127 }
128}
129
130impl IbanLike for BaseIban {
131 #[inline]
132 #[must_use]
133 fn electronic_str(&self) -> &str {
134 self.s.as_str()
135 }
136}
137
138impl Debug for BaseIban {
139 #[inline]
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 Display::fmt(&self.s, f)
142 }
143}
144
145impl Display for BaseIban {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147 for c in self.s.chars().enumerate().flat_map(|(i, c)| {
148 // Add a space before a character if it is the start of a group of four.
149 if i != 0 && i % PAPER_GROUP_SIZE == 0 {
150 Some(' ')
151 } else {
152 None
153 }
154 .into_iter()
155 .chain(core::iter::once(c))
156 }) {
157 write!(f, "{c}")?;
158 }
159 Ok(())
160 }
161}
162
163/// Indicates that the string does not follow the basic IBAN rules.
164///
165/// # Example
166/// An example of parsing invalid IBANs:
167/// ```rust
168/// use iban::{BaseIban, ParseBaseIbanError};
169///
170/// // Invalid formatting because the spaces are in the wrong places
171/// assert_eq!(
172/// "MR0 041 9".parse::<BaseIban>(),
173/// Err(ParseBaseIbanError::InvalidFormat)
174/// );
175///
176/// // This IBAN follows the correct basic format but has an invalid checksum
177/// assert_eq!(
178/// "MR00 0002 0001 0100 0012 3456 754".parse::<BaseIban>(),
179/// Err(ParseBaseIbanError::InvalidChecksum)
180/// );
181/// ```
182#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
183#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
184pub enum ParseBaseIbanError {
185 /// The string doesn't have the correct format to be an IBAN. This can be because it's too
186 /// short, too long or because it contains unexpected characters at some location.
187 InvalidFormat,
188 /// The IBAN has an invalid structure.
189 InvalidChecksum,
190}
191
192impl fmt::Display for ParseBaseIbanError {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 write!(
195 f,
196 "{}",
197 match self {
198 ParseBaseIbanError::InvalidFormat =>
199 "the string doesn't conform to the IBAN format",
200 ParseBaseIbanError::InvalidChecksum => "the IBAN has an invalid checksum",
201 }
202 )
203 }
204}
205
206impl Error for ParseBaseIbanError {}
207
208impl BaseIban {
209 /// Compute the checksum for the address. The code that the string contains
210 /// only valid characters: `'0'..='9'` and `'A'..='Z'`.
211 #[must_use]
212 fn validate_checksum(address: &str) -> bool {
213 address
214 .as_bytes()
215 .iter()
216 // Move the first four characters to the back
217 .cycle()
218 .skip(4)
219 .take(address.len())
220 // Calculate the checksum
221 .fold(0_u16, |acc, &c| {
222 const MASK_DIGIT: u8 = 0b0010_0000;
223
224 debug_assert!(char::from(c).is_digit(36), "An address was supplied to compute_checksum with an invalid \
225 character. Please file an issue at \
226 https://github.com/ThomasdenH/iban_validate.");
227
228 // We expect only '0'-'9' and 'A'-'Z', so we can use a mask for
229 // faster testing.
230 (if c & MASK_DIGIT != 0 {
231 // '0' - '9'. We should multiply the accumulator by 10 and
232 // add this value.
233 (acc * 10) + u16::from(c - b'0')
234 } else {
235 // 'A' - 'Z'. We should multiply the accumulator by 100 and
236 // add this value.
237 // Note: We can multiply by (100 % 97) = 3 instead. This
238 // doesn't impact performance though, so or simplicity we
239 // use 100.
240 (acc * 100) + u16::from(c - b'A' + 10)
241 }) % 97
242 })
243 == 1 &&
244 // Check digits with value 01 or 00 are invalid!
245 &address[2..4] != "00" &&
246 &address[2..4] != "01"
247 }
248
249 /// Parse a standardized IBAN string from an iterator. We iterate through
250 /// bytes, not characters. When a character is not ASCII, the IBAN is
251 /// automatically invalid.
252 fn try_form_string_from_electronic<T>(
253 mut chars: T,
254 ) -> Result<ArrayString<MAX_IBAN_LEN>, ParseBaseIbanError>
255 where
256 T: Iterator<Item = u8>,
257 {
258 let mut address_no_spaces = ArrayString::<MAX_IBAN_LEN>::new();
259
260 // First expect exactly two uppercase letters and append them to the
261 // string.
262 for _ in 0..2 {
263 let c = chars
264 .next()
265 .filter(u8::is_ascii_uppercase)
266 .ok_or(ParseBaseIbanError::InvalidFormat)?;
267 address_no_spaces
268 .try_push(c as char)
269 .map_err(|_| ParseBaseIbanError::InvalidFormat)?;
270 }
271
272 // Now expect exactly two digits.
273 for _ in 0..2 {
274 let c = chars
275 .next()
276 .filter(u8::is_ascii_digit)
277 .ok_or(ParseBaseIbanError::InvalidFormat)?;
278 address_no_spaces
279 .try_push(c as char)
280 .map_err(|_| ParseBaseIbanError::InvalidFormat)?;
281 }
282
283 // Finally take up to 30 other characters. The BBAN part can actually
284 // be both lower or upper case, but we normalize it to uppercase here.
285 // The number of characters is limited by the capacity of the
286 // destination string.
287 for c in chars {
288 if c.is_ascii_alphanumeric() {
289 address_no_spaces
290 .try_push(c.to_ascii_uppercase() as char)
291 .map_err(|_| ParseBaseIbanError::InvalidFormat)?;
292 } else {
293 return Err(ParseBaseIbanError::InvalidFormat);
294 }
295 }
296
297 Ok(address_no_spaces)
298 }
299
300 /// Parse a pretty print 'paper' IBAN from a `str`.
301 fn try_form_string_from_pretty_print(
302 s: &str,
303 ) -> Result<ArrayString<MAX_IBAN_LEN>, ParseBaseIbanError> {
304 // The pretty print format consists of a number of groups of four
305 // characters, separated by a space.
306
307 let bytes = s.as_bytes();
308
309 // If the number of bytes of a printed IBAN is divisible by 5, then it
310 // means that the last character should be a space, but this is
311 // invalid. If it is not, then the last character is a character that
312 // appears in the IBAN.
313 if bytes.len() % (PAPER_GROUP_SIZE + 1) == 0 {
314 return Err(ParseBaseIbanError::InvalidFormat);
315 }
316
317 // We check that every fifth character is a space, knowing already that
318 // account number ends with a character that appears in the IBAN.
319 if bytes
320 .chunks_exact(PAPER_GROUP_SIZE + 1)
321 .any(|chunk| chunk[PAPER_GROUP_SIZE] != b' ')
322 {
323 return Err(ParseBaseIbanError::InvalidFormat);
324 }
325
326 // Every character that is not in a position that is a multiple of 5
327 // + 1 should appear in the IBAN. We thus filter out every fifth
328 // character and check whether that constitutes a valid IBAN.
329 BaseIban::try_form_string_from_electronic(
330 bytes
331 .iter()
332 .enumerate()
333 .filter_map(|(i, c)| if i % 5 != 4 { Some(c) } else { None })
334 .copied(),
335 )
336 }
337}
338
339impl FromStr for BaseIban {
340 type Err = ParseBaseIbanError;
341 /// Parse a basic iban without taking the BBAN into consideration.
342 ///
343 /// # Errors
344 /// If the string does not match the IBAN format or the checksum is
345 /// invalid, an [`ParseBaseIbanError`](crate::ParseBaseIbanError) will be
346 /// returned.
347 fn from_str(address: &str) -> Result<Self, Self::Err> {
348 let address_no_spaces =
349 BaseIban::try_form_string_from_electronic(address.as_bytes().iter().copied())
350 .or_else(|_| BaseIban::try_form_string_from_pretty_print(address))?;
351
352 if !BaseIban::validate_checksum(&address_no_spaces) {
353 return Err(ParseBaseIbanError::InvalidChecksum);
354 }
355
356 Ok(BaseIban {
357 s: address_no_spaces,
358 })
359 }
360}
361
362impl<'a> TryFrom<&'a str> for BaseIban {
363 type Error = ParseBaseIbanError;
364 /// Parse a basic IBAN without taking the BBAN into consideration.
365 ///
366 /// # Errors
367 /// If the string does not match the IBAN format or the checksum is
368 /// invalid, an [`ParseBaseIbanError`](crate::ParseBaseIbanError) will be
369 /// returned.
370 #[inline]
371 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
372 value.parse()
373 }
374}