1#! /usr/bin/env python3
2
3"""
4This script should be called *manually* when we want to upgrade SSLError
5`library` and `reason` mnemonics to a more recent OpenSSL version.
6
7It takes two arguments:
8- the path to the OpenSSL source tree (e.g. git checkout)
9- the path to the C file to be generated
10  (probably Modules/_ssl_data.h)
11"""
12
13import datetime
14import glob
15import os
16import re
17import sys
18import _ssl
19
20
21def parse_error_codes(h_file, prefix, libcode):
22    pat = re.compile(r"#\s*define\W+(%s([\w]+))\W+(\d+)\b" % re.escape(prefix))
23    codes = []
24    with open(h_file, "r", encoding="latin1") as f:
25        for line in f:
26            match = pat.search(line)
27            if match:
28                code, name, num = match.groups()
29                num = int(num)
30                # e.g. ("SSL_R_BAD_DATA", ("ERR_LIB_SSL", "BAD_DATA", 390))
31                codes.append((code, (libcode, name, num)))
32    assert codes, f"no codes found in {h_file}"
33    return codes
34
35if __name__ == "__main__":
36    openssl_inc = sys.argv[1]
37    outfile = sys.argv[2]
38    use_stdout = outfile == '-'
39    f = sys.stdout if use_stdout else open(outfile, "w")
40    # mnemonic -> (library code, error prefix, header file)
41    error_libraries = {}
42    for error_header in glob.glob(os.path.join(glob.escape(openssl_inc), 'include/openssl/*err.h')):
43        base = os.path.basename(error_header)
44        if base in ('buffererr.h', 'objectserr.h', 'storeerr.h'):
45            # Deprecated in 3.0.
46            continue
47        mnemonic = base[:-5].upper()
48        if mnemonic == "":
49            # err.h
50            lib_codes = {
51                code: num
52                for (code, (_, _, num)) in parse_error_codes(error_header, 'ERR_LIB_', None)
53            }
54        else:
55            error_libraries[mnemonic] = (f'ERR_LIB_{mnemonic}', f'{mnemonic}_R_', error_header)
56
57    # Read codes from libraries
58    new_codes = []
59    for libcode, prefix, h_file in sorted(error_libraries.values()):
60        new_codes += parse_error_codes(h_file, prefix, libcode)
61    new_code_nums = set((libcode, num)
62                        for (code, (libcode, name, num)) in new_codes)
63
64    # Merge with existing codes (in case some old codes disappeared).
65    codes = {}
66    for errname, (libnum, errnum) in _ssl.err_names_to_codes.items():
67        lib = error_libraries[_ssl.lib_codes_to_names[libnum]]
68        libcode = lib[0]              # e.g. ERR_LIB_PEM
69        errcode = lib[1] + errname    # e.g. SSL_R_BAD_SSL_SESSION_ID_LENGTH
70        # Only keep it if the numeric codes weren't reused
71        if (libcode, errnum) not in new_code_nums:
72            codes[errcode] = libcode, errname, errnum
73    codes.update(dict(new_codes))
74
75    def w(l):
76        f.write(l + "\n")
77    w("/* File generated by Tools/ssl/make_ssl_data.py */")
78    w("/* Generated on %s */" % datetime.datetime.now().isoformat())
79    w("")
80
81    w("static struct py_ssl_library_code library_codes[] = {")
82    for mnemo, (libcode, _, _) in sorted(error_libraries.items()):
83        w(f'#ifdef {libcode}')
84        w('    {"%s", %s},' % (mnemo, libcode))
85        w('#endif')
86    w('    { NULL }')
87    w('};')
88    w("")
89
90    w("static struct py_ssl_error_code error_codes[] = {")
91    for errcode, (libcode, name, num) in sorted(codes.items()):
92        w('  #ifdef %s' % (errcode))
93        w('    {"%s", %s, %s},' % (name, libcode, errcode))
94        w('  #else')
95        w('    {"%s", %s, %d},' % (name, lib_codes[libcode], num))
96        w('  #endif')
97    w('    { NULL }')
98    w('};')
99    if not use_stdout:
100        f.close()
101