iban-tools

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

commit c0ee1fd098974aa34230cb73021f2578f0cfe8fc
parent 1a693daa459cf24202142db2763f08d97ddc6e70
Author: Antoine A <>
Date:   Fri, 21 Feb 2025 17:58:55 +0100

Improve rs codegen

Diffstat:
Mcodegen_rs.py | 62++++++++++++++++++++++++++------------------------------------
Mparse_registry.py | 27+++++++++++++++++----------
2 files changed, 43 insertions(+), 46 deletions(-)

diff --git a/codegen_rs.py b/codegen_rs.py @@ -68,18 +68,20 @@ pub enum PatternErr { } """ + def gen_match(name, return_type, match): global rust rust += f"pub const fn {name}(self) -> {return_type} {{\nmatch self {{\n" for r in registry: - rust += f"{r['code']} => {match(r)},\n" + rust += f"{r['code']} => {match(r)},\n" rust += "}\n}\n\n" - + def gen_range(name): def fmt_range(r): (start, end) = r[name + "_range"] or [0, 0] return f"{start}..{end}" + gen_match(f"{name}_id", "core::ops::Range<usize>", fmt_range) @@ -90,36 +92,9 @@ def gen_pattern(name): for repetition, char in pattern: fmt += f"({repetition}, {char.upper()})," return fmt + "]" - gen_match(f"{name}_pattern", "Pattern", fmt_pattern) + gen_match(f"{name}_pattern", "Pattern", fmt_pattern) -def gen_check(name): - global rust - rust += f"""pub fn {name}_check(self, iban_ascii: &[u8]) -> Result<(), PatternErr> {{ - // IBAN are made of ASCII digits and uppercase - debug_assert!(iban_ascii.iter().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())); - match self {{ - """ - for r in registry: - pattern = r[name + "_rules"] or [] - length = r[name + "_len"] - rust += f"{r['code']} => {{\n" - rust += f" if iban_ascii.len() != {length} {{" - rust += f" return Err(PatternErr::Len({length}, iban_ascii.len()))\n" - rust += " }" - if len(pattern) != 0: - cursor = 0 - rust += " else if " - for repetition, char in pattern: - if cursor > 0: - rust += " || " - rust += f"!{char.upper()}.check(&iban_ascii[{cursor}..{cursor + repetition}])" - cursor += repetition - rust += """ { - return Err(PatternErr::Malformed) - }""" - rust += "\n}\n" - rust += "}\nOk(())\n}\n" rust += "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n" rust += "pub enum Country {\n" @@ -135,7 +110,7 @@ rust += " _ => None,\n" rust += " }\n" rust += "}\n\n" gen_match("as_str", "&'static str", lambda r: f'"{r["code"]}"') -gen_match("as_bytes", "[u8; 2]", lambda r: f"[b'{r['code'][0]}', b'{r['code'][1]}']") +gen_match("as_bytes", "&'static [u8; 2]", lambda r: f'b"{r["code"]}"') gen_match("iban_len", "usize", lambda r: f"{r['iban_len']}") rust += "pub const fn bban_len(self) -> usize {\n" rust += " self.iban_len() - 4\n" @@ -143,11 +118,8 @@ rust += "}\n\n" gen_range("bank") gen_range("branch") gen_pattern("bban") -# gen_pattern("bank") -# gen_pattern("branch") -gen_check("bban") -# gen_check("bank") -# gen_check("branch") +gen_pattern("bank") +gen_pattern("branch") rust += "}\n\n" rust += """ @@ -173,6 +145,24 @@ pub fn rng_pattern(out: &mut [u8], pattern: Pattern) { } } +/// Valid an IBAN slice against a pattern +pub fn check_pattern(iban_ascii: &[u8], pattern: Pattern) -> Result<(), PatternErr> { + // IBAN are made of ASCII digits and uppercase + debug_assert!(iban_ascii.iter().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())); + let pattern_len: u8 = pattern.iter().map(|(len, _)| *len as u8).sum(); + if iban_ascii.len() != pattern_len as usize { + return Err(PatternErr::Len(pattern_len, iban_ascii.len())); + } + let mut cursor = 0; + for (repetition, char) in pattern { + if !char.check(&iban_ascii[cursor..cursor + *repetition as usize]) { + return Err(PatternErr::Malformed); + } + cursor += *repetition as usize; + } + Ok(()) +} + """ rust += "#[cfg(test)]\n" rust += f"pub const VALID_IBAN: [(&str, Option<&str>); {len(registry)}] = [\n" diff --git a/parse_registry.py b/parse_registry.py @@ -22,7 +22,7 @@ def parse_line(prefix): return parts -def parse_list_line(prefix): +def parse_countries(prefix): line = parse_line(prefix) return list(map(lambda x: [] if x is None else x.strip('"').split(", "), line)) @@ -38,6 +38,8 @@ def parse_int_line(prefix): def parse_pattern(encoded): + if encoded is None: + return (0, [], "") assert re.match(STRUCTURE_PATTERN, encoded), f"{STRUCTURE_PATTERN} {encoded}" pattern_len = 0 rules = [] @@ -74,20 +76,20 @@ def parse_range(range): parse_line("Data element") country_names = parse_line("Name of country") country_code = parse_line("IBAN prefix country code (ISO 3166)") -country_code_include = parse_list_line( +country_code_include = parse_countries( "Country code includes other countries/territories" ) sepa = parse_bool_line("SEPA country") -sepa_include = parse_list_line("SEPA country also includes") +sepa_include = parse_countries("SEPA country also includes") account_example = parse_line("Domestic account number example") parse_line("BBAN") bban_patterns = parse_line("BBAN structure") bban_len = parse_int_line("BBAN length") bank_range = parse_line("Bank identifier position within the BBAN") -bban_bank_structure = parse_line("Bank identifier pattern") +bank_patterns = parse_line("Bank identifier pattern") branch_range = parse_line("Branch identifier position within the BBAN") -bban_branch_structure = parse_line("Branch identifier pattern") +branch_patterns = parse_line("Branch identifier pattern") bban_bank_example = parse_line("Bank identifier example") bban_branch_example = parse_line("Branch identifier example") bban_example = parse_line("BBAN example") @@ -111,13 +113,13 @@ for i in range(len(country_names)): elif code == "NO": bban_patterns[i] = "4!n6!n1!n" elif code == "AL": - bban_bank_structure[i] = "3!n" - bban_branch_structure[i] = "5!n" + bank_patterns[i] = "3!n" + branch_patterns[i] = "5!n" elif code == "EG": - bban_bank_structure[i] += "n" - bban_branch_structure[i] += "n" + bank_patterns[i] += "n" + branch_patterns[i] += "n" elif code == "FI": - bban_bank_structure[i] = "3!n" + bank_patterns[i] = "3!n" elif code == "BA": # The BBAN does not match the IBAN. The bank and branch match # the BBAN. Manually fix all three to correspond to IBAN. @@ -147,6 +149,9 @@ for i in range(len(country_names)): (bban_length, bban_rules, bban_regex) = parse_pattern(bban_pattern) assert bban_len[i] == bban_length == iban_len[i] - 4 + (_, bank_rules, _) = parse_pattern(bank_patterns[i]) + (_, branch_rules, _) = parse_pattern(branch_patterns[i]) + # if bban_bank[i] is not None: # assert range_len(bban_bank[i]) == structure_len(bban_bank_structure[i]) # if bban_branch[i] is not None: @@ -166,7 +171,9 @@ for i in range(len(country_names)): "bban_regex": bban_regex, "bban_example": bban_example[i], "bank_range": parse_range(bank_range[i]), + "bank_rules": bank_rules, "branch_range": parse_range(branch_range[i]), + "branch_rules": branch_rules, } )