iban-tools

Tools / code generators for IBAN validation
Log | Files | Refs

codegen_rs.py (5477B)


      1 #!/usr/bin/env python3
      2 
      3 import json
      4 
      5 with open("registry.json", "r") as json_file:
      6     registry = json.load(json_file)
      7 
      8 rust = """/*
      9   This file is part of TALER
     10   Copyright (C) 2025 Taler Systems SA
     11 
     12   TALER is free software; you can redistribute it and/or modify it under the
     13   terms of the GNU Affero General Public License as published by the Free Software
     14   Foundation; either version 3, or (at your option) any later version.
     15 
     16   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     17   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     18   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     19 
     20   You should have received a copy of the GNU Affero General Public License along with
     21   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     22 */
     23 
     24 use std::fmt::Display;
     25 
     26 use IbanC::*;
     27 use Country::*;
     28 
     29 /// IBAN ASCII characters rules
     30 #[derive(Debug, Copy, Clone, PartialEq, Eq)]
     31 pub enum IbanC {
     32     /// Digits (0-9)
     33     N,
     34     /// Uppercase (A-Z)
     35     A,
     36     /// Digits or uppercase (0-9 & A-Z)
     37     C,
     38 }
     39 
     40 impl IbanC {
     41     /// Check if a valid IBAN slice follow a specific characters rules
     42     pub fn check(self, iban_ascii: &[u8]) -> bool {
     43         // IBAN are made of ASCII digits and uppercase
     44         debug_assert!(iban_ascii
     45             .iter()
     46             .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()));
     47         // As all characters are ASCII digits or uppercase
     48         // we can use simple masks to check the character kind
     49         const MASK_IS_UPPERCASE: u8 = 0b0100_0000;
     50         const MASK_IS_DIGIT: u8 = 0b0010_0000;
     51 
     52         let mask = match self {
     53             Self::N => MASK_IS_UPPERCASE,
     54             Self::A => MASK_IS_DIGIT,
     55             Self::C => return true,
     56         };
     57         iban_ascii.iter().all(|b| (*b & mask) == 0)
     58     }
     59 }
     60 
     61 /// An IBAN pattern, an array of characters rules over a number of character
     62 pub type Pattern = &'static [(u8, IbanC)];
     63 
     64 #[derive(Debug)]
     65 pub enum PatternErr {
     66     Len(u8, usize),
     67     Malformed,
     68 }
     69 """
     70 
     71 
     72 def gen_match(name, return_type, match):
     73     global rust
     74     rust += f"pub const fn {name}(self) -> {return_type} {{\nmatch self {{\n"
     75     for r in registry:
     76         rust += f"{r['code']} => {match(r)},\n"
     77     rust += "}\n}\n\n"
     78 
     79 
     80 def gen_range(name):
     81     def fmt_range(r):
     82         (start, end) = r[name + "_range"] or [0, 0]
     83         return f"{start}..{end}"
     84 
     85     gen_match(f"{name}_id", "core::ops::Range<usize>", fmt_range)
     86 
     87 
     88 def gen_pattern(name):
     89     def fmt_pattern(r):
     90         pattern = r[name + "_rules"] or []
     91         fmt = "&["
     92         for repetition, char in pattern:
     93             fmt += f"({repetition}, {char.upper()}),"
     94         return fmt + "]"
     95 
     96     gen_match(f"{name}_pattern", "Pattern", fmt_pattern)
     97 
     98 
     99 rust += "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n"
    100 rust += "pub enum Country {\n"
    101 for r in registry:
    102     rust += f"    {r['code']},\n"
    103 rust += "}\n\n"
    104 rust += "impl Country {\n"
    105 rust += "pub fn from_str(country: &str) -> Option<Self> {\n"
    106 rust += "    match country {\n"
    107 for r in registry:
    108     rust += f'"{r["code"]}" => Some({r["code"]}),\n'
    109 rust += "        _ => None,\n"
    110 rust += "    }\n"
    111 rust += "}\n\n"
    112 gen_match("as_str", "&'static str", lambda r: f'"{r["code"]}"')
    113 gen_match("as_bytes", "&'static [u8; 2]", lambda r: f'b"{r["code"]}"')
    114 gen_match("iban_len", "usize", lambda r: f"{r['iban_len']}")
    115 rust += "pub const fn bban_len(self) -> usize {\n"
    116 rust += "    self.iban_len() - 4\n"
    117 rust += "}\n\n"
    118 gen_range("bank")
    119 gen_range("branch")
    120 gen_pattern("bban")
    121 gen_pattern("bank")
    122 gen_pattern("branch")
    123 rust += "}\n\n"
    124 
    125 rust += """
    126 impl Display for Country {
    127     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    128         f.write_str(self.as_str())
    129     }
    130 }
    131 
    132 /// Generate random ASCII string following an IBAN pattern rules
    133 pub fn rng_pattern(out: &mut [u8], pattern: Pattern) {
    134     let mut cursor = 0;
    135     for (len, rule) in pattern {
    136         let alphabet = match rule {
    137             IbanC::C => "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
    138             IbanC::N => "0123456789",
    139             IbanC::A => "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    140         };
    141         for b in &mut out[cursor..cursor + *len as usize] {
    142             *b = *fastrand::choice(alphabet.as_bytes()).unwrap();
    143         }
    144         cursor += *len as usize
    145     }
    146 }
    147 
    148 /// Valid an IBAN slice against a pattern
    149 pub fn check_pattern(iban_ascii: &[u8], pattern: Pattern) -> Result<(), PatternErr> {
    150     // IBAN are made of ASCII digits and uppercase
    151     debug_assert!(iban_ascii.iter().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()));
    152     let pattern_len: u8 = pattern.iter().map(|(len, _)| *len as u8).sum();
    153     if iban_ascii.len() != pattern_len as usize {
    154         return Err(PatternErr::Len(pattern_len, iban_ascii.len()));
    155     }
    156     let mut cursor = 0;
    157     for (repetition, char) in pattern {
    158         if !char.check(&iban_ascii[cursor..cursor + *repetition as usize]) {
    159             return Err(PatternErr::Malformed);
    160         }
    161         cursor += *repetition as usize;
    162     }
    163     Ok(())
    164 }
    165 
    166 """
    167 rust += "#[cfg(test)]\n"
    168 rust += f"pub const VALID_IBAN: [(&str, Option<&str>); {len(registry)}] = [\n"
    169 for r in registry:
    170     rust += f'("{r["iban_example"]}", '
    171     bban = r["bban_example"]
    172     if bban is None:
    173         rust += "None"
    174     else:
    175         bban = "".join(filter(str.isalnum, bban))
    176         rust += f'Some("{bban}")'
    177     rust += "),\n"
    178 rust += "]\n;"
    179 
    180 with open("registry.rs", "w") as rs_file:
    181     rs_file.write(rust)