blob: f86d64b69a11ff71a95179e40e41327492c78a9e [file] [log] [blame]
Gilles Peskineb4063892019-07-27 21:36:44 +02001#!/usr/bin/env python3
2
3"""Mbed TLS configuration file manipulation library and tool
4
5Basic usage, to read the Mbed TLS or Mbed Crypto configuration:
6 config = ConfigFile()
7 if 'MBEDTLS_RSA_C' in config: print('RSA is enabled')
8"""
9
10## Copyright (C) 2019, ARM Limited, All Rights Reserved
11## SPDX-License-Identifier: Apache-2.0
12##
13## Licensed under the Apache License, Version 2.0 (the "License"); you may
14## not use this file except in compliance with the License.
15## You may obtain a copy of the License at
16##
17## http://www.apache.org/licenses/LICENSE-2.0
18##
19## Unless required by applicable law or agreed to in writing, software
20## distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
21## WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22## See the License for the specific language governing permissions and
23## limitations under the License.
24##
25## This file is part of Mbed TLS (https://tls.mbed.org)
26
27import re
28
29class Setting:
30 """Representation of one Mbed TLS config.h setting.
31
32 Fields:
33 * name: the symbol name ('MBEDTLS_xxx').
34 * value: the value of the macro. The empty string for a plain #define
35 with no value.
36 * active: True if name is defined, False if a #define for name is
37 present in config.h but commented out.
38 """
39 # pylint: disable=too-few-public-methods
40 def __init__(self, active, name, value=''):
41 self.active = active
42 self.name = name
43 self.value = value
44
45class Config:
46 """Representation of the Mbed TLS configuration.
47
48 In the documentation of this class, a symbol is said to be *active*
49 if there is a #define for it that is not commented out, and *known*
50 if there is a #define for it whether commented out or not.
51
52 This class supports the following protocols:
53 * `name in config` is True if the symbol `name` is set in the
54 configuration, False otherwise (whether `name` is known but commented
55 out or not known at all).
56 * `config[name]` is the value of the macro `name`. If `name` is not
57 set, raise `KeyError` (even if a definition for `name` is present
58 but commented out).
59 * `config[name] = value` sets the value associated to `name`. `name`
60 must be known, but does not need to be set. This does not cause
61 name to become set.
62 """
63
64 def __init__(self):
65 self.settings = {}
66
67 def __contains__(self, name):
68 """True if the given symbol is active (i.e. set).
69
70 False if the given symbol is not set, even if a definition
71 is present but commented out.
72 """
73 return name in self.settings and self.settings[name].active
74
75 def all(self, *names):
76 """True if all the elements of names are active (i.e. set)."""
77 return all(self.__contains__(name) for name in names)
78
79 def any(self, *names):
80 """True if at least one symbol in names are active (i.e. set)."""
81 return any(self.__contains__(name) for name in names)
82
83 def known(self, name):
84 """True if a #define for name is present, whether it's commented out or not."""
85 return name in self.settings
86
87 def __getitem__(self, name):
88 """Get the value of name, i.e. what the preprocessor symbol expands to.
89
90 If name is not known, raise KeyError. name does not need to be active.
91 """
92 return self.settings[name].value
93
94 def get(self, name, default=None):
95 """Get the value of name. If name is inactive (not set), return default.
96
97 If a #define for name is present and not commented out, return
98 its expansion, even if this is the empty string.
99
100 If a #define for name is present but commented out, return default.
101 """
102 if name in self.settings:
103 return self.settings[name].value
104 else:
105 return default
106
107 def __setitem__(self, name, value):
108 """If name is known, set its value.
109
110 If name is not known, raise KeyError.
111 """
112 self.settings[name].value = value
113
114 def set(self, name, value=None):
115 """Set name to the given value and make it active.
116
117 If value is None and name is already known, don't change its value.
118 If value is None and name is not known, set its value to the empty
119 string.
120 """
121 if name in self.settings:
122 if value is not None:
123 self.settings[name].value = value
124 self.settings[name].active = True
125 else:
126 self.settings[name] = Setting(True, name, value=value)
127
128 def unset(self, name):
129 """Make name unset (inactive).
130
131 name remains known.
132 """
133 self.set(name)
134 self.settings[name].active = False
135
136 def adapt(self, adapter):
137 """Run adapter on each known symbol and (de)activate it accordingly.
138
139 `adapter` must be a function that returns a boolean. It is called as
140 `adapter(name, active)` for each setting, where `active` is `True`
141 if `name` is set and `False` if `name` is known but unset. If
142 `adapter` returns `True`, then set `name` (i.e. make it active),
143 otherwise unset `name` (i.e. make it known but inactive).
144 """
145 for setting in self.settings.values():
146 setting.active = adapter(setting.name, setting.active)
147
148def realfull_adapter(_name, _set):
149 """Uncomment everything."""
150 return True
151
152class ConfigFile(Config):
153 """Representation of the Mbed TLS configuration read for a file.
154
155 See the documentation of the `Config` class for methods to query
156 and modify the configuration.
157 """
158
159 default_path = 'include/mbedtls/config.h'
160
161 def __init__(self, filename=None):
162 """Read the Mbed TLS configuration file."""
163 if filename is None:
164 filename = self.default_path
165 super().__init__()
166 self.filename = filename
167 with open(filename) as file:
168 self.templates = [self._parse_line(line) for line in file]
169
170 def set(self, name, value=None):
171 if name not in self.settings:
172 self.templates.append((name, '', '#define ' + name + ' '))
173 super().set(name, value)
174
175 _define_line_regexp = (r'(?P<indentation>\s*)' +
176 r'(?P<commented_out>(//\s*)?)' +
177 r'(?P<define>#\s*define\s+)' +
178 r'(?P<name>\w+)' +
179 r'(?P<arguments>(?:\((?:\w|\s|,)*\))?)' +
180 r'(?P<separator>\s*)' +
181 r'(?P<value>.*)')
182 def _parse_line(self, line):
183 """Parse a line in config.h and return the corresponding template."""
184 line = line.rstrip('\r\n')
185 m = re.match(self._define_line_regexp, line)
186 if m:
187 active = not m.group('commented_out')
188 name = m.group('name')
189 value = m.group('value')
190 template = (name,
191 m.group('indentation'),
192 m.group('define') + name +
193 m.group('arguments') + m.group('separator'))
194 self.settings[name] = Setting(active, name, value)
195 return template
196 else:
197 return line
198
199 def _format_template(self, name, indent, middle):
200 """Build a line for config.h for the given setting.
201
202 The line has the form "<indent>#define <name><middle> <value>".
203 """
204 setting = self.settings[name]
205 return ''.join([indent,
206 '' if setting.active else '//',
207 middle,
208 setting.value]).rstrip()
209
210 def write_to_stream(self, output):
211 """Write the whole configuration to output."""
212 for template in self.templates:
213 if isinstance(template, str):
214 line = template
215 else:
216 line = self._format_template(*template)
217 output.write(line + '\n')
218
219 def write(self, filename=None):
220 """Write the whole configuration to the file it was read from.
221
222 If filename is specified, write to this file instead.
223 """
224 if filename is None:
225 filename = self.filename
226 with open(filename, 'w') as output:
227 self.write_to_stream(output)
228
229if __name__ == '__main__':
230 def main():
231 """Command line config.h manipulation tool."""
232 parser = argparse.ArgumentParser(description="""
233 Mbed TLS and Mbed Crypto configuration file manipulation tool.
234 """)
235 parser.add_argument('--file', '-f',
236 help="""File to read (and modify if requested).
237 Default: {}.
238 """.format(ConfigFile.default_path))
239 parser.add_argument('--force', '-o',
240 help="""For the set command, if SYMBOL is not
241 present, add a definition for it.""")
242 subparsers = parser.add_subparsers(dest='command',
243 title='Commands')
244 parser_get = subparsers.add_parser('get',
245 help="""Find the value of SYMBOL
246 and print it. Exit with
247 status 0 if a #define for SYMBOL is
248 found, 1 otherwise.
249 """)
250 parser_get.add_argument('symbol', metavar='SYMBOL')
251 parser_set = subparsers.add_parser('set',
252 help="""Set SYMBOL to VALUE.
253 If VALUE is omitted, just uncomment
254 the #define for SYMBOL.
255 Error out of a line defining
256 SYMBOL (commented or not) is not
257 found, unless --force is passed.
258 """)
259 parser_set.add_argument('symbol', metavar='SYMBOL')
260 parser_set.add_argument('value', metavar='VALUE', nargs='?')
261 parser_unset = subparsers.add_parser('unset',
262 help="""Comment out the #define
263 for SYMBOL. Do nothing if none
264 is present.""")
265 parser_unset.add_argument('symbol', metavar='SYMBOL')
266
267 def add_adapter(name, function, description):
268 subparser = subparsers.add_parser(name, help=description)
269 subparser.set_defaults(adapter=function)
270 add_adapter('realfull', realfull_adapter,
271 """Uncomment all #defines. No exceptions.""")
272
273 args = parser.parse_args()
274 config = ConfigFile(args.file)
275 if args.command == 'get':
276 if args.symbol in config:
277 value = config[args.symbol]
278 if value:
279 sys.stdout.write(value + '\n')
280 return args.symbol not in config
281 elif args.command == 'set':
282 if not args.force and args.symbol not in config:
283 sys.stderr.write("A #define for the symbol {} "
284 "was not found in {}"
285 .format(args.symbol, args.file))
286 return 1
287 config.set(args.symbol, value=args.value)
288 elif args.command == 'unset':
289 config.unset(args.symbol)
290 else:
291 config.adapt(args.adapter)
292 config.write()
293
294 # Import modules only used by main only if main is defined and called.
295 # pylint: disable=wrong-import-position
296 import argparse
297 import sys
298 sys.exit(main())