#!/usr/bin/env python3 """Generate library/ssl_debug_helpers_generated.c The code generated by this module includes debug helper functions that can not be implemented by fixed codes. """ # Copyright The Mbed TLS Contributors # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later import sys import re import os import textwrap import argparse from mbedtls_dev import build_tree def remove_c_comments(string): """ Remove C style comments from input string """ string_pattern = r"(?P\".*?\"|\'.*?\')" comment_pattern = r"(?P/\*.*?\*/|//[^\r\n]*$)" pattern = re.compile(string_pattern + r'|' + comment_pattern, re.MULTILINE | re.DOTALL) def replacer(match): if match.lastgroup == 'comment': return "" return match.group() return pattern.sub(replacer, string) class CondDirectiveNotMatch(Exception): pass def preprocess_c_source_code(source, *classes): """ Simple preprocessor for C source code. Only processes condition directives without expanding them. Yield object according to the classes input. Most match firstly If the directive pair does not match , raise CondDirectiveNotMatch. Assume source code does not include comments and compile pass. """ pattern = re.compile(r"^[ \t]*#[ \t]*" + r"(?P(if[ \t]|ifndef[ \t]|ifdef[ \t]|else|endif))" + r"[ \t]*(?P(.*\\\n)*.*$)", re.MULTILINE) stack = [] def _yield_objects(s, d, p, st, end): """ Output matched source piece """ nonlocal stack start_line, end_line = '', '' if stack: start_line = '#{} {}'.format(d, p) if d == 'if': end_line = '#endif /* {} */'.format(p) elif d == 'ifdef': end_line = '#endif /* defined({}) */'.format(p) else: end_line = '#endif /* !defined({}) */'.format(p) has_instance = False for cls in classes: for instance in cls.extract(s, st, end): if has_instance is False: has_instance = True yield pair_start, start_line yield instance.span()[0], instance if has_instance: yield start, end_line for match in pattern.finditer(source): directive = match.groupdict()['directive'].strip() param = match.groupdict()['param'] start, end = match.span() if directive in ('if', 'ifndef', 'ifdef'): stack.append((directive, param, start, end)) continue if not stack: raise CondDirectiveNotMatch() pair_directive, pair_param, pair_start, pair_end = stack.pop() yield from _yield_objects(source, pair_directive, pair_param, pair_end, start) if directive == 'endif': continue if pair_directive == 'if': directive = 'if' param = "!( {} )".format(pair_param) elif pair_directive == 'ifdef': directive = 'ifndef' param = pair_param else: directive = 'ifdef' param = pair_param stack.append((directive, param, start, end)) assert not stack, len(stack) class EnumDefinition: """ Generate helper functions around enumeration. Currently, it generate translation function from enum value to string. Enum definition looks like: [typedef] enum [prefix name] { [body] } [suffix name]; Known limitation: - the '}' and ';' SHOULD NOT exist in different macro blocks. Like ``` enum test { .... #if defined(A) .... }; #else .... }; #endif ``` """ @classmethod def extract(cls, source_code, start=0, end=-1): enum_pattern = re.compile(r'enum\s*(?P\w*)\s*' + r'{\s*(?P[^}]*)}' + r'\s*(?P\w*)\s*;', re.MULTILINE | re.DOTALL) for match in enum_pattern.finditer(source_code, start, end): yield EnumDefinition(source_code, span=match.span(), group=match.groupdict()) def __init__(self, source_code, span=None, group=None): assert isinstance(group, dict) prefix_name = group.get('prefix_name', None) suffix_name = group.get('suffix_name', None) body = group.get('body', None) assert prefix_name or suffix_name assert body assert span # If suffix_name exists, it is a typedef self._prototype = suffix_name if suffix_name else 'enum ' + prefix_name self._name = suffix_name if suffix_name else prefix_name self._body = body self._source = source_code self._span = span def __repr__(self): return 'Enum({},{})'.format(self._name, self._span) def __str__(self): return repr(self) def span(self): return self._span def generate_translation_function(self): """ Generate function for translating value to string """ translation_table = [] for line in self._body.splitlines(): if line.strip().startswith('#'): # Preprocess directive, keep it in table translation_table.append(line.strip()) continue if not line.strip(): continue for field in line.strip().split(','): if not field.strip(): continue member = field.strip().split()[0] translation_table.append( '{space}case {member}:\n{space} return "{member}";' .format(member=member, space=' '*8) ) body = textwrap.dedent('''\ const char *{name}_str( {prototype} in ) {{ switch (in) {{ {translation_table} default: return "UNKNOWN_VALUE"; }} }} ''') body = body.format(translation_table='\n'.join(translation_table), name=self._name, prototype=self._prototype) return body class SignatureAlgorithmDefinition: """ Generate helper functions for signature algorithms. It generates translation function from signature algorithm define to string. Signature algorithm definition looks like: #define MBEDTLS_TLS1_3_SIG_[ upper case signature algorithm ] [ value(hex) ] Known limitation: - the definitions SHOULD exist in same macro blocks. """ @classmethod def extract(cls, source_code, start=0, end=-1): sig_alg_pattern = re.compile(r'#define\s+(?PMBEDTLS_TLS1_3_SIG_\w+)\s+' + r'(?P0[xX][0-9a-fA-F]+)$', re.MULTILINE | re.DOTALL) matches = list(sig_alg_pattern.finditer(source_code, start, end)) if matches: yield SignatureAlgorithmDefinition(source_code, definitions=matches) def __init__(self, source_code, definitions=None): if definitions is None: definitions = [] assert isinstance(definitions, list) and definitions self._definitions = definitions self._source = source_code def __repr__(self): return 'SigAlgs({})'.format(self._definitions[0].span()) def span(self): return self._definitions[0].span() def __str__(self): """ Generate function for translating value to string """ translation_table = [] for m in self._definitions: name = m.groupdict()['name'] return_val = name[len('MBEDTLS_TLS1_3_SIG_'):].lower() translation_table.append( ' case {}:\n return "{}";'.format(name, return_val)) body = textwrap.dedent('''\ const char *mbedtls_ssl_sig_alg_to_str( uint16_t in ) {{ switch( in ) {{ {translation_table} }}; return "UNKNOWN"; }}''') body = body.format(translation_table='\n'.join(translation_table)) return body class NamedGroupDefinition: """ Generate helper functions for named group It generates translation function from named group define to string. Named group definition looks like: #define MBEDTLS_SSL_IANA_TLS_GROUP_[ upper case named group ] [ value(hex) ] Known limitation: - the definitions SHOULD exist in same macro blocks. """ @classmethod def extract(cls, source_code, start=0, end=-1): named_group_pattern = re.compile(r'#define\s+(?PMBEDTLS_SSL_IANA_TLS_GROUP_\w+)\s+' + r'(?P0[xX][0-9a-fA-F]+)$', re.MULTILINE | re.DOTALL) matches = list(named_group_pattern.finditer(source_code, start, end)) if matches: yield NamedGroupDefinition(source_code, definitions=matches) def __init__(self, source_code, definitions=None): if definitions is None: definitions = [] assert isinstance(definitions, list) and definitions self._definitions = definitions self._source = source_code def __repr__(self): return 'NamedGroup({})'.format(self._definitions[0].span()) def span(self): return self._definitions[0].span() def __str__(self): """ Generate function for translating value to string """ translation_table = [] for m in self._definitions: name = m.groupdict()['name'] iana_name = name[len('MBEDTLS_SSL_IANA_TLS_GROUP_'):].lower() translation_table.append(' case {}:\n return "{}";'.format(name, iana_name)) body = textwrap.dedent('''\ const char *mbedtls_ssl_named_group_to_str( uint16_t in ) {{ switch( in ) {{ {translation_table} }}; return "UNKOWN"; }}''') body = body.format(translation_table='\n'.join(translation_table)) return body OUTPUT_C_TEMPLATE = '''\ /* Automatically generated by generate_ssl_debug_helpers.py. DO NOT EDIT. */ /** * \\file ssl_debug_helpers_generated.c * * \\brief Automatically generated helper functions for debugging */ /* * Copyright The Mbed TLS Contributors * SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later * */ #include "common.h" #if defined(MBEDTLS_DEBUG_C) #include "ssl_debug_helpers.h" {functions} #endif /* MBEDTLS_DEBUG_C */ /* End of automatically generated file. */ ''' def generate_ssl_debug_helpers(output_directory, mbedtls_root): """ Generate functions of debug helps """ mbedtls_root = os.path.abspath( mbedtls_root or build_tree.guess_mbedtls_root()) with open(os.path.join(mbedtls_root, 'include/mbedtls/ssl.h')) as f: source_code = remove_c_comments(f.read()) definitions = dict() for start, instance in preprocess_c_source_code(source_code, EnumDefinition, SignatureAlgorithmDefinition, NamedGroupDefinition): if start in definitions: continue if isinstance(instance, EnumDefinition): definition = instance.generate_translation_function() else: definition = instance definitions[start] = definition function_definitions = [str(v) for _, v in sorted(definitions.items())] if output_directory == sys.stdout: sys.stdout.write(OUTPUT_C_TEMPLATE.format( functions='\n'.join(function_definitions))) else: with open(os.path.join(output_directory, 'ssl_debug_helpers_generated.c'), 'w') as f: f.write(OUTPUT_C_TEMPLATE.format( functions='\n'.join(function_definitions))) def main(): """ Command line entry """ parser = argparse.ArgumentParser() parser.add_argument('--mbedtls-root', nargs='?', default=None, help='root directory of mbedtls source code') parser.add_argument('output_directory', nargs='?', default='library', help='source/header files location') args = parser.parse_args() generate_ssl_debug_helpers(args.output_directory, args.mbedtls_root) return 0 if __name__ == '__main__': sys.exit(main())