1#!/usr/bin/python3
2# Extract information from C headers.
3# Copyright (C) 2018-2021 Free Software Foundation, Inc.
4# This file is part of the GNU C Library.
5#
6# The GNU C Library is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# The GNU C Library is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with the GNU C Library; if not, see
18# <https://www.gnu.org/licenses/>.
19
20import os.path
21import re
22import subprocess
23import tempfile
24
25
26def compute_c_consts(sym_data, cc):
27    """Compute the values of some C constants.
28
29    The first argument is a list whose elements are either strings
30    (preprocessor directives, or the special string 'START' to
31    indicate this function should insert its initial boilerplate text
32    in the output there) or pairs of strings (a name and a C
33    expression for the corresponding value).  Preprocessor directives
34    in the middle of the list may be used to select which constants
35    end up being evaluated using which expressions.
36
37    """
38    out_lines = []
39    for arg in sym_data:
40        if isinstance(arg, str):
41            if arg == 'START':
42                out_lines.append('void\ndummy (void)\n{')
43            else:
44                out_lines.append(arg)
45            continue
46        name = arg[0]
47        value = arg[1]
48        out_lines.append('asm ("/* @@@name@@@%s@@@value@@@%%0@@@end@@@ */" '
49                         ': : \"i\" ((long int) (%s)));'
50                         % (name, value))
51    out_lines.append('}')
52    out_lines.append('')
53    out_text = '\n'.join(out_lines)
54    with tempfile.TemporaryDirectory() as temp_dir:
55        c_file_name = os.path.join(temp_dir, 'test.c')
56        s_file_name = os.path.join(temp_dir, 'test.s')
57        with open(c_file_name, 'w') as c_file:
58            c_file.write(out_text)
59        # Compilation has to be from stdin to avoid the temporary file
60        # name being written into the generated dependencies.
61        cmd = ('%s -S -o %s -x c - < %s' % (cc, s_file_name, c_file_name))
62        subprocess.check_call(cmd, shell=True)
63        consts = {}
64        with open(s_file_name, 'r') as s_file:
65            for line in s_file:
66                match = re.search('@@@name@@@([^@]*)'
67                                  '@@@value@@@[^0-9Xxa-fA-F-]*'
68                                  '([0-9Xxa-fA-F-]+).*@@@end@@@', line)
69                if match:
70                    if (match.group(1) in consts
71                        and match.group(2) != consts[match.group(1)]):
72                        raise ValueError('duplicate constant %s'
73                                         % match.group(1))
74                    consts[match.group(1)] = match.group(2)
75        return consts
76
77
78def list_macros(source_text, cc):
79    """List the preprocessor macros defined by the given source code.
80
81    The return value is a pair of dicts, the first one mapping macro
82    names to their expansions and the second one mapping macro names
83    to lists of their arguments, or to None for object-like macros.
84
85    """
86    with tempfile.TemporaryDirectory() as temp_dir:
87        c_file_name = os.path.join(temp_dir, 'test.c')
88        i_file_name = os.path.join(temp_dir, 'test.i')
89        with open(c_file_name, 'w') as c_file:
90            c_file.write(source_text)
91        cmd = ('%s -E -dM -o %s %s' % (cc, i_file_name, c_file_name))
92        subprocess.check_call(cmd, shell=True)
93        macros_exp = {}
94        macros_args = {}
95        with open(i_file_name, 'r') as i_file:
96            for line in i_file:
97                match = re.fullmatch('#define ([0-9A-Za-z_]+)(.*)\n', line)
98                if not match:
99                    raise ValueError('bad -dM output line: %s' % line)
100                name = match.group(1)
101                value = match.group(2)
102                if value.startswith(' '):
103                    value = value[1:]
104                    args = None
105                elif value.startswith('('):
106                    match = re.fullmatch(r'\((.*?)\) (.*)', value)
107                    if not match:
108                        raise ValueError('bad -dM output line: %s' % line)
109                    args = match.group(1).split(',')
110                    value = match.group(2)
111                else:
112                    raise ValueError('bad -dM output line: %s' % line)
113                if name in macros_exp:
114                    raise ValueError('duplicate macro: %s' % line)
115                macros_exp[name] = value
116                macros_args[name] = args
117    return macros_exp, macros_args
118
119
120def compute_macro_consts(source_text, cc, macro_re, exclude_re=None):
121    """Compute the integer constant values of macros defined by source_text.
122
123    Macros must match the regular expression macro_re, and if
124    exclude_re is defined they must not match exclude_re.  Values are
125    computed with compute_c_consts.
126
127    """
128    macros_exp, macros_args = list_macros(source_text, cc)
129    macros_set = {m for m in macros_exp
130                  if (macros_args[m] is None
131                      and re.fullmatch(macro_re, m)
132                      and (exclude_re is None
133                           or not re.fullmatch(exclude_re, m)))}
134    sym_data = [source_text, 'START']
135    sym_data.extend(sorted((m, m) for m in macros_set))
136    return compute_c_consts(sym_data, cc)
137
138
139def compare_macro_consts(source_1, source_2, cc, macro_re, exclude_re=None,
140                         allow_extra_1=False, allow_extra_2=False):
141    """Compare the values of macros defined by two different sources.
142
143    The sources would typically be includes of a glibc header and a
144    kernel header.  If allow_extra_1, the first source may define
145    extra macros (typically if the kernel headers are older than the
146    version glibc has taken definitions from); if allow_extra_2, the
147    second source may define extra macros (typically if the kernel
148    headers are newer than the version glibc has taken definitions
149    from).  Return 1 if there were any differences other than those
150    allowed, 0 if the macro values were the same apart from any
151    allowed differences.
152
153    """
154    macros_1 = compute_macro_consts(source_1, cc, macro_re, exclude_re)
155    macros_2 = compute_macro_consts(source_2, cc, macro_re, exclude_re)
156    if macros_1 == macros_2:
157        return 0
158    print('First source:\n%s\n' % source_1)
159    print('Second source:\n%s\n' % source_2)
160    ret = 0
161    for name, value in sorted(macros_1.items()):
162        if name not in macros_2:
163            print('Only in first source: %s' % name)
164            if not allow_extra_1:
165                ret = 1
166        elif macros_1[name] != macros_2[name]:
167            print('Different values for %s: %s != %s'
168                  % (name, macros_1[name], macros_2[name]))
169            ret = 1
170    for name in sorted(macros_2.keys()):
171        if name not in macros_1:
172            print('Only in second source: %s' % name)
173            if not allow_extra_2:
174                ret = 1
175    return ret
176