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}