1#!/usr/bin/env python3
2
3"""Generate library/ssl_debug_helpers_generated.c
4
5The code generated by this module includes debug helper functions that can not be
6implemented by fixed codes.
7
8"""
9
10# Copyright The Mbed TLS Contributors
11# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
12import sys
13import re
14import os
15import textwrap
16import argparse
17from mbedtls_dev import build_tree
18
19
20def remove_c_comments(string):
21    """
22        Remove C style comments from input string
23    """
24    string_pattern = r"(?P<string>\".*?\"|\'.*?\')"
25    comment_pattern = r"(?P<comment>/\*.*?\*/|//[^\r\n]*$)"
26    pattern = re.compile(string_pattern + r'|' + comment_pattern,
27                         re.MULTILINE | re.DOTALL)
28
29    def replacer(match):
30        if match.lastgroup == 'comment':
31            return ""
32        return match.group()
33    return pattern.sub(replacer, string)
34
35
36class CondDirectiveNotMatch(Exception):
37    pass
38
39
40def preprocess_c_source_code(source, *classes):
41    """
42        Simple preprocessor for C source code.
43
44        Only processes condition directives without expanding them.
45        Yield object according to the classes input. Most match firstly
46
47        If the directive pair does not match , raise CondDirectiveNotMatch.
48
49        Assume source code does not include comments and compile pass.
50
51    """
52
53    pattern = re.compile(r"^[ \t]*#[ \t]*" +
54                         r"(?P<directive>(if[ \t]|ifndef[ \t]|ifdef[ \t]|else|endif))" +
55                         r"[ \t]*(?P<param>(.*\\\n)*.*$)",
56                         re.MULTILINE)
57    stack = []
58
59    def _yield_objects(s, d, p, st, end):
60        """
61            Output matched source piece
62        """
63        nonlocal stack
64        start_line, end_line = '', ''
65        if stack:
66            start_line = '#{} {}'.format(d, p)
67            if d == 'if':
68                end_line = '#endif /* {} */'.format(p)
69            elif d == 'ifdef':
70                end_line = '#endif /* defined({}) */'.format(p)
71            else:
72                end_line = '#endif /* !defined({}) */'.format(p)
73        has_instance = False
74        for cls in classes:
75            for instance in cls.extract(s, st, end):
76                if has_instance is False:
77                    has_instance = True
78                    yield pair_start, start_line
79                yield instance.span()[0], instance
80        if has_instance:
81            yield start, end_line
82
83    for match in pattern.finditer(source):
84
85        directive = match.groupdict()['directive'].strip()
86        param = match.groupdict()['param']
87        start, end = match.span()
88
89        if directive in ('if', 'ifndef', 'ifdef'):
90            stack.append((directive, param, start, end))
91            continue
92
93        if not stack:
94            raise CondDirectiveNotMatch()
95
96        pair_directive, pair_param, pair_start, pair_end = stack.pop()
97        yield from _yield_objects(source,
98                                  pair_directive,
99                                  pair_param,
100                                  pair_end,
101                                  start)
102
103        if directive == 'endif':
104            continue
105
106        if pair_directive == 'if':
107            directive = 'if'
108            param = "!( {} )".format(pair_param)
109        elif pair_directive == 'ifdef':
110            directive = 'ifndef'
111            param = pair_param
112        else:
113            directive = 'ifdef'
114            param = pair_param
115
116        stack.append((directive, param, start, end))
117    assert not stack, len(stack)
118
119
120class EnumDefinition:
121    """
122        Generate helper functions around enumeration.
123
124        Currently, it generate translation function from enum value to string.
125        Enum definition looks like:
126        [typedef] enum [prefix name] { [body] } [suffix name];
127
128        Known limitation:
129        - the '}' and ';' SHOULD NOT exist in different macro blocks. Like
130        ```
131        enum test {
132            ....
133        #if defined(A)
134            ....
135        };
136        #else
137            ....
138        };
139        #endif
140        ```
141    """
142
143    @classmethod
144    def extract(cls, source_code, start=0, end=-1):
145        enum_pattern = re.compile(r'enum\s*(?P<prefix_name>\w*)\s*' +
146                                  r'{\s*(?P<body>[^}]*)}' +
147                                  r'\s*(?P<suffix_name>\w*)\s*;',
148                                  re.MULTILINE | re.DOTALL)
149
150        for match in enum_pattern.finditer(source_code, start, end):
151            yield EnumDefinition(source_code,
152                                 span=match.span(),
153                                 group=match.groupdict())
154
155    def __init__(self, source_code, span=None, group=None):
156        assert isinstance(group, dict)
157        prefix_name = group.get('prefix_name', None)
158        suffix_name = group.get('suffix_name', None)
159        body = group.get('body', None)
160        assert prefix_name or suffix_name
161        assert body
162        assert span
163        # If suffix_name exists, it is a typedef
164        self._prototype = suffix_name if suffix_name else 'enum ' + prefix_name
165        self._name = suffix_name if suffix_name else prefix_name
166        self._body = body
167        self._source = source_code
168        self._span = span
169
170    def __repr__(self):
171        return 'Enum({},{})'.format(self._name, self._span)
172
173    def __str__(self):
174        return repr(self)
175
176    def span(self):
177        return self._span
178
179    def generate_translation_function(self):
180        """
181            Generate function for translating value to string
182        """
183        translation_table = []
184
185        for line in self._body.splitlines():
186
187            if line.strip().startswith('#'):
188                # Preprocess directive, keep it in table
189                translation_table.append(line.strip())
190                continue
191
192            if not line.strip():
193                continue
194
195            for field in line.strip().split(','):
196                if not field.strip():
197                    continue
198                member = field.strip().split()[0]
199                translation_table.append(
200                    '{space}case {member}:\n{space}    return "{member}";'
201                    .format(member=member, space=' '*8)
202                )
203
204        body = textwrap.dedent('''\
205            const char *{name}_str( {prototype} in )
206            {{
207                switch (in) {{
208            {translation_table}
209                    default:
210                        return "UNKNOWN_VALUE";
211                }}
212            }}
213                    ''')
214        body = body.format(translation_table='\n'.join(translation_table),
215                           name=self._name,
216                           prototype=self._prototype)
217        return body
218
219
220class SignatureAlgorithmDefinition:
221    """
222        Generate helper functions for signature algorithms.
223
224        It generates translation function from signature algorithm define to string.
225        Signature algorithm definition looks like:
226        #define MBEDTLS_TLS1_3_SIG_[ upper case signature algorithm ] [ value(hex) ]
227
228        Known limitation:
229        - the definitions SHOULD  exist in same macro blocks.
230    """
231
232    @classmethod
233    def extract(cls, source_code, start=0, end=-1):
234        sig_alg_pattern = re.compile(r'#define\s+(?P<name>MBEDTLS_TLS1_3_SIG_\w+)\s+' +
235                                     r'(?P<value>0[xX][0-9a-fA-F]+)$',
236                                     re.MULTILINE | re.DOTALL)
237        matches = list(sig_alg_pattern.finditer(source_code, start, end))
238        if matches:
239            yield SignatureAlgorithmDefinition(source_code, definitions=matches)
240
241    def __init__(self, source_code, definitions=None):
242        if definitions is None:
243            definitions = []
244        assert isinstance(definitions, list) and definitions
245        self._definitions = definitions
246        self._source = source_code
247
248    def __repr__(self):
249        return 'SigAlgs({})'.format(self._definitions[0].span())
250
251    def span(self):
252        return self._definitions[0].span()
253
254    def __str__(self):
255        """
256            Generate function for translating value to string
257        """
258        translation_table = []
259        for m in self._definitions:
260            name = m.groupdict()['name']
261            return_val = name[len('MBEDTLS_TLS1_3_SIG_'):].lower()
262            translation_table.append(
263                '    case {}:\n        return "{}";'.format(name, return_val))
264
265        body = textwrap.dedent('''\
266            const char *mbedtls_ssl_sig_alg_to_str( uint16_t in )
267            {{
268                switch( in )
269                {{
270            {translation_table}
271                }};
272
273                return "UNKNOWN";
274            }}''')
275        body = body.format(translation_table='\n'.join(translation_table))
276        return body
277
278
279class NamedGroupDefinition:
280    """
281        Generate helper functions for named group
282
283        It generates translation function from named group define to string.
284        Named group definition looks like:
285        #define MBEDTLS_SSL_IANA_TLS_GROUP_[ upper case named group ] [ value(hex) ]
286
287        Known limitation:
288        - the definitions SHOULD exist in same macro blocks.
289    """
290
291    @classmethod
292    def extract(cls, source_code, start=0, end=-1):
293        named_group_pattern = re.compile(r'#define\s+(?P<name>MBEDTLS_SSL_IANA_TLS_GROUP_\w+)\s+' +
294                                         r'(?P<value>0[xX][0-9a-fA-F]+)$',
295                                         re.MULTILINE | re.DOTALL)
296        matches = list(named_group_pattern.finditer(source_code, start, end))
297        if matches:
298            yield NamedGroupDefinition(source_code, definitions=matches)
299
300    def __init__(self, source_code, definitions=None):
301        if definitions is None:
302            definitions = []
303        assert isinstance(definitions, list) and definitions
304        self._definitions = definitions
305        self._source = source_code
306
307    def __repr__(self):
308        return 'NamedGroup({})'.format(self._definitions[0].span())
309
310    def span(self):
311        return self._definitions[0].span()
312
313    def __str__(self):
314        """
315            Generate function for translating value to string
316        """
317        translation_table = []
318        for m in self._definitions:
319            name = m.groupdict()['name']
320            iana_name = name[len('MBEDTLS_SSL_IANA_TLS_GROUP_'):].lower()
321            translation_table.append('    case {}:\n        return "{}";'.format(name, iana_name))
322
323        body = textwrap.dedent('''\
324            const char *mbedtls_ssl_named_group_to_str( uint16_t in )
325            {{
326                switch( in )
327                {{
328            {translation_table}
329                }};
330
331                return "UNKOWN";
332            }}''')
333        body = body.format(translation_table='\n'.join(translation_table))
334        return body
335
336
337OUTPUT_C_TEMPLATE = '''\
338/* Automatically generated by generate_ssl_debug_helpers.py. DO NOT EDIT. */
339
340/**
341 * \\file ssl_debug_helpers_generated.c
342 *
343 * \\brief Automatically generated helper functions for debugging
344 */
345/*
346 *  Copyright The Mbed TLS Contributors
347 *  SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
348 *
349 */
350
351#include "common.h"
352
353#if defined(MBEDTLS_DEBUG_C)
354
355#include "ssl_debug_helpers.h"
356
357{functions}
358
359#endif /* MBEDTLS_DEBUG_C */
360/* End of automatically generated file. */
361
362'''
363
364
365def generate_ssl_debug_helpers(output_directory, mbedtls_root):
366    """
367        Generate functions of debug helps
368    """
369    mbedtls_root = os.path.abspath(
370        mbedtls_root or build_tree.guess_mbedtls_root())
371    with open(os.path.join(mbedtls_root, 'include/mbedtls/ssl.h')) as f:
372        source_code = remove_c_comments(f.read())
373
374    definitions = dict()
375    for start, instance in preprocess_c_source_code(source_code,
376                                                    EnumDefinition,
377                                                    SignatureAlgorithmDefinition,
378                                                    NamedGroupDefinition):
379        if start in definitions:
380            continue
381        if isinstance(instance, EnumDefinition):
382            definition = instance.generate_translation_function()
383        else:
384            definition = instance
385        definitions[start] = definition
386
387    function_definitions = [str(v) for _, v in sorted(definitions.items())]
388    if output_directory == sys.stdout:
389        sys.stdout.write(OUTPUT_C_TEMPLATE.format(
390            functions='\n'.join(function_definitions)))
391    else:
392        with open(os.path.join(output_directory, 'ssl_debug_helpers_generated.c'), 'w') as f:
393            f.write(OUTPUT_C_TEMPLATE.format(
394                functions='\n'.join(function_definitions)))
395
396
397def main():
398    """
399    Command line entry
400    """
401    parser = argparse.ArgumentParser()
402    parser.add_argument('--mbedtls-root', nargs='?', default=None,
403                        help='root directory of mbedtls source code')
404    parser.add_argument('output_directory', nargs='?',
405                        default='library', help='source/header files location')
406
407    args = parser.parse_args()
408
409    generate_ssl_debug_helpers(args.output_directory, args.mbedtls_root)
410    return 0
411
412
413if __name__ == '__main__':
414    sys.exit(main())
415