├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── fmt_and_clippy.yml │ ├── generation_code.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE_MIT ├── README.md ├── contributing.md ├── iban_validate ├── Cargo.toml ├── README.md ├── benches │ └── iban_benchmark.rs ├── src │ ├── base_iban.rs │ ├── countries.rs │ ├── generated.rs │ └── lib.rs └── tests │ ├── as_ref.rs │ ├── format.rs │ ├── impls.rs │ ├── parse.rs │ ├── proptest.rs │ ├── registry_examples.rs │ ├── registry_examples_generated.rs │ ├── serde.rs │ ├── split.rs │ └── validate_country.rs ├── iban_validate_afl_fuzz ├── Cargo.toml ├── Readme.md ├── inputs │ ├── input │ └── wikipedia └── src │ └── main.rs └── iban_validate_registry_generation ├── Cargo.toml ├── README.md └── src └── main.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | 8 | [*.rs] 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.yml] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "07:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: afl 11 | versions: 12 | - 0.8.0 13 | -------------------------------------------------------------------------------- /.github/workflows/fmt_and_clippy.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: Fmt & Clippy 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | profile: minimal 13 | toolchain: stable 14 | override: true 15 | components: rustfmt, clippy 16 | 17 | # Clippy & Fmt 18 | - uses: actions-rs/cargo@v1 19 | with: 20 | command: fmt 21 | args: --all -- --check 22 | 23 | - uses: actions-rs/cargo@v1 24 | with: 25 | command: clippy 26 | args: -- -D warnings 27 | -------------------------------------------------------------------------------- /.github/workflows/generation_code.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: Continuous integration (Generation code) 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | profile: minimal 13 | toolchain: stable 14 | override: true 15 | 16 | # Generation code 17 | - uses: actions-rs/cargo@v1 18 | with: 19 | command: build 20 | args: --manifest-path iban_validate_registry_generation/Cargo.toml 21 | 22 | - uses: actions-rs/cargo@v1 23 | with: 24 | command: test 25 | args: --manifest-path iban_validate_registry_generation/Cargo.toml 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: Continuous integration (iban_validate) 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | rust: 11 | - stable 12 | - beta 13 | - nightly 14 | features: 15 | - "" 16 | - --no-default-features 17 | - "--no-default-features --features serde" 18 | - "--features serde" 19 | command: 20 | - build 21 | - test 22 | - doc 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - uses: actions-rs/toolchain@v1 28 | with: 29 | profile: minimal 30 | toolchain: ${{ matrix.rust }} 31 | override: true 32 | components: rustfmt, clippy 33 | 34 | # Default features 35 | - uses: actions-rs/cargo@v1 36 | with: 37 | command: ${{ matrix.command }} 38 | args: ${{ matrix.features }} --manifest-path iban_validate/Cargo.toml 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | **/*.bk 3 | .idea/ 4 | Cargo.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 5.1.0 2 | - Add implementations of `AsRef` and `AsMut` 3 | - Enable all features on `docs.rs` 4 | - Remove now default `intra_rustdoc_links` feature flag 5 | 6 | # 5.0.1 7 | - Update registry to latest version: Release 98. 8 | - Update country specific generation code. Inconsistencies are now resolved manually when generating the country specific code. Incorrect examples are now fixed before testing where they were previously ignored. 9 | - For "JO" (Jordan), we now return the bank branch. The registry provides an example but no range so this seems to be the best solution. 10 | - Use `#[inline]` and `#[must_use]` where appropriate 11 | 12 | # 5.0.0 13 | This edition is technically breaking since it removes the `std` feature and implements `Error` from `core` instead of `std`. 14 | 15 | - Update registry to latest version. 16 | - Implement `Error` from `core` instead of `std`. 17 | - Remove feature `std`. 18 | - Update edition. 19 | - Update documentation links. 20 | 21 | # 4.0.1 22 | This version generated country-specific code automatically from the IBAN registry. It also tests all examples that can be tested. The registry is somewhat messy and inconsistent actually, and as such still requires quite some manual effort. Sometimes the registry is plainly incorrect and national documentation was used instead. This means that the crate now performs better than some of the online tools I've tested. 23 | 24 | - Fix some country-specific code. 25 | - Test for compliance with all applicable examples in the registry. 26 | - Update to the newest IBAN registry (October 2021). 27 | - Drop minimal supported version. (The minimal supported version was already increased by dependencies, so from now on just the latest stable is guaranteed.) 28 | 29 | # 4.0.0 30 | 31 | This version overhauls the internal parsing code entirely. It removes its dependency on `regex`, which allows the crate to be run in `no_std` mode. It also speeds up parsing by a factor of about 100. The implemented changes: 32 | 33 | - _Breaking_: Added a feature (enabled by default) `std`, which enables usage of the standard library. Other than the lack of `std::error::Error`, there are no restrictions on `no_std` mode. 34 | - Follow the standard more closely: allow lowercase characters in the BBAN position but normalize them, and disallow `00` or `01` as check characters. 35 | - Updated to follow latest IBAN spec. 36 | - Added sub crate in workspace for fuzzing with AFL. 37 | - Many functions are a lot faster. 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | 4 | members = [ 5 | "iban_validate", 6 | "iban_validate_afl_fuzz", 7 | "iban_validate_registry_generation" 8 | ] 9 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE_MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iban_validate 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/iban_validate.svg)](https://crates.io/crates/iban_validate) 4 | ![test](https://github.com/ThomasdenH/iban_validate/actions/workflows/test.yml/badge.svg) 5 | ![Generation code](https://github.com/ThomasdenH/iban_validate/actions/workflows/generation_code.yml/badge.svg) 6 | ![fmt & clippy](https://github.com/ThomasdenH/iban_validate/actions/workflows/fmt_and_clippy.yml/badge.svg) 7 | 8 | This crate provides an easy way to validate an IBAN (International Bank Account Number). To do so, you can use the function [`parse()`](https://doc.rust-lang.org/stable/std/primitive.str.html#method.parse). This will check the IBAN rules as well as the BBAN structure. The provided [`Iban`] structure provides many methods to easy the handling of an IBAN. Many of these methods are provided via the [`IbanLike`](https://docs.rs/iban_validate/5.0.1/iban/trait.IbanLike.html) trait. 9 | 10 | When BBAN parsing fails, the error type [`ParseIbanError`](https://docs.rs/iban_validate/5.0.1/iban/enum.ParseIbanError.html) provides useful information about what went wrong. Additionally, the error contains [`BaseIban`], which can still be used to access useful information. 11 | 12 | ## Example 13 | 14 | The following example does a full validation of the IBAN and BBAN format. 15 | 16 | ```rust 17 | use iban::*; 18 | 19 | fn main() -> Result<(), ParseIbanError> { 20 | let account = "DE44500105175407324931".parse::()?; 21 | assert_eq!(account.country_code(), "DE"); 22 | assert_eq!(account.check_digits(), 44); 23 | assert_eq!(account.bban(), "500105175407324931"); 24 | // The electronic format is also returned through `Debug::fmt` 25 | assert_eq!(account.electronic_str(), "DE44500105175407324931"); 26 | // The pretty 'paper' format can be obtained via `Display::fmt` 27 | assert_eq!(account.to_string(), "DE44 5001 0517 5407 3249 31"); 28 | assert_eq!(account.bank_identifier(), Some("50010517")); 29 | assert_eq!(account.branch_identifier(), None); 30 | Ok(()) 31 | } 32 | ``` 33 | 34 | ## What does this library provide? 35 | 36 | - A [`Iban`] type that can be used to parse account numbers very quickly. It doesn't require allocations at all, and instead leverages [`arrayvec`](https://crates.io/crates/arrayvec) under the hood. 37 | - A flexible API that is useful even when the country is not in the Swift registry (using [`BaseIban`]). Instead of using panic, the crate provides typed errors with what went wrong. 38 | - All functionality can be used in a `no_std` environment. 39 | - Optional serialization and deserialization via [`serde`](https://crates.io/crates/serde). 40 | - CI tested results via the Swift provided and custom test cases, as well as proptest. 41 | - `#![forbid(unsafe_code)]`, making sure all code is written in safe Rust. 42 | 43 | ## Usage 44 | 45 | The crate can be found on [crates.io](https://crates.io/crates/iban_validate). To use this crate, just add it as an 46 | dependency: 47 | 48 | ```toml 49 | [dependencies] 50 | iban_validate = "5" 51 | ``` 52 | 53 | ## Features 54 | 55 | The following features can be used to configure the crate: 56 | 57 | - _serde_: Enable `serde` support for [`Iban`] and [`BaseIban`]. 58 | 59 | ## Contributing 60 | 61 | If you experience issues with this crate or want to help, please look [here](https://github.com/ThomasdenH/iban_validate/blob/master/contributing.md). 62 | 63 | ## Stability 64 | 65 | This crate is usable on the latest stable release of the Rust compiler and adheres to semver. The IBAN registry may be updated with patch releases, because of this results may differ even between patch versions. 66 | 67 | ## License 68 | 69 | Licensed under either of 70 | 71 | - Apache License, Version 2.0 72 | ([LICENSE-APACHE](https://github.com/ThomasdenH/iban_validate/blob/master/LICENSE-APACHE) or ) 73 | - MIT license 74 | ([LICENSE-MIT](https://github.com/ThomasdenH/iban_validate/blob/master/LICENSE-MIT) or ) 75 | 76 | at your option. 77 | 78 | ### Contribution 79 | 80 | Unless you explicitly state otherwise, any contribution intentionally submitted 81 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 82 | dual licensed as above, without any additional terms or conditions. 83 | 84 | [`iban`]: https://docs.rs/iban_validate/5.0.1/iban/struct.Iban.html 85 | [`baseiban`]: https://docs.rs/iban_validate/5.0.1/iban/struct.BaseIban.html 86 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome. This is a guide about the process. 4 | 5 | ## Issues 6 | 7 | If you encounter a bug, miss a feature or you have a question regarding this library, please don't hesitate to create an issue on 8 | GitHub. To make things easier, include all information relevant to the issue. 9 | 10 | ## Code 11 | 12 | If you want to add or fix functionality in this library, you can create a pull request. The library follows the [Rust API guidelines](https://github.com/brson/rust-api-guidelines) as 13 | much as possible. The code is being tested on both Travis CI and Appveyor. As part of testing, 14 | [rustfmt](https://github.com/rust-lang-nursery/rustfmt) is ran to check if the code follows style guidelines. The 15 | easiest way to prevent these tests from failing is to use `cargo fmt` before a commit. 16 | -------------------------------------------------------------------------------- /iban_validate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "iban_validate" 3 | # Note: When updating, also update in lib.rs and README.md 4 | version = "5.0.1" 5 | authors = ["Thomas den Hollander "] 6 | description = "A small crate to verify IBAN account numbers." 7 | repository = "https://github.com/ThomasdenH/iban_check" 8 | license = "MIT OR Apache-2.0" 9 | readme = "README.md" 10 | keywords = ["iban", "iban-validator"] 11 | categories = ["parsing"] 12 | edition = "2021" 13 | include = ["src/**/*", "README.md"] 14 | 15 | [badges] 16 | maintenance = { status = "passively-maintained" } 17 | 18 | [lib] 19 | name = "iban" 20 | path = "src/lib.rs" 21 | 22 | [features] 23 | default = [] 24 | 25 | # Enables all features when building documentation 26 | [package.metadata.docs.rs] 27 | features = ["serde"] 28 | 29 | [dependencies.serde] 30 | version = "1" 31 | optional = true 32 | default-features = false 33 | features = ["derive"] 34 | 35 | [dependencies.arrayvec] 36 | version = "0.7" 37 | default-features = false 38 | 39 | [dev-dependencies] 40 | proptest = "1" 41 | static_assertions = "1" 42 | serde_test = "1" 43 | criterion = "0.3" 44 | 45 | [[bench]] 46 | name = "iban_benchmark" 47 | path = "benches/iban_benchmark.rs" 48 | harness = false 49 | -------------------------------------------------------------------------------- /iban_validate/README.md: -------------------------------------------------------------------------------- 1 | # `iban_validate` 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/iban_validate.svg)](https://crates.io/crates/iban_validate) 4 | ![test](https://github.com/ThomasdenH/iban_validate/actions/workflows/test.yml/badge.svg) 5 | ![Generation code](https://github.com/ThomasdenH/iban_validate/actions/workflows/generation_code.yml/badge.svg) 6 | ![fmt & clippy](https://github.com/ThomasdenH/iban_validate/actions/workflows/fmt_and_clippy.yml/badge.svg) 7 | 8 | This crate provides an easy way to validate an IBAN (International Bank Account Number). To do so, you can use the function [`parse()`](https://doc.rust-lang.org/stable/std/primitive.str.html#method.parse). This will check the IBAN rules as well as the BBAN structure. The provided [`Iban`] structure provides many methods to easy the handling of an IBAN. Many of these methods are provided via the [`IbanLike`](https://docs.rs/iban_validate/5.0.1/iban/trait.IbanLike.html) trait. 9 | 10 | When BBAN parsing fails, the error type [`ParseIbanError`](https://docs.rs/iban_validate/5.0.1/iban/enum.ParseIbanError.html) provides useful information about what went wrong. Additionally, the error contains [`BaseIban`], which can still be used to access useful information. 11 | 12 | ## Example 13 | 14 | The following example does a full validation of the IBAN and BBAN format. 15 | 16 | ```rust 17 | use iban::*; 18 | 19 | fn main() -> Result<(), ParseIbanError> { 20 | let account = "DE44500105175407324931".parse::()?; 21 | assert_eq!(account.country_code(), "DE"); 22 | assert_eq!(account.check_digits(), 44); 23 | assert_eq!(account.bban(), "500105175407324931"); 24 | // The electronic format is also returned through `Debug::fmt` 25 | assert_eq!(account.electronic_str(), "DE44500105175407324931"); 26 | // The pretty 'paper' format can be obtained via `Display::fmt` 27 | assert_eq!(account.to_string(), "DE44 5001 0517 5407 3249 31"); 28 | assert_eq!(account.bank_identifier(), Some("50010517")); 29 | assert_eq!(account.branch_identifier(), None); 30 | Ok(()) 31 | } 32 | ``` 33 | 34 | ## What does this library provide? 35 | 36 | - A [`Iban`] type that can be used to parse account numbers very quickly. It doesn't require allocations at all, and instead leverages [`arrayvec`](https://crates.io/crates/arrayvec) under the hood. 37 | - A flexible API that is useful even when the country is not in the Swift registry (using [`BaseIban`]). Instead of using panic, the crate provides typed errors with what went wrong. 38 | - All functionality can be used in a `no_std` environment. 39 | - Optional serialization and deserialization via [`serde`](https://crates.io/crates/serde). 40 | - CI tested results via the Swift provided and custom test cases, as well as proptest. 41 | - `#![forbid(unsafe_code)]`, making sure all code is written in safe Rust. 42 | 43 | ## Usage 44 | 45 | The crate can be found on [crates.io](https://crates.io/crates/iban_validate). To use this crate, just add it as an 46 | dependency: 47 | 48 | ```toml 49 | [dependencies] 50 | iban_validate = "5" 51 | ``` 52 | 53 | ## Features 54 | 55 | The following features can be used to configure the crate: 56 | 57 | - _serde_: Enable `serde` support for [`Iban`] and [`BaseIban`]. 58 | 59 | ## Contributing 60 | 61 | If you experience issues with this crate or want to help, please look [here](https://github.com/ThomasdenH/iban_validate/blob/master/contributing.md). 62 | 63 | ## Stability 64 | 65 | This crate is usable on the latest stable release of the Rust compiler and adheres to semver. The IBAN registry may be updated with patch releases, because of this results may differ even between patch versions. 66 | 67 | ## License 68 | 69 | Licensed under either of 70 | 71 | - Apache License, Version 2.0 72 | ([LICENSE-APACHE](https://github.com/ThomasdenH/iban_validate/blob/master/LICENSE-APACHE) or ) 73 | - MIT license 74 | ([LICENSE-MIT](https://github.com/ThomasdenH/iban_validate/blob/master/LICENSE-MIT) or ) 75 | 76 | at your option. 77 | 78 | ### Contribution 79 | 80 | Unless you explicitly state otherwise, any contribution intentionally submitted 81 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 82 | dual licensed as above, without any additional terms or conditions. 83 | 84 | [`iban`]: https://docs.rs/iban_validate/5.0.1/iban/struct.Iban.html 85 | [`baseiban`]: https://docs.rs/iban_validate/5.0.1/iban/struct.BaseIban.html 86 | -------------------------------------------------------------------------------- /iban_validate/benches/iban_benchmark.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | use iban::Iban; 3 | use std::str::FromStr; 4 | 5 | pub fn criterion_benchmark(c: &mut Criterion) { 6 | let iban_str = "DE44500105175407324931"; 7 | c.bench_function(iban_str, |b| b.iter(|| Iban::from_str(black_box(iban_str)))); 8 | } 9 | 10 | pub fn criterion_benchmark_with_spaces(c: &mut Criterion) { 11 | let iban_str = "LV80 BANK 0000 4351 9500 1"; 12 | c.bench_function(iban_str, |b| b.iter(|| Iban::from_str(black_box(iban_str)))); 13 | } 14 | 15 | pub fn display_benchmark(c: &mut Criterion) { 16 | let iban = Iban::from_str(black_box("DE44500105175407324931")).unwrap(); 17 | c.bench_function("iban display", |b| b.iter(|| black_box(iban).to_string())); 18 | } 19 | 20 | pub fn display_with_spaces(c: &mut Criterion) { 21 | let iban = Iban::from_str(black_box("LV80 BANK 0000 4351 9500 1")).unwrap(); 22 | c.bench_function("iban display #2", |b| { 23 | b.iter(|| black_box(iban).to_string()); 24 | }); 25 | } 26 | 27 | criterion_group!( 28 | benches, 29 | criterion_benchmark, 30 | criterion_benchmark_with_spaces, 31 | display_benchmark, 32 | display_with_spaces 33 | ); 34 | criterion_main!(benches); 35 | -------------------------------------------------------------------------------- /iban_validate/src/base_iban.rs: -------------------------------------------------------------------------------- 1 | use crate::IbanLike; 2 | #[cfg(doc)] 3 | use crate::{Iban, ParseIbanError}; 4 | use arrayvec::ArrayString; 5 | use core::fmt::{self, Debug, Display}; 6 | use core::str::FromStr; 7 | use core::{convert::TryFrom, error::Error}; 8 | #[cfg(feature = "serde")] 9 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 10 | 11 | /// The size of a group of characters in the paper format. 12 | const 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. 17 | const 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::(), 62 | /// Err(ParseBaseIbanError::InvalidFormat) 63 | /// ); 64 | /// 65 | /// assert_eq!( 66 | /// "MR0000020001010000123456754".parse::(), 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)] 89 | pub 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, 94 | } 95 | 96 | #[cfg(feature = "serde")] 97 | impl Serialize for BaseIban { 98 | fn serialize(&self, serializer: S) -> Result { 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")] 108 | impl<'de> Deserialize<'de> for BaseIban { 109 | #[must_use] 110 | fn deserialize>(deserializer: D) -> Result { 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(self, value: &str) -> Result { 122 | value.parse::().map_err(E::custom) 123 | } 124 | } 125 | 126 | deserializer.deserialize_str(IbanStringVisitor) 127 | } 128 | } 129 | 130 | impl IbanLike for BaseIban { 131 | #[inline] 132 | #[must_use] 133 | fn electronic_str(&self) -> &str { 134 | self.s.as_str() 135 | } 136 | } 137 | 138 | impl Debug for BaseIban { 139 | #[inline] 140 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 141 | Display::fmt(&self.s, f) 142 | } 143 | } 144 | 145 | impl 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::(), 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::(), 179 | /// Err(ParseBaseIbanError::InvalidChecksum) 180 | /// ); 181 | /// ``` 182 | #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] 183 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 184 | pub 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 | 192 | impl 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 | 206 | impl Error for ParseBaseIbanError {} 207 | 208 | impl AsRef for ParseBaseIbanError { 209 | fn as_ref(&self) -> &ParseBaseIbanError { 210 | self 211 | } 212 | } 213 | 214 | impl AsMut for ParseBaseIbanError { 215 | fn as_mut(&mut self) -> &mut ParseBaseIbanError { 216 | self 217 | } 218 | } 219 | 220 | impl BaseIban { 221 | /// Compute the checksum for the address. The code that the string contains 222 | /// only valid characters: `'0'..='9'` and `'A'..='Z'`. 223 | #[must_use] 224 | fn validate_checksum(address: &str) -> bool { 225 | address 226 | .as_bytes() 227 | .iter() 228 | // Move the first four characters to the back 229 | .cycle() 230 | .skip(4) 231 | .take(address.len()) 232 | // Calculate the checksum 233 | .fold(0_u16, |acc, &c| { 234 | const MASK_DIGIT: u8 = 0b0010_0000; 235 | 236 | debug_assert!(char::from(c).is_digit(36), "An address was supplied to compute_checksum with an invalid \ 237 | character. Please file an issue at \ 238 | https://github.com/ThomasdenH/iban_validate."); 239 | 240 | // We expect only '0'-'9' and 'A'-'Z', so we can use a mask for 241 | // faster testing. 242 | (if c & MASK_DIGIT != 0 { 243 | // '0' - '9'. We should multiply the accumulator by 10 and 244 | // add this value. 245 | (acc * 10) + u16::from(c - b'0') 246 | } else { 247 | // 'A' - 'Z'. We should multiply the accumulator by 100 and 248 | // add this value. 249 | // Note: We can multiply by (100 % 97) = 3 instead. This 250 | // doesn't impact performance though, so or simplicity we 251 | // use 100. 252 | (acc * 100) + u16::from(c - b'A' + 10) 253 | }) % 97 254 | }) 255 | == 1 && 256 | // Check digits with value 01 or 00 are invalid! 257 | &address[2..4] != "00" && 258 | &address[2..4] != "01" 259 | } 260 | 261 | /// Parse a standardized IBAN string from an iterator. We iterate through 262 | /// bytes, not characters. When a character is not ASCII, the IBAN is 263 | /// automatically invalid. 264 | fn try_form_string_from_electronic( 265 | mut chars: T, 266 | ) -> Result, ParseBaseIbanError> 267 | where 268 | T: Iterator, 269 | { 270 | let mut address_no_spaces = ArrayString::::new(); 271 | 272 | // First expect exactly two uppercase letters and append them to the 273 | // string. 274 | for _ in 0..2 { 275 | let c = chars 276 | .next() 277 | .filter(u8::is_ascii_uppercase) 278 | .ok_or(ParseBaseIbanError::InvalidFormat)?; 279 | address_no_spaces 280 | .try_push(c as char) 281 | .map_err(|_| ParseBaseIbanError::InvalidFormat)?; 282 | } 283 | 284 | // Now expect exactly two digits. 285 | for _ in 0..2 { 286 | let c = chars 287 | .next() 288 | .filter(u8::is_ascii_digit) 289 | .ok_or(ParseBaseIbanError::InvalidFormat)?; 290 | address_no_spaces 291 | .try_push(c as char) 292 | .map_err(|_| ParseBaseIbanError::InvalidFormat)?; 293 | } 294 | 295 | // Finally take up to 30 other characters. The BBAN part can actually 296 | // be both lower or upper case, but we normalize it to uppercase here. 297 | // The number of characters is limited by the capacity of the 298 | // destination string. 299 | for c in chars { 300 | if c.is_ascii_alphanumeric() { 301 | address_no_spaces 302 | .try_push(c.to_ascii_uppercase() as char) 303 | .map_err(|_| ParseBaseIbanError::InvalidFormat)?; 304 | } else { 305 | return Err(ParseBaseIbanError::InvalidFormat); 306 | } 307 | } 308 | 309 | Ok(address_no_spaces) 310 | } 311 | 312 | /// Parse a pretty print 'paper' IBAN from a `str`. 313 | fn try_form_string_from_pretty_print( 314 | s: &str, 315 | ) -> Result, ParseBaseIbanError> { 316 | // The pretty print format consists of a number of groups of four 317 | // characters, separated by a space. 318 | 319 | let bytes = s.as_bytes(); 320 | 321 | // If the number of bytes of a printed IBAN is divisible by 5, then it 322 | // means that the last character should be a space, but this is 323 | // invalid. If it is not, then the last character is a character that 324 | // appears in the IBAN. 325 | if bytes.len() % (PAPER_GROUP_SIZE + 1) == 0 { 326 | return Err(ParseBaseIbanError::InvalidFormat); 327 | } 328 | 329 | // We check that every fifth character is a space, knowing already that 330 | // account number ends with a character that appears in the IBAN. 331 | if bytes 332 | .chunks_exact(PAPER_GROUP_SIZE + 1) 333 | .any(|chunk| chunk[PAPER_GROUP_SIZE] != b' ') 334 | { 335 | return Err(ParseBaseIbanError::InvalidFormat); 336 | } 337 | 338 | // Every character that is not in a position that is a multiple of 5 339 | // + 1 should appear in the IBAN. We thus filter out every fifth 340 | // character and check whether that constitutes a valid IBAN. 341 | BaseIban::try_form_string_from_electronic( 342 | bytes 343 | .iter() 344 | .enumerate() 345 | .filter_map(|(i, c)| if i % 5 != 4 { Some(c) } else { None }) 346 | .copied(), 347 | ) 348 | } 349 | } 350 | 351 | impl FromStr for BaseIban { 352 | type Err = ParseBaseIbanError; 353 | /// Parse a basic iban without taking the BBAN into consideration. 354 | /// 355 | /// # Errors 356 | /// If the string does not match the IBAN format or the checksum is 357 | /// invalid, an [`ParseBaseIbanError`](crate::ParseBaseIbanError) will be 358 | /// returned. 359 | fn from_str(address: &str) -> Result { 360 | let address_no_spaces = 361 | BaseIban::try_form_string_from_electronic(address.as_bytes().iter().copied()) 362 | .or_else(|_| BaseIban::try_form_string_from_pretty_print(address))?; 363 | 364 | if !BaseIban::validate_checksum(&address_no_spaces) { 365 | return Err(ParseBaseIbanError::InvalidChecksum); 366 | } 367 | 368 | Ok(BaseIban { 369 | s: address_no_spaces, 370 | }) 371 | } 372 | } 373 | 374 | impl<'a> TryFrom<&'a str> for BaseIban { 375 | type Error = ParseBaseIbanError; 376 | /// Parse a basic IBAN without taking the BBAN into consideration. 377 | /// 378 | /// # Errors 379 | /// If the string does not match the IBAN format or the checksum is 380 | /// invalid, an [`ParseBaseIbanError`](crate::ParseBaseIbanError) will be 381 | /// returned. 382 | #[inline] 383 | fn try_from(value: &'a str) -> Result { 384 | value.parse() 385 | } 386 | } 387 | 388 | impl AsRef for BaseIban { 389 | fn as_ref(&self) -> &BaseIban { 390 | self 391 | } 392 | } 393 | 394 | impl AsMut for BaseIban { 395 | fn as_mut(&mut self) -> &mut BaseIban { 396 | self 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /iban_validate/src/countries.rs: -------------------------------------------------------------------------------- 1 | //! A module for parsing the BBAN structures from a definition. The format is 2 | //! very simple and can be optimized well by the compiler. 3 | 4 | /// A `CharacterType` can match a single character. This corresponds to the 5 | /// categories in the Swift registry for the most part, except that it doesn't 6 | /// allow lowercase characters for `c`. However, when parsing we have 7 | /// normalized the case anyway. 8 | #[derive(Copy, Clone)] 9 | pub(super) enum CharacterType { 10 | C, 11 | N, 12 | A, 13 | } 14 | 15 | impl CharacterType { 16 | #[inline] 17 | #[must_use] 18 | fn matches(self, c: u8) -> bool { 19 | use CharacterType::{A, C, N}; 20 | const MASK_CAPITAL: u8 = 0b0100_0000; 21 | const MASK_DIGIT: u8 = 0b0010_0000; 22 | debug_assert!(c.is_ascii_uppercase() || c.is_ascii_digit()); 23 | match self { 24 | A => (c & MASK_DIGIT) == 0, 25 | N => (c & MASK_CAPITAL) == 0, 26 | C => true, 27 | } 28 | } 29 | } 30 | 31 | pub(super) trait Matchable { 32 | fn match_str(self, s: &str) -> bool; 33 | } 34 | 35 | impl Matchable for &'_ [(usize, CharacterType)] { 36 | /// Check if the string matches the format. The format is a list of counts 37 | /// followed by their character type. For example, [(3, A) (2, N)] would 38 | /// mean three letters followed by two numbers. The string should also have 39 | /// the correct length. 40 | #[must_use] 41 | fn match_str(self, s: &str) -> bool { 42 | s.len() == len(self) 43 | && self 44 | .iter() 45 | .flat_map(|(count, character_type)| (0..*count).map(move |_| character_type)) 46 | .zip(s.as_bytes()) 47 | .all(|(character_type, c)| character_type.matches(*c)) 48 | } 49 | } 50 | 51 | #[inline] 52 | #[must_use] 53 | fn len(a: &[(usize, CharacterType)]) -> usize { 54 | a.iter().map(|(count, _)| count).sum() 55 | } 56 | -------------------------------------------------------------------------------- /iban_validate/src/generated.rs: -------------------------------------------------------------------------------- 1 | //! This file is automatically generated by `iban_validate_registry_generation` from the IBAN registry. 2 | 3 | /// Get the position of the bank in the BBAN. 4 | #[inline] 5 | pub(crate) fn bank_identifier(country_code: &str) -> Option> { 6 | #[allow(clippy::match_same_arms)] // For clarity, identical arms are not combined. 7 | match country_code { 8 | "AD" => Some(0..4), 9 | "AE" => Some(0..3), 10 | "AL" => Some(0..3), 11 | "AT" => Some(0..5), 12 | "AZ" => Some(0..4), 13 | "BA" => Some(0..3), 14 | "BE" => Some(0..3), 15 | "BG" => Some(0..4), 16 | "BH" => Some(0..4), 17 | "BI" => Some(0..5), 18 | "BR" => Some(0..8), 19 | "BY" => Some(0..4), 20 | "CH" => Some(0..5), 21 | "CR" => Some(0..4), 22 | "CY" => Some(0..3), 23 | "CZ" => Some(0..4), 24 | "DE" => Some(0..8), 25 | "DJ" => Some(0..5), 26 | "DK" => Some(0..4), 27 | "DO" => Some(0..4), 28 | "EE" => Some(0..2), 29 | "EG" => Some(0..4), 30 | "ES" => Some(0..4), 31 | "FI" => Some(0..3), 32 | "FK" => Some(0..2), 33 | "FO" => Some(0..4), 34 | "FR" => Some(0..5), 35 | "GB" => Some(0..4), 36 | "GE" => Some(0..2), 37 | "GI" => Some(0..4), 38 | "GL" => Some(0..4), 39 | "GR" => Some(0..3), 40 | "GT" => Some(0..4), 41 | "HR" => Some(0..7), 42 | "HU" => Some(0..3), 43 | "IE" => Some(0..4), 44 | "IL" => Some(0..3), 45 | "IQ" => Some(0..4), 46 | "IS" => Some(0..2), 47 | "IT" => Some(1..6), 48 | "JO" => Some(0..4), 49 | "KW" => Some(0..4), 50 | "KZ" => Some(0..3), 51 | "LB" => Some(0..4), 52 | "LC" => Some(0..4), 53 | "LI" => Some(0..5), 54 | "LT" => Some(0..5), 55 | "LU" => Some(0..3), 56 | "LV" => Some(0..4), 57 | "LY" => Some(0..3), 58 | "MC" => Some(0..5), 59 | "MD" => Some(0..2), 60 | "ME" => Some(0..3), 61 | "MK" => Some(0..3), 62 | "MN" => Some(0..4), 63 | "MR" => Some(0..5), 64 | "MT" => Some(0..4), 65 | "MU" => Some(0..6), 66 | "NI" => Some(0..4), 67 | "NL" => Some(0..4), 68 | "NO" => Some(0..4), 69 | "OM" => Some(0..3), 70 | "PL" => None, 71 | "PS" => Some(0..4), 72 | "PT" => Some(0..4), 73 | "QA" => Some(0..4), 74 | "RO" => Some(0..4), 75 | "RS" => Some(0..3), 76 | "RU" => Some(0..9), 77 | "SA" => Some(0..2), 78 | "SC" => Some(0..6), 79 | "SD" => Some(0..2), 80 | "SE" => Some(0..3), 81 | "SI" => Some(0..5), 82 | "SK" => Some(0..4), 83 | "SM" => Some(1..6), 84 | "SO" => Some(0..4), 85 | "ST" => Some(0..4), 86 | "SV" => Some(0..4), 87 | "TL" => Some(0..3), 88 | "TN" => Some(0..2), 89 | "TR" => Some(0..5), 90 | "UA" => Some(0..6), 91 | "VA" => Some(0..3), 92 | "VG" => Some(0..4), 93 | "XK" => Some(0..2), 94 | "YE" => Some(0..4), 95 | _ => None, 96 | } 97 | } 98 | 99 | /// Get the position of the branch in the BBAN. 100 | #[inline] 101 | pub(crate) fn branch_identifier(country_code: &str) -> Option> { 102 | #[allow(clippy::match_same_arms)] // For clarity, identical arms are not combined. 103 | match country_code { 104 | "AD" => Some(4..8), 105 | "AE" => None, 106 | "AL" => Some(3..7), 107 | "AT" => None, 108 | "AZ" => None, 109 | "BA" => Some(3..6), 110 | "BE" => None, 111 | "BG" => Some(4..8), 112 | "BH" => None, 113 | "BI" => Some(5..10), 114 | "BR" => Some(8..13), 115 | "BY" => None, 116 | "CH" => None, 117 | "CR" => None, 118 | "CY" => Some(3..8), 119 | "CZ" => None, 120 | "DE" => None, 121 | "DJ" => Some(5..10), 122 | "DK" => None, 123 | "DO" => None, 124 | "EE" => None, 125 | "EG" => Some(4..8), 126 | "ES" => Some(4..8), 127 | "FI" => None, 128 | "FK" => None, 129 | "FO" => None, 130 | "FR" => None, 131 | "GB" => Some(4..10), 132 | "GE" => None, 133 | "GI" => None, 134 | "GL" => None, 135 | "GR" => Some(3..7), 136 | "GT" => None, 137 | "HR" => None, 138 | "HU" => Some(3..7), 139 | "IE" => Some(4..10), 140 | "IL" => Some(3..6), 141 | "IQ" => Some(4..7), 142 | "IS" => Some(2..4), 143 | "IT" => Some(6..11), 144 | "JO" => Some(4..8), 145 | "KW" => None, 146 | "KZ" => None, 147 | "LB" => None, 148 | "LC" => None, 149 | "LI" => None, 150 | "LT" => None, 151 | "LU" => None, 152 | "LV" => None, 153 | "LY" => Some(3..6), 154 | "MC" => Some(5..10), 155 | "MD" => None, 156 | "ME" => None, 157 | "MK" => None, 158 | "MN" => None, 159 | "MR" => Some(5..10), 160 | "MT" => Some(4..9), 161 | "MU" => Some(6..8), 162 | "NI" => None, 163 | "NL" => None, 164 | "NO" => None, 165 | "OM" => None, 166 | "PL" => Some(0..8), 167 | "PS" => None, 168 | "PT" => Some(4..8), 169 | "QA" => None, 170 | "RO" => None, 171 | "RS" => None, 172 | "RU" => Some(9..14), 173 | "SA" => None, 174 | "SC" => Some(6..8), 175 | "SD" => None, 176 | "SE" => None, 177 | "SI" => None, 178 | "SK" => None, 179 | "SM" => Some(6..11), 180 | "SO" => Some(4..7), 181 | "ST" => Some(4..8), 182 | "SV" => None, 183 | "TL" => None, 184 | "TN" => Some(2..5), 185 | "TR" => None, 186 | "UA" => None, 187 | "VA" => None, 188 | "VG" => None, 189 | "XK" => Some(2..4), 190 | "YE" => Some(4..8), 191 | _ => None, 192 | } 193 | } 194 | 195 | use crate::countries::CharacterType; 196 | 197 | #[inline] 198 | pub(crate) fn country_pattern(country_code: &str) -> Option<&[(usize, CharacterType)]> { 199 | use core::borrow::Borrow; 200 | use CharacterType::{A, C, N}; 201 | match country_code { 202 | "AD" => Some([(4, N), (4, N), (12, C)].borrow()), 203 | "AE" => Some([(3, N), (16, N)].borrow()), 204 | "AL" => Some([(8, N), (16, C)].borrow()), 205 | "AT" => Some([(5, N), (11, N)].borrow()), 206 | "AZ" => Some([(4, A), (20, C)].borrow()), 207 | "BA" => Some([(3, N), (3, N), (8, N), (2, N)].borrow()), 208 | "BE" => Some([(3, N), (7, N), (2, N)].borrow()), 209 | "BG" => Some([(4, A), (4, N), (2, N), (8, C)].borrow()), 210 | "BH" => Some([(4, A), (14, C)].borrow()), 211 | "BI" => Some([(5, N), (5, N), (11, N), (2, N)].borrow()), 212 | "BR" => Some([(8, N), (5, N), (10, N), (1, A), (1, C)].borrow()), 213 | "BY" => Some([(4, C), (4, N), (16, C)].borrow()), 214 | "CH" => Some([(5, N), (12, C)].borrow()), 215 | "CR" => Some([(4, N), (14, N)].borrow()), 216 | "CY" => Some([(3, N), (5, N), (16, C)].borrow()), 217 | "CZ" => Some([(4, N), (6, N), (10, N)].borrow()), 218 | "DE" => Some([(8, N), (10, N)].borrow()), 219 | "DJ" => Some([(5, N), (5, N), (11, N), (2, N)].borrow()), 220 | "DK" => Some([(4, N), (9, N), (1, N)].borrow()), 221 | "DO" => Some([(4, C), (20, N)].borrow()), 222 | "EE" => Some([(2, N), (2, N), (11, N), (1, N)].borrow()), 223 | "EG" => Some([(4, N), (4, N), (17, N)].borrow()), 224 | "ES" => Some([(4, N), (4, N), (1, N), (1, N), (10, N)].borrow()), 225 | "FI" => Some([(3, N), (11, N)].borrow()), 226 | "FK" => Some([(2, A), (12, N)].borrow()), 227 | "FO" => Some([(4, N), (9, N), (1, N)].borrow()), 228 | "FR" => Some([(5, N), (5, N), (11, C), (2, N)].borrow()), 229 | "GB" => Some([(4, A), (6, N), (8, N)].borrow()), 230 | "GE" => Some([(2, A), (16, N)].borrow()), 231 | "GI" => Some([(4, A), (15, C)].borrow()), 232 | "GL" => Some([(4, N), (9, N), (1, N)].borrow()), 233 | "GR" => Some([(3, N), (4, N), (16, C)].borrow()), 234 | "GT" => Some([(4, C), (20, C)].borrow()), 235 | "HR" => Some([(7, N), (10, N)].borrow()), 236 | "HU" => Some([(3, N), (4, N), (1, N), (15, N), (1, N)].borrow()), 237 | "IE" => Some([(4, A), (6, N), (8, N)].borrow()), 238 | "IL" => Some([(3, N), (3, N), (13, N)].borrow()), 239 | "IQ" => Some([(4, A), (3, N), (12, N)].borrow()), 240 | "IS" => Some([(4, N), (2, N), (6, N), (10, N)].borrow()), 241 | "IT" => Some([(1, A), (5, N), (5, N), (12, C)].borrow()), 242 | "JO" => Some([(4, A), (4, N), (18, C)].borrow()), 243 | "KW" => Some([(4, A), (22, C)].borrow()), 244 | "KZ" => Some([(3, N), (13, C)].borrow()), 245 | "LB" => Some([(4, N), (20, C)].borrow()), 246 | "LC" => Some([(4, A), (24, C)].borrow()), 247 | "LI" => Some([(5, N), (12, C)].borrow()), 248 | "LT" => Some([(5, N), (11, N)].borrow()), 249 | "LU" => Some([(3, N), (13, C)].borrow()), 250 | "LV" => Some([(4, A), (13, C)].borrow()), 251 | "LY" => Some([(3, N), (3, N), (15, N)].borrow()), 252 | "MC" => Some([(5, N), (5, N), (11, C), (2, N)].borrow()), 253 | "MD" => Some([(2, C), (18, C)].borrow()), 254 | "ME" => Some([(3, N), (13, N), (2, N)].borrow()), 255 | "MK" => Some([(3, N), (10, C), (2, N)].borrow()), 256 | "MN" => Some([(4, N), (12, N)].borrow()), 257 | "MR" => Some([(5, N), (5, N), (11, N), (2, N)].borrow()), 258 | "MT" => Some([(4, A), (5, N), (18, C)].borrow()), 259 | "MU" => Some([(4, A), (2, N), (2, N), (12, N), (3, N), (3, A)].borrow()), 260 | "NI" => Some([(4, A), (20, N)].borrow()), 261 | "NL" => Some([(4, A), (10, N)].borrow()), 262 | "NO" => Some([(4, N), (6, N), (1, N)].borrow()), 263 | "OM" => Some([(3, N), (16, C)].borrow()), 264 | "PL" => Some([(8, N), (16, N)].borrow()), 265 | "PS" => Some([(4, A), (21, C)].borrow()), 266 | "PT" => Some([(4, N), (4, N), (11, N), (2, N)].borrow()), 267 | "QA" => Some([(4, A), (21, C)].borrow()), 268 | "RO" => Some([(4, A), (16, C)].borrow()), 269 | "RS" => Some([(3, N), (13, N), (2, N)].borrow()), 270 | "RU" => Some([(9, N), (5, N), (15, C)].borrow()), 271 | "SA" => Some([(2, N), (18, C)].borrow()), 272 | "SC" => Some([(4, A), (2, N), (2, N), (16, N), (3, A)].borrow()), 273 | "SD" => Some([(2, N), (12, N)].borrow()), 274 | "SE" => Some([(3, N), (16, N), (1, N)].borrow()), 275 | "SI" => Some([(5, N), (8, N), (2, N)].borrow()), 276 | "SK" => Some([(4, N), (6, N), (10, N)].borrow()), 277 | "SM" => Some([(1, A), (5, N), (5, N), (12, C)].borrow()), 278 | "SO" => Some([(4, N), (3, N), (12, N)].borrow()), 279 | "ST" => Some([(4, N), (4, N), (11, N), (2, N)].borrow()), 280 | "SV" => Some([(4, A), (20, N)].borrow()), 281 | "TL" => Some([(3, N), (14, N), (2, N)].borrow()), 282 | "TN" => Some([(2, N), (3, N), (13, N), (2, N)].borrow()), 283 | "TR" => Some([(5, N), (1, N), (16, C)].borrow()), 284 | "UA" => Some([(6, N), (19, C)].borrow()), 285 | "VA" => Some([(3, N), (15, N)].borrow()), 286 | "VG" => Some([(4, A), (16, N)].borrow()), 287 | "XK" => Some([(4, N), (10, N), (2, N)].borrow()), 288 | "YE" => Some([(4, A), (4, N), (18, C)].borrow()), 289 | _ => None, 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /iban_validate/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![doc(html_root_url = "https://docs.rs/iban_validate/5.0.1")] 3 | #![forbid(unsafe_code)] 4 | #![deny(missing_docs)] 5 | #![deny(bare_trait_objects)] 6 | #![deny(elided_lifetimes_in_paths)] 7 | #![deny(missing_debug_implementations)] 8 | #![no_std] 9 | 10 | use core::convert::TryFrom; 11 | use core::error::Error; 12 | use core::fmt::{self, Debug, Display}; 13 | use core::str; 14 | 15 | mod base_iban; 16 | mod countries; 17 | mod generated; 18 | #[cfg(feature = "serde")] 19 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 20 | 21 | pub use base_iban::{BaseIban, ParseBaseIbanError}; 22 | 23 | /// A trait that provide basic functions on an IBAN. It is implemented by both [`Iban`], 24 | /// which represents a fully validated IBAN, and [`BaseIban`], which might not have a correct BBAN. 25 | pub trait IbanLike { 26 | /// Get the IBAN in the electronic format, without whitespace. This method 27 | /// is simply a view into the inner string. 28 | /// 29 | /// # Example 30 | /// ```rust 31 | /// use iban::*; 32 | /// let iban: Iban = "DE44 5001 0517 5407 3249 31".parse()?; 33 | /// assert_eq!(iban.electronic_str(), "DE44500105175407324931"); 34 | /// # Ok::<(), ParseIbanError>(()) 35 | /// ``` 36 | #[must_use] 37 | fn electronic_str(&self) -> &str; 38 | 39 | /// Get the country code of the IBAN. This method simply returns a slice of 40 | /// the inner representation. 41 | /// 42 | /// # Example 43 | /// ```rust 44 | /// use iban::*; 45 | /// let iban: Iban = "DE44 5001 0517 5407 3249 31".parse()?; 46 | /// assert_eq!(iban.country_code(), "DE"); 47 | /// # Ok::<(), ParseIbanError>(()) 48 | /// ``` 49 | #[inline] 50 | #[must_use] 51 | fn country_code(&self) -> &str { 52 | &self.electronic_str()[0..2] 53 | } 54 | 55 | /// Get the check digits of the IBAN, as a string slice. This method simply returns 56 | /// a slice of the inner representation. To obtain an integer instead, 57 | /// use [`check_digits`](IbanLike::check_digits). 58 | /// 59 | /// # Example 60 | /// ```rust 61 | /// use iban::*; 62 | /// let iban: Iban = "DE44 5001 0517 5407 3249 31".parse()?; 63 | /// assert_eq!(iban.check_digits_str(), "44"); 64 | /// # Ok::<(), ParseIbanError>(()) 65 | /// ``` 66 | #[inline] 67 | #[must_use] 68 | fn check_digits_str(&self) -> &str { 69 | &self.electronic_str()[2..4] 70 | } 71 | 72 | /// Get the check digits of the IBAN. This method parses the digits to an 73 | /// integer, performing slightly more work than [`check_digits_str`](IbanLike::check_digits_str). 74 | /// 75 | /// # Example 76 | /// ```rust 77 | /// use iban::*; 78 | /// let iban: Iban = "DE44 5001 0517 5407 3249 31".parse()?; 79 | /// assert_eq!(iban.check_digits(), 44); 80 | /// # Ok::<(), ParseIbanError>(()) 81 | /// ``` 82 | #[inline] 83 | #[must_use] 84 | fn check_digits(&self) -> u8 { 85 | self.check_digits_str().parse().expect( 86 | "Could not parse check digits. Please create an issue at \ 87 | https://github.com/ThomasdenH/iban_validate.", 88 | ) 89 | } 90 | 91 | /// Get the BBAN part of the IBAN, as a `&str`. Note that the BBAN is not 92 | /// necessarily valid if this is not guaranteed by the implementing type. 93 | /// Use [`Iban::bban`] to guarantee a correct BBAN. 94 | /// 95 | /// # Example 96 | /// ```rust 97 | /// use iban::*; 98 | /// let iban: Iban = "DE44 5001 0517 5407 3249 31".parse()?; 99 | /// assert_eq!(iban.bban_unchecked(), "500105175407324931"); 100 | /// # Ok::<(), ParseIbanError>(()) 101 | /// ``` 102 | #[inline] 103 | #[must_use] 104 | fn bban_unchecked(&self) -> &str { 105 | &self.electronic_str()[4..] 106 | } 107 | } 108 | 109 | impl IbanLike for Iban { 110 | #[inline] 111 | #[must_use] 112 | fn electronic_str(&self) -> &str { 113 | self.base_iban.electronic_str() 114 | } 115 | } 116 | 117 | impl Iban { 118 | /// Get the BBAN part of the IBAN, as a `&str`. This method, in contrast to [`IbanLike::bban_unchecked`], 119 | /// is only available on the [`Iban`] structure, which means the returned BBAN string is always correct. 120 | /// 121 | /// # Example 122 | /// ```rust 123 | /// use iban::*; 124 | /// let iban: Iban = "DE44 5001 0517 5407 3249 31".parse()?; 125 | /// assert_eq!(iban.bban(), "500105175407324931"); 126 | /// # Ok::<(), ParseIbanError>(()) 127 | /// ``` 128 | #[inline] 129 | #[must_use] 130 | pub fn bban(&self) -> &str { 131 | self.bban_unchecked() 132 | } 133 | 134 | /// Get the bank identifier of the IBAN. The bank identifier might not be 135 | /// defined, in which case this method returns `None`. 136 | /// 137 | /// # Example 138 | /// ``` 139 | /// use iban::*; 140 | /// let iban: Iban = "AD12 0001 2030 2003 5910 0100".parse()?; 141 | /// assert_eq!(iban.bank_identifier(), Some("0001")); 142 | /// # Ok::<(), ParseIbanError>(()) 143 | /// ``` 144 | #[inline] 145 | #[must_use] 146 | pub fn bank_identifier(&self) -> Option<&str> { 147 | generated::bank_identifier(self.country_code()) 148 | .map(|range| &self.electronic_str()[4..][range]) 149 | } 150 | 151 | /// Get the branch identifier of the IBAN. The branch identifier might not be 152 | /// defined, in which case this method returns `None`. 153 | /// 154 | /// # Example 155 | /// ``` 156 | /// use iban::*; 157 | /// let iban: Iban = "AD12 0001 2030 2003 5910 0100".parse()?; 158 | /// assert_eq!(iban.branch_identifier(), Some("2030")); 159 | /// # Ok::<(), ParseIbanError>(()) 160 | /// ``` 161 | #[must_use] 162 | #[inline] 163 | pub fn branch_identifier(&self) -> Option<&str> { 164 | generated::branch_identifier(self.country_code()) 165 | .map(|range| &self.electronic_str()[4..][range]) 166 | } 167 | } 168 | 169 | impl From for BaseIban { 170 | #[inline] 171 | #[must_use] 172 | fn from(value: Iban) -> BaseIban { 173 | value.base_iban 174 | } 175 | } 176 | 177 | impl Debug for Iban { 178 | #[inline] 179 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 180 | Debug::fmt(&self.base_iban, f) 181 | } 182 | } 183 | 184 | impl Display for Iban { 185 | #[inline] 186 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 187 | Display::fmt(&self.base_iban, f) 188 | } 189 | } 190 | 191 | /// Represents an IBAN. To obtain it, make use of the [`parse()`] function, which will make sure the 192 | /// string follows the ISO 13616 standard. Apart from its own methods, `Iban` implements [`IbanLike`], 193 | /// which provides more functionality. 194 | /// 195 | /// The impementation of [`Display`] provides spaced formatting of the IBAN. Electronic 196 | /// formatting can be obtained via [`electronic_str`](IbanLike::electronic_str). 197 | /// 198 | /// A valid IBAN... 199 | /// - must start with two uppercase ASCII letters, followed 200 | /// by two digits, followed by any number of digits and ASCII 201 | /// letters. 202 | /// - must have a valid checksum. 203 | /// - must contain no whitespace, or be in the paper format, where 204 | /// characters are in space-separated groups of four. 205 | /// - must adhere to the country-specific format. 206 | /// 207 | /// Sometimes it may be desirable to accept IBANs that do not have their 208 | /// country registered in the IBAN registry, or it may simply be unimportant 209 | /// whether the country's BBAN format was followed. In that case, you can use 210 | /// a [`BaseIban`] instead. 211 | /// 212 | /// # Examples 213 | /// ```rust 214 | /// use iban::*; 215 | /// let address = "KZ86125KZT5004100100".parse::()?; 216 | /// assert_eq!(address.to_string(), "KZ86 125K ZT50 0410 0100"); 217 | /// # Ok::<(), iban::ParseIbanError>(()) 218 | /// ``` 219 | /// 220 | /// ## Formatting 221 | /// The IBAN specification describes two formats: an electronic format without 222 | /// whitespace and a paper format which seperates the IBAN in groups of 223 | /// four characters. Both will be parsed correctly by this crate. When 224 | /// formatting, [`Debug`] can be used to output the former and [`Display`] for 225 | /// the latter. This is true for a [`BaseIban`] as well as an [`Iban`]. 226 | /// Alternatively, you can use [`IbanLike::electronic_str`] to obtain the 227 | /// electronic format as a string slice. 228 | /// ``` 229 | /// # use iban::ParseIbanError; 230 | /// let iban: iban::Iban = "RO66BACX0000001234567890".parse()?; 231 | /// // Use Debug for the electronic format. 232 | /// assert_eq!(&format!("{:?}", iban), "RO66BACX0000001234567890"); 233 | /// // Use Display for the pretty print format. 234 | /// assert_eq!(&format!("{}", iban), "RO66 BACX 0000 0012 3456 7890"); 235 | /// # Ok::<(), ParseIbanError>(()) 236 | /// ``` 237 | /// [`parse()`]: https://doc.rust-lang.org/std/primitive.str.html#method.parse 238 | #[derive(Clone, Copy, Eq, PartialEq, Hash)] 239 | pub struct Iban { 240 | /// The inner IBAN, which has been checked. 241 | base_iban: BaseIban, 242 | } 243 | 244 | /// An error indicating the IBAN could not be parsed. 245 | /// 246 | /// # Example 247 | /// ```rust 248 | /// use iban::{BaseIban, Iban, ParseIbanError, ParseBaseIbanError}; 249 | /// use core::convert::TryFrom; 250 | /// 251 | /// // The following IBAN has an invalid checksum 252 | /// assert_eq!( 253 | /// "MR00 0002 0001 0100 0012 3456 754".parse::(), 254 | /// Err(ParseIbanError::from(ParseBaseIbanError::InvalidChecksum)) 255 | /// ); 256 | /// 257 | /// // The following IBAN doesn't follow the country format 258 | /// let base_iban: BaseIban = "AL84212110090000AB023569874".parse()?; 259 | /// assert_eq!( 260 | /// Iban::try_from(base_iban), 261 | /// Err(ParseIbanError::InvalidBban(base_iban)) 262 | /// ); 263 | /// # Ok::<(), ParseBaseIbanError>(()) 264 | /// ``` 265 | #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] 266 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 267 | pub enum ParseIbanError { 268 | /// This variant indicates that the basic IBAN structure was not followed. 269 | InvalidBaseIban { 270 | /// The error indicating what went wrong when parsing the Iban. 271 | source: ParseBaseIbanError, 272 | }, 273 | /// This variant indicates that the BBAN did not follow the correct format. 274 | /// The `BaseIban` provides functionality on the IBAN part of the 275 | /// address. 276 | InvalidBban(BaseIban), 277 | /// This variant indicated that the country code of the IBAN was not recognized. 278 | /// The `BaseIban` provides functionality on the IBAN part of the 279 | /// address. 280 | UnknownCountry(BaseIban), 281 | } 282 | 283 | impl From for ParseIbanError { 284 | #[inline] 285 | #[must_use] 286 | fn from(source: ParseBaseIbanError) -> ParseIbanError { 287 | ParseIbanError::InvalidBaseIban { source } 288 | } 289 | } 290 | 291 | impl fmt::Display for ParseIbanError { 292 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 293 | write!( 294 | f, 295 | "{}", 296 | match self { 297 | ParseIbanError::InvalidBaseIban { .. } => 298 | "the string does not follow the base IBAN rules", 299 | ParseIbanError::InvalidBban(..) => "the IBAN doesn't have a correct BBAN", 300 | ParseIbanError::UnknownCountry(..) => "the IBAN country code wasn't recognized", 301 | } 302 | ) 303 | } 304 | } 305 | 306 | impl Error for ParseIbanError { 307 | #[inline] 308 | #[must_use] 309 | fn source(&self) -> Option<&(dyn Error + 'static)> { 310 | match self { 311 | ParseIbanError::InvalidBaseIban { source } => Some(source), 312 | _ => None, 313 | } 314 | } 315 | } 316 | 317 | impl AsRef for ParseIbanError { 318 | fn as_ref(&self) -> &ParseIbanError { 319 | self 320 | } 321 | } 322 | 323 | impl AsMut for ParseIbanError { 324 | fn as_mut(&mut self) -> &mut ParseIbanError { 325 | self 326 | } 327 | } 328 | 329 | impl<'a> TryFrom<&'a str> for Iban { 330 | type Error = ParseIbanError; 331 | /// Parse an IBAN without taking the BBAN into consideration. 332 | /// 333 | /// # Errors 334 | /// If the string does not match the IBAN format or the checksum is 335 | /// invalid, [`ParseIbanError::InvalidBaseIban`] will be 336 | /// returned. If the country format is invalid or unknown, the other 337 | /// variants will be returned with the [`BaseIban`] giving 338 | /// access to some basic functionality nonetheless. 339 | #[inline] 340 | fn try_from(value: &'a str) -> Result { 341 | value 342 | .parse::() 343 | .map_err(|source| ParseIbanError::InvalidBaseIban { source }) 344 | .and_then(Iban::try_from) 345 | } 346 | } 347 | 348 | impl TryFrom for Iban { 349 | type Error = ParseIbanError; 350 | /// Parse an IBAN without taking the BBAN into consideration. 351 | /// 352 | /// # Errors 353 | /// If the string does not match the IBAN format or the checksum is 354 | /// invalid, [`ParseIbanError::InvalidBaseIban`] will be 355 | /// returned. If the country format is invalid or unknown, the other 356 | /// variants will be returned with the [`BaseIban`] giving 357 | /// access to some basic functionality nonetheless. 358 | fn try_from(base_iban: BaseIban) -> Result { 359 | use countries::Matchable; 360 | generated::country_pattern(base_iban.country_code()) 361 | .ok_or(ParseIbanError::UnknownCountry(base_iban)) 362 | .and_then(|matcher: &[(usize, _)]| { 363 | if matcher.match_str(base_iban.bban_unchecked()) { 364 | Ok(Iban { base_iban }) 365 | } else { 366 | Err(ParseIbanError::InvalidBban(base_iban)) 367 | } 368 | }) 369 | } 370 | } 371 | 372 | impl str::FromStr for Iban { 373 | type Err = ParseIbanError; 374 | #[inline] 375 | fn from_str(address: &str) -> Result { 376 | Iban::try_from(address) 377 | } 378 | } 379 | 380 | impl AsRef for Iban { 381 | fn as_ref(&self) -> &Iban { 382 | self 383 | } 384 | } 385 | 386 | impl AsMut for Iban { 387 | fn as_mut(&mut self) -> &mut Iban { 388 | self 389 | } 390 | } 391 | 392 | impl AsRef for Iban { 393 | fn as_ref(&self) -> &BaseIban { 394 | &self.base_iban 395 | } 396 | } 397 | 398 | #[cfg(feature = "serde")] 399 | impl Serialize for Iban { 400 | #[inline] 401 | fn serialize(&self, serializer: S) -> Result { 402 | self.base_iban.serialize(serializer) 403 | } 404 | } 405 | 406 | #[cfg(feature = "serde")] 407 | impl<'de> Deserialize<'de> for Iban { 408 | #[must_use] 409 | fn deserialize>(deserializer: D) -> Result { 410 | struct IbanStringVisitor; 411 | use serde::de; 412 | 413 | impl<'vi> de::Visitor<'vi> for IbanStringVisitor { 414 | type Value = Iban; 415 | 416 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 417 | write!(formatter, "an IBAN string") 418 | } 419 | 420 | fn visit_str(self, value: &str) -> Result { 421 | value.parse::().map_err(E::custom) 422 | } 423 | } 424 | 425 | deserializer.deserialize_str(IbanStringVisitor) 426 | } 427 | } 428 | -------------------------------------------------------------------------------- /iban_validate/tests/as_ref.rs: -------------------------------------------------------------------------------- 1 | use core::error::Error; 2 | 3 | use iban::{BaseIban, Iban}; 4 | 5 | /// [`Iban`] implements [`AsRef`] to [`BaseIban`]. Test it here and see if it 6 | /// displays the same. 7 | #[test] 8 | fn test_as_ref() -> Result<(), Box> { 9 | let iban: Iban = "KW81CBKU0000000000001234560101".parse()?; 10 | 11 | fn pretty_format(base_iban: impl AsRef) -> String { 12 | let base_iban: &BaseIban = base_iban.as_ref(); 13 | base_iban.to_string() 14 | } 15 | 16 | let s = pretty_format(iban); 17 | assert_eq!(s.as_str(), "KW81 CBKU 0000 0000 0000 1234 5601 01"); 18 | assert_eq!(iban.to_string(), s); 19 | 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /iban_validate/tests/format.rs: -------------------------------------------------------------------------------- 1 | //! This module tests the formatting of IBANs 2 | 3 | use iban::{Iban, IbanLike, ParseIbanError}; 4 | 5 | /// This test checks the electronic formatting method. 6 | #[test] 7 | fn electronic() -> Result<(), ParseIbanError> { 8 | // An IBAN without spaces 9 | assert_eq!( 10 | "BE68539007547034".parse::()?.electronic_str(), 11 | "BE68539007547034" 12 | ); 13 | 14 | // An IBAN in the pretty print format 15 | assert_eq!( 16 | "BE68 5390 0754 7034".parse::()?.electronic_str(), 17 | "BE68539007547034" 18 | ); 19 | Ok(()) 20 | } 21 | 22 | /// This test checks the print formatting method. 23 | #[test] 24 | fn print() -> Result<(), ParseIbanError> { 25 | // An IBAN without spaces 26 | assert_eq!( 27 | "KW81CBKU0000000000001234560101" 28 | .parse::()? 29 | .to_string(), 30 | "KW81 CBKU 0000 0000 0000 1234 5601 01" 31 | ); 32 | 33 | // An IBAN in the pretty print format 34 | assert_eq!( 35 | "PL61 1090 1014 0000 0712 1981 2874" 36 | .parse::()? 37 | .to_string(), 38 | "PL61 1090 1014 0000 0712 1981 2874" 39 | ); 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /iban_validate/tests/impls.rs: -------------------------------------------------------------------------------- 1 | //! This module statically checks whether types implement the expected traits. 2 | use core::convert::TryFrom; 3 | use core::fmt::{Debug, Display}; 4 | use core::hash::Hash; 5 | use core::str::FromStr; 6 | use iban::{BaseIban, Iban, ParseBaseIbanError, ParseIbanError}; 7 | use static_assertions::assert_impl_all; 8 | 9 | assert_impl_all!( 10 | BaseIban: Copy, 11 | Clone, 12 | Eq, 13 | PartialEq, 14 | Hash, 15 | Debug, 16 | Display, 17 | FromStr, 18 | TryFrom<&'static str>, 19 | Send, 20 | Sync, 21 | From, 22 | AsRef, 23 | AsMut 24 | ); 25 | assert_impl_all!( 26 | Iban: Copy, 27 | Clone, 28 | Eq, 29 | PartialEq, 30 | Hash, 31 | Debug, 32 | Display, 33 | FromStr, 34 | TryFrom, 35 | TryFrom<&'static str>, 36 | Send, 37 | Sync, 38 | Into, 39 | // We can convert between references. We cannot convert between mutable 40 | // references, since a changed BaseIban may not be a valid Iban anymore. 41 | AsRef, 42 | AsRef, 43 | AsMut 44 | ); 45 | assert_impl_all!( 46 | ParseBaseIbanError: Copy, 47 | Clone, 48 | Eq, 49 | PartialEq, 50 | Hash, 51 | Debug, 52 | Send, 53 | Sync, 54 | Display, 55 | Into, 56 | AsRef, 57 | AsMut 58 | ); 59 | assert_impl_all!( 60 | ParseIbanError: Copy, 61 | Clone, 62 | Eq, 63 | PartialEq, 64 | Hash, 65 | Debug, 66 | Send, 67 | Sync, 68 | Display, 69 | From, 70 | AsRef, 71 | AsMut 72 | ); 73 | 74 | assert_impl_all!(ParseBaseIbanError: core::error::Error); 75 | assert_impl_all!(ParseIbanError: core::error::Error); 76 | 77 | #[cfg(feature = "serde")] 78 | mod impls_serde { 79 | use super::{assert_impl_all, BaseIban, Iban}; 80 | use serde::{Deserialize, Serialize}; 81 | assert_impl_all!(BaseIban: Serialize, Deserialize<'static>); 82 | assert_impl_all!(Iban: Serialize, Deserialize<'static>); 83 | } 84 | -------------------------------------------------------------------------------- /iban_validate/tests/parse.rs: -------------------------------------------------------------------------------- 1 | //! This module tests the parsing of various IBAN numbers 2 | 3 | use iban::{Iban, ParseBaseIbanError, ParseIbanError}; 4 | 5 | #[test] 6 | /// This test checks whether IBANs having an invalid structure are detected to be invalid. 7 | fn test_invalid_format() { 8 | let invalid_formats = &[ 9 | "DE4", 10 | "DE445001023460732493147896512575467", 11 | "G416011012500000834112300695", 12 | "CHI300762011623852957", 13 | "DE44@0010234607324931", 14 | "$A0380000000648510167519", 15 | "tr330006100519786457465326", 16 | "DE4 450 010 517 540 732 493 1", 17 | "TR33000610051978645746532 ", 18 | ]; 19 | for s in invalid_formats { 20 | assert_eq!( 21 | s.parse::(), 22 | Err(ParseIbanError::InvalidBaseIban { 23 | source: ParseBaseIbanError::InvalidFormat 24 | }) 25 | ); 26 | } 27 | } 28 | 29 | #[test] 30 | /// This test checks whether IBANs having an invalid checksum are detected as such. 31 | fn test_checksum() { 32 | let invalid_checksums = [ 33 | "DE4450010234607324931", 34 | "GR16011012500000834112300695", 35 | "GB29NWBK60934331926819", 36 | "SA0380000000648510167519", 37 | "CH9300762011645852957", 38 | "TR330006100519786457465326", 39 | ]; 40 | 41 | for &i in &invalid_checksums { 42 | assert_eq!( 43 | i.parse::(), 44 | Err(ParseIbanError::InvalidBaseIban { 45 | source: ParseBaseIbanError::InvalidChecksum 46 | }) 47 | ); 48 | } 49 | } 50 | 51 | #[test] 52 | /// This test checks whether valid IBANs are marked valid. 53 | fn test_valid_iban() { 54 | let valid_ibans = [ 55 | "DE44500105175407324931", 56 | "GR1601101250000000012300695", 57 | "GB29NWBK60161331926819", 58 | "SA0380000000608010167519", 59 | "CH9300762011623852957", 60 | "TR330006100519786457841326", 61 | ]; 62 | 63 | for &i in &valid_ibans { 64 | assert!(i.parse::().is_ok()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /iban_validate/tests/proptest.rs: -------------------------------------------------------------------------------- 1 | use iban::{BaseIban, Iban, IbanLike}; 2 | use proptest::prelude::*; 3 | 4 | proptest! { 5 | #[test] 6 | fn parse_iban_format_electronic(country_code in "[A-Z]{2}", 7 | check_digits in 2_u8..=99_u8, 8 | bban in "[A-Z0-9]{1,30}") { 9 | 10 | let iban_string = format!("{}{:02}{}", country_code, check_digits, bban); 11 | 12 | if let Ok(iban) = iban_string.parse::() { 13 | // Format 14 | assert_eq!(iban.electronic_str(), iban_string); 15 | 16 | // Split 17 | assert_eq!(iban.country_code(), country_code); 18 | assert_eq!(iban.check_digits(), check_digits); 19 | assert_eq!(iban.bban(), bban); 20 | let _ = iban.bank_identifier(); 21 | let _ = iban.branch_identifier(); 22 | 23 | // Convert to string and parse again 24 | let print_string = iban.to_string(); 25 | assert_eq!(print_string.parse::().unwrap(), iban); 26 | } 27 | } 28 | } 29 | 30 | proptest! { 31 | #[test] 32 | fn parse_base_iban_format_electronic(country_code in "[A-Z]{2}", 33 | check_digits in 2_u8..=99_u8, 34 | bban in "[A-Z0-9]{1,30}") { 35 | 36 | let iban_string = format!("{}{:02}{}", country_code, check_digits, bban); 37 | 38 | if let Ok(iban) = iban_string.parse::() { 39 | // Format 40 | assert_eq!(iban.electronic_str(), iban_string); 41 | 42 | // Split 43 | assert_eq!(iban.country_code(), country_code); 44 | assert_eq!(iban.check_digits(), check_digits); 45 | assert_eq!(iban.bban_unchecked(), bban); 46 | 47 | // Convert to string and parse again 48 | let print_string = iban.to_string(); 49 | assert_eq!(print_string.parse::().unwrap(), iban); 50 | } 51 | } 52 | } 53 | 54 | proptest! { 55 | #[test] 56 | fn doesnt_crash_random_input(s in "\\PC*") { 57 | let _ = s.parse::(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /iban_validate/tests/registry_examples.rs: -------------------------------------------------------------------------------- 1 | //! Use the registry examples to validate the code. As you can see, there are a lot of errors/exceptions, unfortunately. 2 | 3 | use iban::{Iban, IbanLike, ParseIbanError}; 4 | 5 | pub mod registry_examples_generated; 6 | use registry_examples_generated::{RegistryExample, EXAMPLES}; 7 | 8 | #[test] 9 | fn test_registry_examples() -> Result<(), ParseIbanError> { 10 | for RegistryExample { 11 | country_code, 12 | bank_identifier, 13 | branch_identifier, 14 | bban, 15 | iban_electronic, 16 | iban_print, 17 | } in EXAMPLES 18 | { 19 | let iban_1 = iban_electronic.parse::().unwrap_or_else(|_| { 20 | panic!( 21 | "could not parse electronic format of country {}", 22 | country_code 23 | ) 24 | }); 25 | 26 | // For the countries that abide by the pretty print format, check if the parsed IBAN is identical. 27 | // We could remove the whitespace and parse again, but that's probably not worth it. 28 | let iban_2 = iban_print 29 | .parse::() 30 | .unwrap_or_else(|_| panic!("could not parse print format of country {}", country_code)); 31 | assert_eq!( 32 | iban_1, iban_2, 33 | "parsed ibans should be identical, regardless of format" 34 | ); 35 | 36 | // Validate the country code. 37 | assert_eq!( 38 | iban_1.country_code(), 39 | *country_code, 40 | "country codes do not match for country {}", 41 | country_code 42 | ); 43 | 44 | // Validate the bank identifier. 45 | let bank_identifier: Option = 46 | bank_identifier.map(|c| c.chars().filter(|c| c.is_ascii_alphanumeric()).collect()); 47 | 48 | assert_eq!( 49 | iban_1.bank_identifier(), 50 | bank_identifier.as_deref(), 51 | "bank identifiers do not match for country {}", 52 | country_code 53 | ); 54 | 55 | // Test that the branch identifier matches. 56 | assert_eq!( 57 | iban_1.branch_identifier(), 58 | *branch_identifier, 59 | "branch identifiers do not match for country {}", 60 | country_code 61 | ); 62 | 63 | // Validate the BBAN. 64 | let bban: String = bban 65 | .chars() 66 | .filter(|c: &char| c.is_ascii_alphanumeric()) 67 | .collect(); 68 | 69 | assert_eq!( 70 | iban_1.bban(), 71 | bban, 72 | "the bban doesn't match for country {}", 73 | country_code 74 | ); 75 | } 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /iban_validate/tests/registry_examples_generated.rs: -------------------------------------------------------------------------------- 1 | //! This file was automatically generated by `iban_validate_registry_generation`. 2 | 3 | pub struct RegistryExample<'a> { 4 | pub country_code: &'a str, 5 | pub bank_identifier: Option<&'a str>, 6 | pub branch_identifier: Option<&'a str>, 7 | pub bban: &'a str, 8 | pub iban_electronic: &'a str, 9 | pub iban_print: &'a str, 10 | } 11 | 12 | pub const EXAMPLES: &[RegistryExample] = &[ 13 | RegistryExample { 14 | country_code: "AD", 15 | bank_identifier: Some("0001"), 16 | branch_identifier: Some("2030"), 17 | bban: "00012030200359100100", 18 | iban_electronic: "AD1200012030200359100100", 19 | iban_print: "AD12 0001 2030 2003 5910 0100", 20 | }, 21 | RegistryExample { 22 | country_code: "AE", 23 | bank_identifier: Some("033"), 24 | branch_identifier: None, 25 | bban: "0331234567890123456", 26 | iban_electronic: "AE070331234567890123456", 27 | iban_print: "AE07 0331 2345 6789 0123 456", 28 | }, 29 | RegistryExample { 30 | country_code: "AL", 31 | bank_identifier: Some("212"), 32 | branch_identifier: Some("1100"), 33 | bban: "212110090000000235698741", 34 | iban_electronic: "AL47212110090000000235698741", 35 | iban_print: "AL47 2121 1009 0000 0002 3569 8741", 36 | }, 37 | RegistryExample { 38 | country_code: "AT", 39 | bank_identifier: Some("19043"), 40 | branch_identifier: None, 41 | bban: "1904300234573201", 42 | iban_electronic: "AT611904300234573201", 43 | iban_print: "AT61 1904 3002 3457 3201", 44 | }, 45 | RegistryExample { 46 | country_code: "AZ", 47 | bank_identifier: Some("NABZ"), 48 | branch_identifier: None, 49 | bban: "NABZ00000000137010001944", 50 | iban_electronic: "AZ21NABZ00000000137010001944", 51 | iban_print: "AZ21 NABZ 0000 0000 1370 1000 1944", 52 | }, 53 | RegistryExample { 54 | country_code: "BA", 55 | bank_identifier: Some("129"), 56 | branch_identifier: Some("007"), 57 | bban: "1290079401028494", 58 | iban_electronic: "BA391290079401028494", 59 | iban_print: "BA39 1290 0794 0102 8494", 60 | }, 61 | RegistryExample { 62 | country_code: "BE", 63 | bank_identifier: Some("539"), 64 | branch_identifier: None, 65 | bban: "539007547034", 66 | iban_electronic: "BE68539007547034", 67 | iban_print: "BE68 5390 0754 7034", 68 | }, 69 | RegistryExample { 70 | country_code: "BG", 71 | bank_identifier: Some("BNBG"), 72 | branch_identifier: Some("9661"), 73 | bban: "BNBG96611020345678", 74 | iban_electronic: "BG80BNBG96611020345678", 75 | iban_print: "BG80 BNBG 9661 1020 3456 78", 76 | }, 77 | RegistryExample { 78 | country_code: "BH", 79 | bank_identifier: Some("BMAG"), 80 | branch_identifier: None, 81 | bban: "BMAG00001299123456", 82 | iban_electronic: "BH67BMAG00001299123456", 83 | iban_print: "BH67 BMAG 0000 1299 1234 56", 84 | }, 85 | RegistryExample { 86 | country_code: "BI", 87 | bank_identifier: Some("10000"), 88 | branch_identifier: Some("10001"), 89 | bban: "10000100010000332045181", 90 | iban_electronic: "BI4210000100010000332045181", 91 | iban_print: "BI42 1000 0100 0100 0033 2045 181", 92 | }, 93 | RegistryExample { 94 | country_code: "BR", 95 | bank_identifier: Some("00360305"), 96 | branch_identifier: Some("00001"), 97 | bban: "00360305000010009795493C1", 98 | iban_electronic: "BR1800360305000010009795493C1", 99 | iban_print: "BR18 0036 0305 0000 1000 9795 493C 1", 100 | }, 101 | RegistryExample { 102 | country_code: "BY", 103 | bank_identifier: Some("NBRB"), 104 | branch_identifier: None, 105 | bban: "NBRB 3600900000002Z00AB00", 106 | iban_electronic: "BY13NBRB3600900000002Z00AB00", 107 | iban_print: "BY13 NBRB 3600 9000 0000 2Z00 AB00", 108 | }, 109 | RegistryExample { 110 | country_code: "CH", 111 | bank_identifier: Some("00762"), 112 | branch_identifier: None, 113 | bban: "00762011623852957", 114 | iban_electronic: "CH9300762011623852957", 115 | iban_print: "CH93 0076 2011 6238 5295 7", 116 | }, 117 | RegistryExample { 118 | country_code: "CR", 119 | bank_identifier: Some("0152"), 120 | branch_identifier: None, 121 | bban: "015202001026284066", 122 | iban_electronic: "CR05015202001026284066", 123 | iban_print: "CR05 0152 0200 1026 2840 66", 124 | }, 125 | RegistryExample { 126 | country_code: "CY", 127 | bank_identifier: Some("002"), 128 | branch_identifier: Some("00128"), 129 | bban: "002001280000001200527600", 130 | iban_electronic: "CY17002001280000001200527600", 131 | iban_print: "CY17 0020 0128 0000 0012 0052 7600", 132 | }, 133 | RegistryExample { 134 | country_code: "CZ", 135 | bank_identifier: Some("0800"), 136 | branch_identifier: None, 137 | bban: "08000000192000145399", 138 | iban_electronic: "CZ6508000000192000145399", 139 | iban_print: "CZ65 0800 0000 1920 0014 5399", 140 | }, 141 | RegistryExample { 142 | country_code: "DE", 143 | bank_identifier: Some("37040044"), 144 | branch_identifier: None, 145 | bban: "370400440532013000", 146 | iban_electronic: "DE89370400440532013000", 147 | iban_print: "DE89 3704 0044 0532 0130 00", 148 | }, 149 | RegistryExample { 150 | country_code: "DJ", 151 | bank_identifier: Some("00010"), 152 | branch_identifier: Some("00000"), 153 | bban: "00010000000154000100186", 154 | iban_electronic: "DJ2100010000000154000100186", 155 | iban_print: "DJ21 0001 0000 0001 5400 0100 186", 156 | }, 157 | RegistryExample { 158 | country_code: "DK", 159 | bank_identifier: Some("0040"), 160 | branch_identifier: None, 161 | bban: "00400440116243", 162 | iban_electronic: "DK5000400440116243", 163 | iban_print: "DK50 0040 0440 1162 43", 164 | }, 165 | RegistryExample { 166 | country_code: "DO", 167 | bank_identifier: Some("BAGR"), 168 | branch_identifier: None, 169 | bban: "BAGR00000001212453611324", 170 | iban_electronic: "DO28BAGR00000001212453611324", 171 | iban_print: "DO28 BAGR 0000 0001 2124 5361 1324", 172 | }, 173 | RegistryExample { 174 | country_code: "EE", 175 | bank_identifier: Some("22"), 176 | branch_identifier: None, 177 | bban: "2200221020145685", 178 | iban_electronic: "EE382200221020145685", 179 | iban_print: "EE38 2200 2210 2014 5685", 180 | }, 181 | RegistryExample { 182 | country_code: "EG", 183 | bank_identifier: Some("0019"), 184 | branch_identifier: Some("0005"), 185 | bban: "0019000500000000263180002", 186 | iban_electronic: "EG380019000500000000263180002", 187 | iban_print: "EG380019000500000000263180002", 188 | }, 189 | RegistryExample { 190 | country_code: "ES", 191 | bank_identifier: Some("2100"), 192 | branch_identifier: Some("0418"), 193 | bban: "21000418450200051332", 194 | iban_electronic: "ES9121000418450200051332", 195 | iban_print: "ES91 2100 0418 4502 0005 1332", 196 | }, 197 | RegistryExample { 198 | country_code: "FI", 199 | bank_identifier: Some("123"), 200 | branch_identifier: None, 201 | bban: "12345600000785", 202 | iban_electronic: "FI2112345600000785", 203 | iban_print: "FI21 1234 5600 0007 85", 204 | }, 205 | RegistryExample { 206 | country_code: "FK", 207 | bank_identifier: Some("SC"), 208 | branch_identifier: None, 209 | bban: "SC123456789012", 210 | iban_electronic: "FK88SC123456789012", 211 | iban_print: "FK88 SC12 3456 7890 12", 212 | }, 213 | RegistryExample { 214 | country_code: "FO", 215 | bank_identifier: Some("6460"), 216 | branch_identifier: None, 217 | bban: "64600001631634", 218 | iban_electronic: "FO6264600001631634", 219 | iban_print: "FO62 6460 0001 6316 34", 220 | }, 221 | RegistryExample { 222 | country_code: "FR", 223 | bank_identifier: Some("20041"), 224 | branch_identifier: None, 225 | bban: "20041010050500013M02606", 226 | iban_electronic: "FR1420041010050500013M02606", 227 | iban_print: "FR14 2004 1010 0505 0001 3M02 606", 228 | }, 229 | RegistryExample { 230 | country_code: "GB", 231 | bank_identifier: Some("NWBK"), 232 | branch_identifier: Some("601613"), 233 | bban: "NWBK60161331926819", 234 | iban_electronic: "GB29NWBK60161331926819", 235 | iban_print: "GB29 NWBK 6016 1331 9268 19", 236 | }, 237 | RegistryExample { 238 | country_code: "GE", 239 | bank_identifier: Some("NB"), 240 | branch_identifier: None, 241 | bban: "NB0000000101904917", 242 | iban_electronic: "GE29NB0000000101904917", 243 | iban_print: "GE29 NB00 0000 0101 9049 17", 244 | }, 245 | RegistryExample { 246 | country_code: "GI", 247 | bank_identifier: Some("NWBK"), 248 | branch_identifier: None, 249 | bban: "NWBK000000007099453", 250 | iban_electronic: "GI75NWBK000000007099453", 251 | iban_print: "GI75 NWBK 0000 0000 7099 453", 252 | }, 253 | RegistryExample { 254 | country_code: "GL", 255 | bank_identifier: Some("6471"), 256 | branch_identifier: None, 257 | bban: "64710001000206", 258 | iban_electronic: "GL8964710001000206", 259 | iban_print: "GL89 6471 0001 0002 06", 260 | }, 261 | RegistryExample { 262 | country_code: "GR", 263 | bank_identifier: Some("011"), 264 | branch_identifier: Some("0125"), 265 | bban: "01101250000000012300695", 266 | iban_electronic: "GR1601101250000000012300695", 267 | iban_print: "GR16 0110 1250 0000 0001 2300 695", 268 | }, 269 | RegistryExample { 270 | country_code: "GT", 271 | bank_identifier: Some("TRAJ"), 272 | branch_identifier: None, 273 | bban: "TRAJ01020000001210029690", 274 | iban_electronic: "GT82TRAJ01020000001210029690", 275 | iban_print: "GT82 TRAJ 0102 0000 0012 1002 9690", 276 | }, 277 | RegistryExample { 278 | country_code: "HR", 279 | bank_identifier: Some("1001005"), 280 | branch_identifier: None, 281 | bban: "10010051863000160", 282 | iban_electronic: "HR1210010051863000160", 283 | iban_print: "HR12 1001 0051 8630 0016 0", 284 | }, 285 | RegistryExample { 286 | country_code: "HU", 287 | bank_identifier: Some("117"), 288 | branch_identifier: Some("7301"), 289 | bban: "117730161111101800000000", 290 | iban_electronic: "HU42117730161111101800000000", 291 | iban_print: "HU42 1177 3016 1111 1018 0000 0000", 292 | }, 293 | RegistryExample { 294 | country_code: "IE", 295 | bank_identifier: Some("AIBK"), 296 | branch_identifier: Some("931152"), 297 | bban: "AIBK93115212345678", 298 | iban_electronic: "IE29AIBK93115212345678", 299 | iban_print: "IE29 AIBK 9311 5212 3456 78", 300 | }, 301 | RegistryExample { 302 | country_code: "IL", 303 | bank_identifier: Some("010"), 304 | branch_identifier: Some("800"), 305 | bban: "0108000000099999999", 306 | iban_electronic: "IL620108000000099999999", 307 | iban_print: "IL62 0108 0000 0009 9999 999", 308 | }, 309 | RegistryExample { 310 | country_code: "IQ", 311 | bank_identifier: Some("NBIQ"), 312 | branch_identifier: Some("850"), 313 | bban: "NBIQ850123456789012", 314 | iban_electronic: "IQ98NBIQ850123456789012", 315 | iban_print: "IQ98 NBIQ 8501 2345 6789 012", 316 | }, 317 | RegistryExample { 318 | country_code: "IS", 319 | bank_identifier: Some("01"), 320 | branch_identifier: Some("59"), 321 | bban: "0159260076545510730339", 322 | iban_electronic: "IS140159260076545510730339", 323 | iban_print: "IS14 0159 2600 7654 5510 7303 39", 324 | }, 325 | RegistryExample { 326 | country_code: "IT", 327 | bank_identifier: Some("05428"), 328 | branch_identifier: Some("11101"), 329 | bban: "X0542811101000000123456", 330 | iban_electronic: "IT60X0542811101000000123456", 331 | iban_print: "IT60 X054 2811 1010 0000 0123 456", 332 | }, 333 | RegistryExample { 334 | country_code: "JO", 335 | bank_identifier: Some("CBJO"), 336 | branch_identifier: Some("0010"), 337 | bban: "CBJO0010000000000131000302", 338 | iban_electronic: "JO94CBJO0010000000000131000302", 339 | iban_print: "JO94 CBJO 0010 0000 0000 0131 0003 02", 340 | }, 341 | RegistryExample { 342 | country_code: "KW", 343 | bank_identifier: Some("CBKU"), 344 | branch_identifier: None, 345 | bban: "CBKU0000000000001234560101", 346 | iban_electronic: "KW81CBKU0000000000001234560101", 347 | iban_print: "KW81 CBKU 0000 0000 0000 1234 5601 01", 348 | }, 349 | RegistryExample { 350 | country_code: "KZ", 351 | bank_identifier: Some("125"), 352 | branch_identifier: None, 353 | bban: "125KZT5004100100", 354 | iban_electronic: "KZ86125KZT5004100100", 355 | iban_print: "KZ86 125K ZT50 0410 0100", 356 | }, 357 | RegistryExample { 358 | country_code: "LB", 359 | bank_identifier: Some("0999"), 360 | branch_identifier: None, 361 | bban: "0999 0000 0001 0019 0122 9114", 362 | iban_electronic: "LB62099900000001001901229114", 363 | iban_print: "LB62 0999 0000 0001 0019 0122 9114", 364 | }, 365 | RegistryExample { 366 | country_code: "LC", 367 | bank_identifier: Some("HEMM"), 368 | branch_identifier: None, 369 | bban: "HEMM000100010012001200023015", 370 | iban_electronic: "LC55HEMM000100010012001200023015", 371 | iban_print: "LC55 HEMM 0001 0001 0012 0012 0002 3015", 372 | }, 373 | RegistryExample { 374 | country_code: "LI", 375 | bank_identifier: Some("08810"), 376 | branch_identifier: None, 377 | bban: "088100002324013AA", 378 | iban_electronic: "LI21088100002324013AA", 379 | iban_print: "LI21 0881 0000 2324 013A A", 380 | }, 381 | RegistryExample { 382 | country_code: "LT", 383 | bank_identifier: Some("10000"), 384 | branch_identifier: None, 385 | bban: "1000011101001000", 386 | iban_electronic: "LT121000011101001000", 387 | iban_print: "LT12 1000 0111 0100 1000", 388 | }, 389 | RegistryExample { 390 | country_code: "LU", 391 | bank_identifier: Some("001"), 392 | branch_identifier: None, 393 | bban: "0019400644750000", 394 | iban_electronic: "LU280019400644750000", 395 | iban_print: "LU28 0019 4006 4475 0000", 396 | }, 397 | RegistryExample { 398 | country_code: "LV", 399 | bank_identifier: Some("BANK"), 400 | branch_identifier: None, 401 | bban: "BANK0000435195001", 402 | iban_electronic: "LV80BANK0000435195001", 403 | iban_print: "LV80 BANK 0000 4351 9500 1", 404 | }, 405 | RegistryExample { 406 | country_code: "LY", 407 | bank_identifier: Some("002"), 408 | branch_identifier: Some("048"), 409 | bban: "002048000020100120361", 410 | iban_electronic: "LY83002048000020100120361", 411 | iban_print: "LY83 0020 4800 0020 1001 2036 1", 412 | }, 413 | RegistryExample { 414 | country_code: "MC", 415 | bank_identifier: Some("11222"), 416 | branch_identifier: Some("00001"), 417 | bban: "11222 00001 01234567890 30", 418 | iban_electronic: "MC5811222000010123456789030", 419 | iban_print: "MC58 1122 2000 0101 2345 6789 030", 420 | }, 421 | RegistryExample { 422 | country_code: "MD", 423 | bank_identifier: Some("AG"), 424 | branch_identifier: None, 425 | bban: "AG000225100013104168", 426 | iban_electronic: "MD24AG000225100013104168", 427 | iban_print: "MD24 AG00 0225 1000 1310 4168", 428 | }, 429 | RegistryExample { 430 | country_code: "ME", 431 | bank_identifier: Some("505"), 432 | branch_identifier: None, 433 | bban: "505000012345678951", 434 | iban_electronic: "ME25505000012345678951", 435 | iban_print: "ME25 5050 0001 2345 6789 51", 436 | }, 437 | RegistryExample { 438 | country_code: "MK", 439 | bank_identifier: Some("250"), 440 | branch_identifier: None, 441 | bban: "250120000058984", 442 | iban_electronic: "MK07250120000058984", 443 | iban_print: "MK07 2501 2000 0058 984", 444 | }, 445 | RegistryExample { 446 | country_code: "MN", 447 | bank_identifier: Some("1234"), 448 | branch_identifier: None, 449 | bban: "1234123456789123", 450 | iban_electronic: "MN121234123456789123", 451 | iban_print: "MN12 1234 1234 5678 9123", 452 | }, 453 | RegistryExample { 454 | country_code: "MR", 455 | bank_identifier: Some("00020"), 456 | branch_identifier: Some("00101"), 457 | bban: "00020001010000123456753", 458 | iban_electronic: "MR1300020001010000123456753", 459 | iban_print: "MR13 0002 0001 0100 0012 3456 753", 460 | }, 461 | RegistryExample { 462 | country_code: "MT", 463 | bank_identifier: Some("MALT"), 464 | branch_identifier: Some("01100"), 465 | bban: "MALT011000012345MTLCAST001S", 466 | iban_electronic: "MT84MALT011000012345MTLCAST001S", 467 | iban_print: "MT84 MALT 0110 0001 2345 MTLC AST0 01S", 468 | }, 469 | RegistryExample { 470 | country_code: "MU", 471 | bank_identifier: Some("BOMM01"), 472 | branch_identifier: Some("01"), 473 | bban: "BOMM0101101030300200000MUR", 474 | iban_electronic: "MU17BOMM0101101030300200000MUR", 475 | iban_print: "MU17 BOMM 0101 1010 3030 0200 000M UR", 476 | }, 477 | RegistryExample { 478 | country_code: "NI", 479 | bank_identifier: Some("BAPR"), 480 | branch_identifier: None, 481 | bban: "BAPR00000013000003558124", 482 | iban_electronic: "NI45BAPR00000013000003558124", 483 | iban_print: "NI45 BAPR 0000 0013 0000 0355 8124", 484 | }, 485 | RegistryExample { 486 | country_code: "NL", 487 | bank_identifier: Some("ABNA"), 488 | branch_identifier: None, 489 | bban: "ABNA0417164300", 490 | iban_electronic: "NL91ABNA0417164300", 491 | iban_print: "NL91 ABNA 0417 1643 00", 492 | }, 493 | RegistryExample { 494 | country_code: "NO", 495 | bank_identifier: Some("8601"), 496 | branch_identifier: None, 497 | bban: "86011117947", 498 | iban_electronic: "NO9386011117947", 499 | iban_print: "NO93 8601 1117 947", 500 | }, 501 | RegistryExample { 502 | country_code: "OM", 503 | bank_identifier: Some("018"), 504 | branch_identifier: None, 505 | bban: "0180000001299123456", 506 | iban_electronic: "OM810180000001299123456", 507 | iban_print: "OM81 0180 0000 0129 9123 456", 508 | }, 509 | RegistryExample { 510 | country_code: "PL", 511 | bank_identifier: None, 512 | branch_identifier: Some("10901014"), 513 | bban: "109010140000071219812874", 514 | iban_electronic: "PL61109010140000071219812874", 515 | iban_print: "PL61 1090 1014 0000 0712 1981 2874", 516 | }, 517 | RegistryExample { 518 | country_code: "PS", 519 | bank_identifier: Some("PALS"), 520 | branch_identifier: None, 521 | bban: "PALS000000000400123456702", 522 | iban_electronic: "PS92PALS000000000400123456702", 523 | iban_print: "PS92 PALS 0000 0000 0400 1234 5670 2", 524 | }, 525 | RegistryExample { 526 | country_code: "PT", 527 | bank_identifier: Some("0002"), 528 | branch_identifier: Some("0123"), 529 | bban: "000201231234567890154", 530 | iban_electronic: "PT50000201231234567890154", 531 | iban_print: "PT50 0002 0123 1234 5678 9015 4", 532 | }, 533 | RegistryExample { 534 | country_code: "QA", 535 | bank_identifier: Some("DOHB"), 536 | branch_identifier: None, 537 | bban: "DOHB00001234567890ABCDEFG", 538 | iban_electronic: "QA58DOHB00001234567890ABCDEFG", 539 | iban_print: "QA58 DOHB 0000 1234 5678 90AB CDEF G", 540 | }, 541 | RegistryExample { 542 | country_code: "RO", 543 | bank_identifier: Some("AAAA"), 544 | branch_identifier: None, 545 | bban: "AAAA1B31007593840000", 546 | iban_electronic: "RO49AAAA1B31007593840000", 547 | iban_print: "RO49 AAAA 1B31 0075 9384 0000", 548 | }, 549 | RegistryExample { 550 | country_code: "RS", 551 | bank_identifier: Some("260"), 552 | branch_identifier: None, 553 | bban: "260005601001611379", 554 | iban_electronic: "RS35260005601001611379", 555 | iban_print: "RS35 2600 0560 1001 6113 79", 556 | }, 557 | RegistryExample { 558 | country_code: "RU", 559 | bank_identifier: Some("044525225"), 560 | branch_identifier: Some("40817"), 561 | bban: "044525225 40817 810 5 3809 1310419", 562 | iban_electronic: "RU0304452522540817810538091310419", 563 | iban_print: "RU03 0445 2522 5408 1781 0538 0913 1041 9", 564 | }, 565 | RegistryExample { 566 | country_code: "SA", 567 | bank_identifier: Some("80"), 568 | branch_identifier: None, 569 | bban: "80000000608010167519", 570 | iban_electronic: "SA0380000000608010167519", 571 | iban_print: "SA03 8000 0000 6080 1016 7519", 572 | }, 573 | RegistryExample { 574 | country_code: "SC", 575 | bank_identifier: Some("SSCB11"), 576 | branch_identifier: Some("01"), 577 | bban: "SSCB11010000000000001497USD", 578 | iban_electronic: "SC18SSCB11010000000000001497USD", 579 | iban_print: "SC18 SSCB 1101 0000 0000 0000 1497 USD", 580 | }, 581 | RegistryExample { 582 | country_code: "SD", 583 | bank_identifier: Some("29"), 584 | branch_identifier: None, 585 | bban: "29010501234001", 586 | iban_electronic: "SD2129010501234001", 587 | iban_print: "SD21 2901 0501 2340 01", 588 | }, 589 | RegistryExample { 590 | country_code: "SE", 591 | bank_identifier: Some("500"), 592 | branch_identifier: None, 593 | bban: "50000000058398257466", 594 | iban_electronic: "SE4550000000058398257466", 595 | iban_print: "SE45 5000 0000 0583 9825 7466", 596 | }, 597 | RegistryExample { 598 | country_code: "SI", 599 | bank_identifier: Some("26330"), 600 | branch_identifier: None, 601 | bban: "263300012039086", 602 | iban_electronic: "SI56263300012039086", 603 | iban_print: "SI56 2633 0001 2039 086", 604 | }, 605 | RegistryExample { 606 | country_code: "SK", 607 | bank_identifier: Some("1200"), 608 | branch_identifier: None, 609 | bban: "12000000198742637541", 610 | iban_electronic: "SK3112000000198742637541", 611 | iban_print: "SK31 1200 0000 1987 4263 7541", 612 | }, 613 | RegistryExample { 614 | country_code: "SM", 615 | bank_identifier: Some("03225"), 616 | branch_identifier: Some("09800"), 617 | bban: "U0322509800000000270100", 618 | iban_electronic: "SM86U0322509800000000270100", 619 | iban_print: "SM86 U032 2509 8000 0000 0270 100", 620 | }, 621 | RegistryExample { 622 | country_code: "SO", 623 | bank_identifier: Some("1000"), 624 | branch_identifier: Some("001"), 625 | bban: "1000001001000100141", 626 | iban_electronic: "SO211000001001000100141", 627 | iban_print: "SO21 1000 0010 0100 0100 141", 628 | }, 629 | RegistryExample { 630 | country_code: "ST", 631 | bank_identifier: Some("0002"), 632 | branch_identifier: Some("0001"), 633 | bban: "000200010192194210112", 634 | iban_electronic: "ST32000200010192194210112", 635 | iban_print: "ST32 0002 0001 0192 1942 1011 2", 636 | }, 637 | RegistryExample { 638 | country_code: "SV", 639 | bank_identifier: Some("CENR"), 640 | branch_identifier: None, 641 | bban: "CENR00000000000000700025", 642 | iban_electronic: "SV62CENR00000000000000700025", 643 | iban_print: "SV62 CENR 0000 0000 0000 0070 0025", 644 | }, 645 | RegistryExample { 646 | country_code: "TL", 647 | bank_identifier: Some("008"), 648 | branch_identifier: None, 649 | bban: "0080012345678910157", 650 | iban_electronic: "TL380080012345678910157", 651 | iban_print: "TL38 0080 0123 4567 8910 157", 652 | }, 653 | RegistryExample { 654 | country_code: "TN", 655 | bank_identifier: Some("10"), 656 | branch_identifier: Some("006"), 657 | bban: "10006035183598478831", 658 | iban_electronic: "TN5910006035183598478831", 659 | iban_print: "TN59 1000 6035 1835 9847 8831", 660 | }, 661 | RegistryExample { 662 | country_code: "TR", 663 | bank_identifier: Some("00061"), 664 | branch_identifier: None, 665 | bban: "0006100519786457841326", 666 | iban_electronic: "TR330006100519786457841326", 667 | iban_print: "TR33 0006 1005 1978 6457 8413 26", 668 | }, 669 | RegistryExample { 670 | country_code: "UA", 671 | bank_identifier: Some("322313"), 672 | branch_identifier: None, 673 | bban: "3223130000026007233566001", 674 | iban_electronic: "UA213223130000026007233566001", 675 | iban_print: "UA21 3223 1300 0002 6007 2335 6600 1", 676 | }, 677 | RegistryExample { 678 | country_code: "VA", 679 | bank_identifier: Some("001"), 680 | branch_identifier: None, 681 | bban: "001123000012345678", 682 | iban_electronic: "VA59001123000012345678", 683 | iban_print: "VA59 0011 2300 0012 3456 78", 684 | }, 685 | RegistryExample { 686 | country_code: "VG", 687 | bank_identifier: Some("VPVG"), 688 | branch_identifier: None, 689 | bban: "VPVG0000012345678901", 690 | iban_electronic: "VG96VPVG0000012345678901", 691 | iban_print: "VG96 VPVG 0000 0123 4567 8901", 692 | }, 693 | RegistryExample { 694 | country_code: "XK", 695 | bank_identifier: Some("12"), 696 | branch_identifier: Some("12"), 697 | bban: "1212012345678906", 698 | iban_electronic: "XK051212012345678906", 699 | iban_print: "XK05 1212 0123 4567 8906", 700 | }, 701 | RegistryExample { 702 | country_code: "YE", 703 | bank_identifier: Some("CBYE"), 704 | branch_identifier: Some("0001"), 705 | bban: "CBYE0001018861234567891234", 706 | iban_electronic: "YE15CBYE0001018861234567891234", 707 | iban_print: "YE15 CBYE 0001 0188 6123 4567 8912 34", 708 | }, 709 | ]; 710 | -------------------------------------------------------------------------------- /iban_validate/tests/serde.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "serde")] 2 | use iban::{BaseIban, Iban, ParseIbanError}; 3 | use serde_test::{assert_tokens, Token}; 4 | 5 | #[test] 6 | fn base_iban_compact() -> Result<(), ParseIbanError> { 7 | use serde_test::Configure; 8 | let address: &str = "KW81CBKU0000000000001234560101"; 9 | let i: BaseIban = address.parse()?; 10 | assert_tokens(&i.compact(), &[Token::Str(address)]); 11 | Ok(()) 12 | } 13 | 14 | #[test] 15 | fn base_iban_readable() -> Result<(), ParseIbanError> { 16 | use serde_test::Configure; 17 | let address: &str = "KW81 CBKU 0000 0000 0000 1234 5601 01"; 18 | let i: BaseIban = address.parse()?; 19 | assert_tokens(&i.readable(), &[Token::Str(address)]); 20 | Ok(()) 21 | } 22 | 23 | #[test] 24 | fn iban_compact() -> Result<(), ParseIbanError> { 25 | use serde_test::Configure; 26 | let address: &str = "KW81CBKU0000000000001234560101"; 27 | let i: Iban = address.parse()?; 28 | assert_tokens(&i.compact(), &[Token::Str(address)]); 29 | Ok(()) 30 | } 31 | 32 | #[test] 33 | fn iban_readable() -> Result<(), ParseIbanError> { 34 | use serde_test::Configure; 35 | let address: &str = "KW81 CBKU 0000 0000 0000 1234 5601 01"; 36 | let i: Iban = address.parse()?; 37 | assert_tokens(&i.readable(), &[Token::Str(address)]); 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /iban_validate/tests/split.rs: -------------------------------------------------------------------------------- 1 | //! This module tests the split utility functions provided by the [`Iban`] type. 2 | 3 | use iban::{Iban, IbanLike, ParseIbanError}; 4 | 5 | #[test] 6 | /// This test checks whether the different splits of an address are correct. 7 | fn test_split() -> Result<(), ParseIbanError> { 8 | let address: Iban = "AD1200012030200359100100".parse()?; 9 | assert_eq!(address.country_code(), "AD"); 10 | assert_eq!(address.check_digits_str(), "12"); 11 | assert_eq!(address.check_digits(), 12); 12 | assert_eq!(address.bban(), "00012030200359100100"); 13 | 14 | let address: Iban = "TR330006100519786457841326".parse()?; 15 | assert_eq!(address.country_code(), "TR"); 16 | assert_eq!(address.check_digits_str(), "33"); 17 | assert_eq!(address.check_digits(), 33); 18 | assert_eq!(address.bban(), "0006100519786457841326"); 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /iban_validate/tests/validate_country.rs: -------------------------------------------------------------------------------- 1 | //! This module contains tests for the BBAN format 2 | 3 | use iban::{BaseIban, Iban, ParseIbanError}; 4 | 5 | #[test] 6 | /// This test checks whether ibans with a valid country format are recognized as such. 7 | fn test_valid_countries() -> Result<(), ParseIbanError> { 8 | let valid_iban_countries = [ 9 | "AD1200012030200359100100", 10 | "AE070331234567890123456", 11 | "AL47212110090000000235698741", 12 | "AT611904300234573201", 13 | "AZ21NABZ00000000137010001944", 14 | "BA391290079401028494", 15 | "BE68539007547034", 16 | "BG80BNBG96611020345678", 17 | "BH67BMAG00001299123456", 18 | "BR1800360305000010009795493C1", 19 | "BY13NBRB3600900000002Z00AB00", 20 | "CH9300762011623852957", 21 | "CR05015202001026284066", 22 | "CY17002001280000001200527600", 23 | "CZ6508000000192000145399", 24 | "DE89370400440532013000", 25 | "DK5000400440116243", 26 | "DO28BAGR00000001212453611324", 27 | "EE382200221020145685", 28 | "ES9121000418450200051332", 29 | "FI2112345600000785", 30 | "FO6264600001631634", 31 | "FR1420041010050500013M02606", 32 | "GB29NWBK60161331926819", 33 | "GE29NB0000000101904917", 34 | "GI75NWBK000000007099453", 35 | "GL8964710001000206", 36 | "GR1601101250000000012300695", 37 | "GT82TRAJ01020000001210029690", 38 | "HR1210010051863000160", 39 | "HU42117730161111101800000000", 40 | "IE29AIBK93115212345678", 41 | "IL620108000000099999999", 42 | "IQ98NBIQ850123456789012", 43 | "IS140159260076545510730339", 44 | "IT60X0542811101000000123456", 45 | "JO94CBJO0010000000000131000302", 46 | "KW81CBKU0000000000001234560101", 47 | "KZ86125KZT5004100100", 48 | "LB62099900000001001901229114", 49 | "LC55HEMM000100010012001200023015", 50 | "LI21088100002324013AA", 51 | "LT121000011101001000", 52 | "LU280019400644750000", 53 | "LV80BANK0000435195001", 54 | "LY83002048000020100120361", 55 | "MC5811222000010123456789030", 56 | "MD24AG000225100013104168", 57 | "ME25505000012345678951", 58 | "MK07250120000058984", 59 | "MR1300020001010000123456753", 60 | "MT84MALT011000012345MTLCAST001S", 61 | "MU17BOMM0101101030300200000MUR", 62 | "NL91ABNA0417164300", 63 | "NO9386011117947", 64 | "PL61109010140000071219812874", 65 | "PS92PALS000000000400123456702", 66 | "PT50000201231234567890154", 67 | "QA58DOHB00001234567890ABCDEFG", 68 | "RO49AAAA1B31007593840000", 69 | "RS35260005601001611379", 70 | "SA0380000000608010167519", 71 | "SC18SSCB11010000000000001497USD", 72 | "SE4550000000058398257466", 73 | "SI56263300012039086", 74 | "SK3112000000198742637541", 75 | "SM86U0322509800000000270100", 76 | "ST68000100010051845310112", 77 | "SV62CENR00000000000000700025", 78 | "TL380080012345678910157", 79 | "TN5910006035183598478831", 80 | "TR330006100519786457841326", 81 | "UA213223130000026007233566001", 82 | "VG96VPVG0000012345678901", 83 | "XK051212012345678906", 84 | "VA59001123000012345678", 85 | ]; 86 | 87 | for &i in valid_iban_countries.iter() { 88 | i.parse::()?; 89 | } 90 | Ok(()) 91 | } 92 | 93 | #[test] 94 | /// This test checks whether invalid country formats are recognized as such. 95 | fn test_invalid_country_format() -> Result<(), ParseIbanError> { 96 | let valid_iban_counties = [ 97 | "AD54BD012030200359100100", 98 | "AE32ABCD234567890123456", 99 | "AL84212110090000AB023569874", 100 | "AT24190430234533203672", 101 | "AZ75N00Z000000000137010001944", 102 | "BA6312900794010284AC", 103 | "BE095390075470", 104 | "BG83BN96611020345678", 105 | "BH93BG00001299123456", 106 | "BR15003605000010009795493C1", 107 | "BY56NBRB36009000002Z00AB00", 108 | ]; 109 | 110 | for &i in &valid_iban_counties { 111 | let base_iban = i.parse::()?; 112 | assert_eq!( 113 | i.parse::(), 114 | Err(ParseIbanError::InvalidBban(base_iban)) 115 | ); 116 | } 117 | Ok(()) 118 | } 119 | 120 | #[test] 121 | /// This test checks whether an iban with an unknown country is recognized as such. 122 | fn test_unknown_country() -> Result<(), ParseIbanError> { 123 | let iban_unknown_string = "ZZ07273912631298461"; 124 | let base_iban = iban_unknown_string.parse::()?; 125 | assert_eq!( 126 | iban_unknown_string.parse::(), 127 | Err(ParseIbanError::UnknownCountry(base_iban)) 128 | ); 129 | Ok(()) 130 | } 131 | -------------------------------------------------------------------------------- /iban_validate_afl_fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "iban_validate_afl_fuzz" 3 | version = "0.1.0" 4 | authors = ["thomasdh"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | iban_validate = { path = "../iban_validate" } 11 | afl = "0.11" 12 | -------------------------------------------------------------------------------- /iban_validate_afl_fuzz/Readme.md: -------------------------------------------------------------------------------- 1 | # How to use 2 | 3 | To use this crate for fuzzing, use the (rust fuzzing book](https://rust-fuzz.github.io/book/afl.html). Basically: 4 | 5 | ``` 6 | cargo afl build 7 | cargo afl fuzz -i inputs/ -o out ../target/debug/iban_validate_afl_fuzz 8 | ``` 9 | 10 | # Attribution 11 | The inputs in the file `wikipedia` come from the Wikipedia page on IBAN's and as such are available under the CC BY-SA license. 12 | -------------------------------------------------------------------------------- /iban_validate_afl_fuzz/inputs/input: -------------------------------------------------------------------------------- 1 | DE44500105175407324931 -------------------------------------------------------------------------------- /iban_validate_afl_fuzz/inputs/wikipedia: -------------------------------------------------------------------------------- 1 | BE71 0961 2345 6769 2 | BR15 0000 0000 0000 1093 2840 814 P2 3 | CR99 0000 0000 0000 8888 88 4 | FR76 3000 6000 0112 3456 7890 189 5 | IE12 BOFI 9000 0112 3456 78 6 | DE91 1000 0000 0123 4567 89 7 | GR96 0810 0010 0000 0123 4567 890 8 | MU43 BOMM 0101 1234 5678 9101 000 MUR 9 | PK70 BANK 0000 1234 5678 9000 10 | PL10 1050 0099 7603 1234 5678 9123 11 | RO09 BCYP 0000 0012 3456 7890 12 | LC14 BOSL 1234 5678 9012 3456 7890 1234 13 | SA44 2000 0001 2345 6789 1234 14 | ES79 2100 0813 6101 2345 6789 15 | SE87 3000 0000 0101 2345 6789 16 | CH56 0483 5012 3456 7800 9 17 | GB98 MIDL 0700 9312 3456 78 -------------------------------------------------------------------------------- /iban_validate_afl_fuzz/src/main.rs: -------------------------------------------------------------------------------- 1 | use afl::fuzz; 2 | use iban::Iban; 3 | 4 | fn main() { 5 | fuzz!(|data: &[u8]| { 6 | if let Ok(s) = std::str::from_utf8(data) { 7 | let _ = s.parse::(); 8 | } 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /iban_validate_registry_generation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "iban_validate_registry_generation" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1" 10 | nom = "7" 11 | csv = "1.1" 12 | -------------------------------------------------------------------------------- /iban_validate_registry_generation/README.md: -------------------------------------------------------------------------------- 1 | # iban_validate_registry_generation 2 | This crate can generate the repetitive country-specific code from the IBAN registry. 3 | 4 | The code tries to read the registry file from the local directory, and automatically generates the files `src/generated` for country specific code and `tests/registry_examples_generated.rs` for country specific tests. There are quite some errors and inconsistencies in the registry, so they cannot be used as tests directly. We try to fix them or mark them as unusable. See [`fix_inconsistencies`] for details. 5 | -------------------------------------------------------------------------------- /iban_validate_registry_generation/src/main.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | use std::{ 4 | fs::File, 5 | io::{Read, Write}, 6 | ops::Range, 7 | }; 8 | 9 | use csv::{ReaderBuilder, StringRecord, Trim}; 10 | 11 | use nom::{ 12 | branch::alt, 13 | bytes::complete::{tag, take, take_while}, 14 | character::complete::{alpha1, digit1, not_line_ending}, 15 | combinator::{eof, map, map_res}, 16 | multi::many1, 17 | sequence::{preceded, separated_pair, terminated}, 18 | IResult, 19 | }; 20 | use std::str::FromStr; 21 | 22 | #[derive(Debug)] 23 | struct RegistryRecord<'a> { 24 | country_code: &'a str, 25 | bban: &'a str, 26 | iban_electronic: &'a str, 27 | iban_print: &'a str, 28 | bank_identifier_position: Option>, 29 | bank_identifier_pattern: Option>, 30 | bank_identifier_example: Option<&'a str>, 31 | branch_identifier_position: Option>, 32 | branch_identifier_example: Option<&'a str>, 33 | iban_structure: Vec<(&'a str, &'a str)>, 34 | } 35 | 36 | impl<'a> RegistryRecord<'a> { 37 | /// Fix all errors, inconsistencies and missing entries in the registry. 38 | /// 39 | /// This method is immediately also a collection of the errors contained in 40 | /// the registry. For the most part, this is just a bank or branch item 41 | /// that does not match the IBAN, which is not wrong, it just mean we can't 42 | /// use it for testing. 43 | fn fix_inconsistencies(&mut self) { 44 | match self.country_code { 45 | "AL" => { 46 | // These seem to incorrectly include the branch as well as the 47 | // national check digit. Correct them manually. 48 | assert_eq!(self.bank_identifier_pattern, Some(vec!["8"])); 49 | assert_eq!(self.bank_identifier_example, Some("212-1100-9")); 50 | self.bank_identifier_pattern = Some(vec!["3"]); 51 | self.bank_identifier_example = Some("212"); 52 | 53 | // Correct branch range that was specified as exclusive where they should have been inclusive. 54 | self.branch_identifier_position.as_mut().unwrap().end -= 1; 55 | } 56 | "BA" => { 57 | // The BBAN does not match the IBAN. The bank and branch match 58 | // the BBAN. Manually fix all three to correspond to IBAN. 59 | assert_eq!(self.bban, "1990440001200279"); 60 | assert_eq!(self.bank_identifier_example, Some("199")); 61 | assert_eq!(self.branch_identifier_example, Some("044")); 62 | self.bban = "1290079401028494"; 63 | self.bank_identifier_example = Some("129"); 64 | self.branch_identifier_example = Some("007"); 65 | } 66 | "BI" => { 67 | // Pretty print format is incorrect, fix. 68 | assert_eq!(self.iban_print, "BI42 10000 10001 00003320451 81"); 69 | self.iban_print = "BI42 1000 0100 0100 0033 2045 181"; 70 | } 71 | "BR" => { 72 | // The BBAN differs by one letter. Fix. 73 | assert_eq!(self.bban, "00360305000010009795493P1"); 74 | self.bban = "00360305000010009795493C1"; 75 | } 76 | "CR" => { 77 | // The BBAN removes the leading '0'. Add it back. 78 | assert_eq!(self.bban, "15202001026284066"); 79 | self.bban = "015202001026284066"; 80 | } 81 | "FI" => { 82 | // Not provided, add manually 83 | assert!(self.bank_identifier_pattern.is_none()); 84 | self.bank_identifier_pattern = Some(vec!["3"]); 85 | 86 | // The BBAN is not provided, add manually as well. 87 | assert_eq!(self.bban, "N/A"); 88 | self.bban = "12345600000785"; 89 | } 90 | "IL" => { 91 | // This looks like a typo. There is one 0 missing in the BBAN. 92 | assert_eq!(self.bban, "010800000099999999"); 93 | self.bban = "0108000000099999999"; 94 | } 95 | "JO" => { 96 | // Fix the bank position. Perhaps it was indexed into the IBAN 97 | // instead of the BBAN? 98 | assert_eq!(self.bank_identifier_position, Some(4..8)); 99 | self.bank_identifier_position = Some(0..4); 100 | 101 | // There is no example of the branch even though there is a range. 102 | // We will just use the range and set the example manually. 103 | // https://www.xe.com/nl/ibancalculator/jordan/ 104 | assert!(self.branch_identifier_example.is_none()); 105 | self.branch_identifier_example = Some("0010"); 106 | 107 | // Note that the .PDF version of the registry is also 108 | // incorrect, but differently. The bank position should be 1-4 109 | // but is 5-8, the branch position should be 5-8 but is empty. 110 | } 111 | "LY" => { 112 | // Incorrect spacing. 113 | assert_eq!(self.iban_print, "LY83 002 048 000020100120361"); 114 | self.iban_print = "LY83 0020 4800 0020 1001 2036 1"; 115 | } 116 | "MK" => { 117 | // The bank identifier does not match the BBAN or IBAN. 118 | assert_eq!(self.bank_identifier_example, Some("300")); 119 | self.bank_identifier_example = Some("250"); 120 | } 121 | "NI" => { 122 | // Check digit incorrect! 123 | assert_eq!(self.iban_electronic, "NI04BAPR00000013000003558124"); 124 | assert_eq!(self.iban_print, "NI04 BAPR 0000 0013 0000 0355 8124"); 125 | self.iban_electronic = "NI45BAPR00000013000003558124"; 126 | self.iban_print = "NI45 BAPR 0000 0013 0000 0355 8124"; 127 | } 128 | "RU" => { 129 | // Check digit incorrect! 130 | assert_eq!(self.iban_electronic, "RU1704452522540817810538091310419"); 131 | assert_eq!(self.iban_print, "RU17 0445 2522 5408 1781 0538 0913 1041 9"); 132 | self.iban_electronic = "RU0304452522540817810538091310419"; 133 | self.iban_print = "RU03 0445 2522 5408 1781 0538 0913 1041 9"; 134 | } 135 | "SE" => { 136 | // The bank identifier does not match. 137 | assert_eq!(self.bank_identifier_example, Some("123")); 138 | self.bank_identifier_example = Some("500"); 139 | } 140 | "ST" => { 141 | // The IBAN and BBAN differ from the PDF, but the bank was not 142 | // updated. 143 | assert_eq!(self.bank_identifier_example, Some("0001")); 144 | self.bank_identifier_example = Some("0002"); 145 | 146 | // Check digit incorrect! 147 | assert_eq!(self.iban_electronic, "ST68000200010192194210112"); 148 | assert_eq!(self.iban_print, "ST68 0002 0001 0192 1942 1011 2"); 149 | self.iban_electronic = "ST32000200010192194210112"; 150 | self.iban_print = "ST32 0002 0001 0192 1942 1011 2"; 151 | } 152 | "SV" => { 153 | assert_eq!(self.iban_print, "SV 62 CENR 00000000000000700025"); 154 | self.iban_print = "SV62 CENR 0000 0000 0000 0070 0025"; 155 | } 156 | "VA" => { 157 | assert_eq!(self.iban_print, "VA59 001 1230 0001 2345 678"); 158 | self.iban_print = "VA59 0011 2300 0012 3456 78"; 159 | } 160 | _ => {} 161 | } 162 | } 163 | 164 | fn check(&mut self) { 165 | // Test for inconsistencies in the input file. We do this by 166 | // considering the bank identifier pattern (i.e. "4!n") and comparing 167 | // its length to the range. 168 | if let Some(bank_position) = &self.bank_identifier_position { 169 | let bank_pattern = self 170 | .bank_identifier_pattern 171 | .as_ref() 172 | .expect("we expect the bank pattern to be given if the position is"); 173 | 174 | // We compute the length from the pattern, i.e. "4!n" implies a 175 | // length of 4. Only the numbers have been retained during 176 | // parsing. 177 | let bank_identifier_length = bank_pattern 178 | .iter() 179 | .map(|len| len.parse::().unwrap()) 180 | .sum(); 181 | 182 | assert_eq!( 183 | bank_position.end - bank_position.start, 184 | bank_identifier_length, 185 | "expect the bank pattern length to be equal to the size of the range" 186 | ); 187 | 188 | // Get the example bank identifier. 189 | let bank_example: String = self 190 | .bank_identifier_example 191 | .expect("expected an example bank identifier") 192 | .chars() 193 | // Remove formatting like spaces and dashes. 194 | .filter(|c| c.is_ascii_alphanumeric()) 195 | .collect(); 196 | 197 | // We check that the bank identifier matches the claimed length. 198 | assert_eq!(bank_example.len(), bank_identifier_length); 199 | 200 | // As a final check, see if the example BBAN and and bank 201 | // identifier match. 202 | assert_eq!(self.bban[bank_position.clone()], bank_example); 203 | assert_eq!( 204 | self.iban_electronic[bank_position.start + 4..bank_position.end + 4], 205 | bank_example 206 | ); 207 | } else { 208 | // No bank position. We don't expect a length or an example either. 209 | assert!(self.bank_identifier_example.is_none()); 210 | assert!(self.bank_identifier_pattern.is_none()); 211 | } 212 | 213 | // Branch info 214 | if let Some(branch_position) = &self.branch_identifier_position { 215 | let branch_example = self.branch_identifier_example.expect("expected example"); 216 | assert_eq!( 217 | branch_example.len(), 218 | branch_position.len(), 219 | "expected branch example to match position" 220 | ); 221 | } else { 222 | assert!( 223 | self.branch_identifier_example.is_none(), 224 | "expected no example" 225 | ); 226 | } 227 | } 228 | } 229 | 230 | struct RegistryReader<'a> { 231 | records: Vec>, 232 | } 233 | 234 | impl<'a> RegistryReader<'a> { 235 | fn new(records_transposed: &'a [StringRecord]) -> anyhow::Result { 236 | let mut records: Vec> = (1..records_transposed[0].len()) 237 | .map(|i| -> anyhow::Result<_> { 238 | Ok(RegistryRecord { 239 | country_code: &records_transposed[2][i], 240 | bban: &records_transposed[16][i], 241 | iban_electronic: &records_transposed[21][i], 242 | iban_print: &records_transposed[22][i], 243 | bank_identifier_position: maybe(parse_range)(&records_transposed[10][i]) 244 | .unwrap() 245 | .1 246 | .map(|(start, end)| ((start - 1)..end)), 247 | bank_identifier_pattern: maybe(potentially_malformed_pattern)( 248 | &records_transposed[11][i], 249 | ) 250 | .unwrap() 251 | .1, 252 | bank_identifier_example: maybe(not_line_ending)(&records_transposed[14][i]) 253 | .unwrap() 254 | .1, 255 | branch_identifier_position: maybe(parse_range)(&records_transposed[12][i]) 256 | .unwrap() 257 | .1 258 | .map(|(start, end)| (start - 1)..end), 259 | branch_identifier_example: maybe(not_line_ending)(&records_transposed[15][i]) 260 | .unwrap() 261 | .1, 262 | iban_structure: iban_structure(&records_transposed[18][i]).unwrap().1, 263 | }) 264 | }) 265 | .collect::>() 266 | .unwrap(); 267 | for record in &mut records { 268 | record.fix_inconsistencies(); 269 | record.check(); 270 | } 271 | Ok(RegistryReader { records }) 272 | } 273 | } 274 | 275 | const FILE_PATH: &str = "./swift_iban_registry.txt"; 276 | 277 | /// Fix the UTF8 of a file by performing a lossless conversion. 278 | fn fix_utf8(file_name: &str) -> anyhow::Result<()> { 279 | // The file is invalid utf8, so we will first process it. 280 | let buf = { 281 | let mut file = File::open(file_name)?; 282 | let mut buf = Vec::new(); 283 | file.read_to_end(&mut buf)?; 284 | buf 285 | }; 286 | let contents = String::from_utf8_lossy(&buf); 287 | File::create(file_name)?.write_all(contents.as_bytes())?; 288 | Ok(()) 289 | } 290 | 291 | fn main() -> anyhow::Result<()> { 292 | // The registry file is actually invalid UTF8, so first try to fix it. 293 | fix_utf8(FILE_PATH)?; 294 | 295 | // By trimming and escaping double quotes we fix entries like `"1-5\n"` (double quotes included). 296 | let mut reader = ReaderBuilder::new() 297 | .delimiter(b'\t') 298 | .double_quote(true) 299 | .has_headers(false) 300 | .trim(Trim::All) 301 | .from_path(FILE_PATH)?; 302 | 303 | let records_transposed: Vec = reader.records().collect::>()?; 304 | let registry = RegistryReader::new(&records_transposed)?; 305 | 306 | // Generate this file for checking and getting country specific info. 307 | let mut generated_file = File::create("../iban_validate/src/generated.rs")?; 308 | writeln!(generated_file, "//! This file is automatically generated by `iban_validate_registry_generation` from the IBAN registry.")?; 309 | generate_bank_identifier_position_in_bban_match_arm(&mut generated_file, ®istry)?; 310 | writeln!(generated_file)?; 311 | generate_branch_identifier_position_in_bban_match_arm(&mut generated_file, ®istry)?; 312 | writeln!(generated_file)?; 313 | generate_format_match_arm(&mut generated_file, ®istry)?; 314 | 315 | // Generate this file with test cases. 316 | let mut generated_file = File::create("../iban_validate/tests/registry_examples_generated.rs")?; 317 | generate_test_file(&mut generated_file, ®istry)?; 318 | 319 | Ok(()) 320 | } 321 | 322 | fn generate_bank_identifier_position_in_bban_match_arm( 323 | mut writer: &mut impl Write, 324 | contents: &RegistryReader, 325 | ) -> anyhow::Result<()> { 326 | writeln!( 327 | writer, 328 | " 329 | /// Get the position of the bank in the BBAN. 330 | #[inline] 331 | pub(crate) fn bank_identifier(country_code: &str) -> Option> {{ 332 | \t#[allow(clippy::match_same_arms)] // For clarity, identical arms are not combined. 333 | \tmatch country_code {{" 334 | )?; 335 | for record in &contents.records { 336 | if let Some(bank_identifier_position) = &record.bank_identifier_position { 337 | writeln!( 338 | &mut writer, 339 | "\t\t\"{}\" => Some({}..{}),", 340 | record.country_code, bank_identifier_position.start, bank_identifier_position.end 341 | )?; 342 | } else { 343 | writeln!(&mut writer, "\t\t\"{}\" => None,", record.country_code)?; 344 | } 345 | } 346 | writeln!(writer, "\t\t_ => None,")?; 347 | writeln!(writer, "\t}}\n}}")?; 348 | Ok(()) 349 | } 350 | 351 | /// Parse using the inner function but accept an empty string or "N/A" as `None`. 352 | fn maybe<'a, T>( 353 | f: impl FnMut(&'a str) -> IResult<&'a str, T>, 354 | ) -> impl FnMut(&'a str) -> IResult<&'a str, Option> { 355 | alt((map(alt((eof, tag("N/A"))), |_| None), map(f, Some))) 356 | } 357 | 358 | fn parse_range(input: &str) -> IResult<&str, (usize, usize)> { 359 | separated_pair( 360 | map_res(digit1, usize::from_str), 361 | tag("-"), 362 | map_res(digit1, usize::from_str), 363 | )(input) 364 | } 365 | 366 | #[test] 367 | fn test_maybe_parse_range() { 368 | let mut maybe_parse_range = maybe(parse_range); 369 | assert_eq!(maybe_parse_range(""), Ok(("", None))); 370 | assert_eq!(maybe_parse_range("N/A"), Ok(("", None))); 371 | assert_eq!(maybe_parse_range("1-4"), Ok(("", Some((1, 4))))); 372 | } 373 | 374 | /// Generate match arms for the branch range in the IBAN. 375 | fn generate_branch_identifier_position_in_bban_match_arm( 376 | write: &mut impl Write, 377 | contents: &RegistryReader, 378 | ) -> anyhow::Result<()> { 379 | writeln!( 380 | write, 381 | "/// Get the position of the branch in the BBAN. 382 | #[inline] 383 | pub(crate) fn branch_identifier(country_code: &str) -> Option> {{ 384 | \t#[allow(clippy::match_same_arms)] // For clarity, identical arms are not combined. 385 | \tmatch country_code {{" 386 | )?; 387 | 388 | for record in &contents.records { 389 | if let Some(branch_position) = record.branch_identifier_position.clone() { 390 | writeln!( 391 | write, 392 | "\t\t\"{}\" => Some({}..{}),", 393 | record.country_code, branch_position.start, branch_position.end 394 | )?; 395 | } else { 396 | writeln!(write, "\t\t\"{}\" => None,", record.country_code)?; 397 | } 398 | } 399 | writeln!(write, "\t\t_ => None,")?; 400 | writeln!(write, "\t}}\n}}")?; 401 | Ok(()) 402 | } 403 | 404 | fn parse_pattern(contents: &str) -> IResult<&str, Vec<(&str, &str)>> { 405 | many1(separated_pair(digit1, tag("!"), alpha1))(contents) 406 | } 407 | 408 | /// Parse a misformed pattern. Now we're desperate: just find the numbers in the input and ignore the rest. 409 | fn parse_malformed_pattern(contents: &str) -> IResult<&str, Vec<&str>> { 410 | many1(terminated( 411 | digit1, 412 | take_while(|s: char| !s.is_ascii_digit()), 413 | ))(contents) 414 | } 415 | 416 | /// Parse a pattern that repeatedly contains the form "4!a". Only the length is stored. 417 | fn potentially_malformed_pattern(contents: &str) -> IResult<&str, Vec<&str>> { 418 | alt(( 419 | map(parse_pattern, |a: Vec<(&str, &str)>| { 420 | a.iter().map(|a| a.0).collect() 421 | }), 422 | parse_malformed_pattern, 423 | ))(contents) 424 | } 425 | 426 | fn iban_structure(contents: &str) -> IResult<&str, Vec<(&str, &str)>> { 427 | preceded( 428 | // Skip country code and check digits 429 | take(5_usize), 430 | parse_pattern, 431 | )(contents) 432 | } 433 | 434 | fn generate_format_match_arm( 435 | write: &mut impl Write, 436 | contents: &RegistryReader, 437 | ) -> anyhow::Result<()> { 438 | writeln!( 439 | write, 440 | "use crate::countries::CharacterType; 441 | 442 | #[inline] 443 | pub(crate) fn country_pattern(country_code: &str) -> Option<&[(usize, CharacterType)]> {{ 444 | \tuse CharacterType::*; 445 | \tuse core::borrow::Borrow; 446 | \tmatch country_code {{" 447 | )?; 448 | for record in &contents.records { 449 | // TODO: Maybe combine sequences of the same character. The compiler will probably optimize this anyway though. 450 | let pos_formatted = record 451 | .iban_structure 452 | .iter() 453 | .map(|(num, t)| format!("({}, {})", num, t.to_ascii_uppercase())) 454 | .collect::>() 455 | .join(", "); 456 | writeln!( 457 | write, 458 | "\t\t\"{}\" => Some([{}].borrow()),", 459 | record.country_code, pos_formatted 460 | )?; 461 | } 462 | writeln!(write, "\t\t_ => None")?; 463 | writeln!(write, "\t}}\n}}")?; 464 | Ok(()) 465 | } 466 | 467 | #[derive(Debug)] 468 | #[allow(dead_code)] // Allow since it is used for printing 469 | struct RegistryExample<'a> { 470 | country_code: &'a str, 471 | bank_identifier: Option<&'a str>, 472 | branch_identifier: Option<&'a str>, 473 | bban: &'a str, 474 | iban_electronic: &'a str, 475 | iban_print: &'a str, 476 | } 477 | 478 | fn generate_test_file(write: &mut impl Write, contents: &RegistryReader) -> anyhow::Result<()> { 479 | writeln!( 480 | write, 481 | "//! This file was automatically generated by `iban_validate_registry_generation`. 482 | 483 | pub struct RegistryExample<'a> {{ 484 | pub country_code: &'a str, 485 | pub bank_identifier: Option<&'a str>, 486 | pub branch_identifier: Option<&'a str>, 487 | pub bban: &'a str, 488 | pub iban_electronic: &'a str, 489 | pub iban_print: &'a str, 490 | }} 491 | 492 | pub const EXAMPLES: &[RegistryExample] = &{:#?};", 493 | contents 494 | .records 495 | .iter() 496 | .map(|record| RegistryExample { 497 | country_code: record.country_code, 498 | bank_identifier: record.bank_identifier_example, 499 | branch_identifier: record.branch_identifier_example, 500 | bban: record.bban, 501 | iban_electronic: record.iban_electronic, 502 | iban_print: record.iban_print, 503 | }) 504 | .collect::>() 505 | .as_slice() 506 | )?; 507 | Ok(()) 508 | } 509 | --------------------------------------------------------------------------------