#!/usr/bin/env python3 """ Generate Rust SSL error mapping code from OpenSSL sources. This is based on CPython's Tools/ssl/make_ssl_data.py but generates Rust code instead of C headers. It takes two arguments: - the path to the OpenSSL source tree (e.g. git checkout) - the path to the Rust file to be generated (e.g. stdlib/src/ssl/ssl_data.rs) - error codes are version specific """ import argparse import datetime import operator import os import re import sys parser = argparse.ArgumentParser( description="Generate ssl_data.rs from OpenSSL sources" ) parser.add_argument("srcdir", help="OpenSSL source directory") parser.add_argument("output", nargs="?", default=None) def _file_search(fname, pat): with open(fname, encoding="utf-8") as f: for line in f: match = pat.search(line) if match is not None: yield match def parse_err_h(args): """Parse err codes, e.g. ERR_LIB_X509: 11""" pat = re.compile(r"#\s*define\W+ERR_LIB_(\w+)\s+(\d+)") lib2errnum = {} for match in _file_search(args.err_h, pat): libname, num = match.groups() lib2errnum[libname] = int(num) return lib2errnum def parse_openssl_error_text(args): """Parse error reasons, X509_R_AKID_MISMATCH""" # ignore backslash line continuation for now pat = re.compile(r"^((\w+?)_R_(\w+)):(\d+):") for match in _file_search(args.errtxt, pat): reason, libname, errname, num = match.groups() if "_F_" in reason: # ignore function codes continue num = int(num) yield reason, libname, errname, num def parse_extra_reasons(args): """Parse extra reasons from openssl.ec""" pat = re.compile(r"^R\s+((\w+)_R_(\w+))\s+(\d+)") for match in _file_search(args.errcodes, pat): reason, libname, errname, num = match.groups() num = int(num) yield reason, libname, errname, num def gen_library_codes_rust(args): """Generate Rust phf map for library codes""" yield "// Maps lib_code -> library name" yield '// Example: 20 -> "SSL"' yield "pub static LIBRARY_CODES: phf::Map = phf_map! {" # Deduplicate: keep the last one if there are duplicates seen = {} for libname in sorted(args.lib2errnum): lib_num = args.lib2errnum[libname] seen[lib_num] = libname for lib_num in sorted(seen.keys()): libname = seen[lib_num] yield f' {lib_num}u32 => "{libname}",' yield "};" yield "" def gen_error_codes_rust(args): """Generate Rust phf map for error codes""" yield "// Maps encoded (lib, reason) -> error mnemonic" yield '// Example: encode_error_key(20, 134) -> "CERTIFICATE_VERIFY_FAILED"' yield "// Key encoding: (lib << 32) | reason" yield "pub static ERROR_CODES: phf::Map = phf_map! {" for reason, libname, errname, num in args.reasons: if libname not in args.lib2errnum: continue lib_num = args.lib2errnum[libname] # Encode (lib, reason) as single u64 key = (lib_num << 32) | num yield f' {key}u64 => "{errname}",' yield "};" yield "" def main(): args = parser.parse_args() args.err_h = os.path.join(args.srcdir, "include", "openssl", "err.h") if not os.path.isfile(args.err_h): # Fall back to infile for OpenSSL 3.0.0 args.err_h += ".in" args.errcodes = os.path.join(args.srcdir, "crypto", "err", "openssl.ec") args.errtxt = os.path.join(args.srcdir, "crypto", "err", "openssl.txt") if not os.path.isfile(args.errtxt): parser.error(f"File {args.errtxt} not found in srcdir\n.") # {X509: 11, ...} args.lib2errnum = parse_err_h(args) # [('X509_R_AKID_MISMATCH', 'X509', 'AKID_MISMATCH', 110), ...] reasons = [] reasons.extend(parse_openssl_error_text(args)) reasons.extend(parse_extra_reasons(args)) # sort by libname, numeric error code args.reasons = sorted(reasons, key=operator.itemgetter(0, 3)) lines = [ "// File generated by tools/make_ssl_data_rs.py", f"// Generated on {datetime.datetime.now(datetime.timezone.utc).isoformat()}", f"// Source: OpenSSL from {args.srcdir}", "// spell-checker: disable", "", "use phf::phf_map;", "", ] lines.extend(gen_library_codes_rust(args)) lines.extend(gen_error_codes_rust(args)) # Add helper function lines.extend( [ "/// Helper function to create encoded key from (lib, reason) pair", "#[inline]", "pub fn encode_error_key(lib: i32, reason: i32) -> u64 {", " ((lib as u64) << 32) | (reason as u64 & 0xFFFFFFFF)", "}", "", ] ) if args.output is None: for line in lines: print(line) else: with open(args.output, "w") as output: for line in lines: print(line, file=output) print(f"Generated {args.output}") print(f"Found {len(args.lib2errnum)} library codes") print(f"Found {len(args.reasons)} error codes") if __name__ == "__main__": main()