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)