config_common.py (19249B)
1 """Mbed TLS and PSA configuration file manipulation library 2 """ 3 4 ## Copyright The Mbed TLS Contributors 5 ## SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later 6 ## 7 8 import argparse 9 import os 10 import re 11 import shutil 12 import sys 13 14 from abc import ABCMeta 15 16 17 class Setting: 18 """Representation of one Mbed TLS mbedtls_config.h or PSA crypto_config.h setting. 19 20 Fields: 21 * name: the symbol name ('MBEDTLS_xxx'). 22 * value: the value of the macro. The empty string for a plain #define 23 with no value. 24 * active: True if name is defined, False if a #define for name is 25 present in mbedtls_config.h but commented out. 26 * section: the name of the section that contains this symbol. 27 * configfile: the representation of the configuration file where the setting is defined 28 """ 29 # pylint: disable=too-few-public-methods, too-many-arguments 30 def __init__(self, configfile, active, name, value='', section=None): 31 self.active = active 32 self.name = name 33 self.value = value 34 self.section = section 35 self.configfile = configfile 36 37 38 class Config: 39 """Representation of the Mbed TLS and PSA configuration. 40 41 In the documentation of this class, a symbol is said to be *active* 42 if there is a #define for it that is not commented out, and *known* 43 if there is a #define for it whether commented out or not. 44 45 This class supports the following protocols: 46 * `name in config` is `True` if the symbol `name` is active, `False` 47 otherwise (whether `name` is inactive or not known). 48 * `config[name]` is the value of the macro `name`. If `name` is inactive, 49 raise `KeyError` (even if `name` is known). 50 * `config[name] = value` sets the value associated to `name`. `name` 51 must be known, but does not need to be set. This does not cause 52 name to become set. 53 """ 54 55 def __init__(self): 56 self.settings = {} 57 self.configfiles = [] 58 59 def __contains__(self, name): 60 """True if the given symbol is active (i.e. set). 61 62 False if the given symbol is not set, even if a definition 63 is present but commented out. 64 """ 65 return name in self.settings and self.settings[name].active 66 67 def all(self, *names): 68 """True if all the elements of names are active (i.e. set).""" 69 return all(name in self for name in names) 70 71 def any(self, *names): 72 """True if at least one symbol in names are active (i.e. set).""" 73 return any(name in self for name in names) 74 75 def known(self, name): 76 """True if a #define for name is present, whether it's commented out or not.""" 77 return name in self.settings 78 79 def __getitem__(self, name): 80 """Get the value of name, i.e. what the preprocessor symbol expands to. 81 82 If name is not known, raise KeyError. name does not need to be active. 83 """ 84 return self.settings[name].value 85 86 def get(self, name, default=None): 87 """Get the value of name. If name is inactive (not set), return default. 88 89 If a #define for name is present and not commented out, return 90 its expansion, even if this is the empty string. 91 92 If a #define for name is present but commented out, return default. 93 """ 94 if name in self: 95 return self.settings[name].value 96 else: 97 return default 98 99 def get_matching(self, regexs, only_enabled): 100 """Get all symbols matching one of the regexs.""" 101 if not regexs: 102 return 103 regex = re.compile('|'.join(regexs)) 104 for setting in self.settings.values(): 105 if regex.search(setting.name): 106 if setting.active or not only_enabled: 107 yield setting.name 108 109 def __setitem__(self, name, value): 110 """If name is known, set its value. 111 112 If name is not known, raise KeyError. 113 """ 114 setting = self.settings[name] 115 if setting.value != value: 116 setting.configfile.modified = True 117 118 setting.value = value 119 120 def set(self, name, value=None): 121 """Set name to the given value and make it active. 122 123 If value is None and name is already known, don't change its value. 124 If value is None and name is not known, set its value. 125 """ 126 if name in self.settings: 127 setting = self.settings[name] 128 if (value is not None and setting.value != value) or not setting.active: 129 setting.configfile.modified = True 130 if value is not None: 131 setting.value = value 132 setting.active = True 133 else: 134 configfile = self._get_configfile(name) 135 self.settings[name] = Setting(configfile, True, name, value=value) 136 configfile.modified = True 137 138 def unset(self, name): 139 """Make name unset (inactive). 140 141 name remains known if it was known before. 142 """ 143 if name not in self.settings: 144 return 145 146 setting = self.settings[name] 147 # Check if modifying the config file 148 if setting.active: 149 setting.configfile.modified = True 150 151 setting.active = False 152 153 def adapt(self, adapter): 154 """Run adapter on each known symbol and (de)activate it accordingly. 155 156 `adapter` must be a function that returns a boolean. It is called as 157 `adapter(name, value, active)` for each setting, where 158 `value` is the macro's expansion (possibly empty), and `active` is 159 `True` if `name` is set and `False` if `name` is known but unset. 160 If `adapter` returns `True`, then set `name` (i.e. make it active), 161 otherwise unset `name` (i.e. make it known but inactive). 162 """ 163 for setting in self.settings.values(): 164 is_active = setting.active 165 setting.active = adapter(setting.name, setting.value, 166 setting.active) 167 # Check if modifying the config file 168 if setting.active != is_active: 169 setting.configfile.modified = True 170 171 def change_matching(self, regexs, enable): 172 """Change all symbols matching one of the regexs to the desired state.""" 173 if not regexs: 174 return 175 regex = re.compile('|'.join(regexs)) 176 for setting in self.settings.values(): 177 if regex.search(setting.name): 178 # Check if modifying the config file 179 if setting.active != enable: 180 setting.configfile.modified = True 181 setting.active = enable 182 183 def _get_configfile(self, name=None): 184 """Get the representation of the configuration file name belongs to 185 186 If the configuration is spread among several configuration files, this 187 function may need to be overridden for the case of an unknown setting. 188 """ 189 190 if name and name in self.settings: 191 return self.settings[name].configfile 192 return self.configfiles[0] 193 194 def write(self, filename=None): 195 """Write the whole configuration to the file(s) it was read from. 196 197 If filename is specified, write to this file(s) instead. 198 """ 199 200 for configfile in self.configfiles: 201 configfile.write(self.settings, filename) 202 203 def filename(self, name=None): 204 """Get the name of the config file where the setting name is defined.""" 205 206 return self._get_configfile(name).filename 207 208 def backup(self, suffix='.bak'): 209 """Back up the configuration file.""" 210 211 for configfile in self.configfiles: 212 configfile.backup(suffix) 213 214 def restore(self): 215 """Restore the configuration file.""" 216 217 for configfile in self.configfiles: 218 configfile.restore() 219 220 221 class ConfigFile(metaclass=ABCMeta): 222 """Representation of a configuration file.""" 223 224 def __init__(self, default_path, name, filename=None): 225 """Check if the config file exists.""" 226 if filename is None: 227 for candidate in default_path: 228 if os.path.lexists(candidate): 229 filename = candidate 230 break 231 232 if not os.path.lexists(filename): 233 raise FileNotFoundError(f'{name} configuration file not found: ' 234 f'{filename if filename else default_path}') 235 236 self.filename = filename 237 self.templates = [] 238 self.current_section = None 239 self.inclusion_guard = None 240 self.modified = False 241 self._backupname = None 242 self._own_backup = False 243 244 _define_line_regexp = (r'(?P<indentation>\s*)' + 245 r'(?P<commented_out>(//\s*)?)' + 246 r'(?P<define>#\s*define\s+)' + 247 r'(?P<name>\w+)' + 248 r'(?P<arguments>(?:\((?:\w|\s|,)*\))?)' + 249 r'(?P<separator>\s*)' + 250 r'(?P<value>.*)') 251 _ifndef_line_regexp = r'#ifndef (?P<inclusion_guard>\w+)' 252 _section_line_regexp = (r'\s*/?\*+\s*[\\@]name\s+SECTION:\s*' + 253 r'(?P<section>.*)[ */]*') 254 _config_line_regexp = re.compile(r'|'.join([_define_line_regexp, 255 _ifndef_line_regexp, 256 _section_line_regexp])) 257 def _parse_line(self, line): 258 """Parse a line in the config file, save the templates representing the lines 259 and return the corresponding setting element. 260 """ 261 262 line = line.rstrip('\r\n') 263 m = re.match(self._config_line_regexp, line) 264 if m is None: 265 self.templates.append(line) 266 return None 267 elif m.group('section'): 268 self.current_section = m.group('section') 269 self.templates.append(line) 270 return None 271 elif m.group('inclusion_guard') and self.inclusion_guard is None: 272 self.inclusion_guard = m.group('inclusion_guard') 273 self.templates.append(line) 274 return None 275 else: 276 active = not m.group('commented_out') 277 name = m.group('name') 278 value = m.group('value') 279 if name == self.inclusion_guard and value == '': 280 # The file double-inclusion guard is not an option. 281 self.templates.append(line) 282 return None 283 template = (name, 284 m.group('indentation'), 285 m.group('define') + name + 286 m.group('arguments') + m.group('separator')) 287 self.templates.append(template) 288 289 return (active, name, value, self.current_section) 290 291 def parse_file(self): 292 """Parse the whole file and return the settings.""" 293 294 with open(self.filename, 'r', encoding='utf-8') as file: 295 for line in file: 296 setting = self._parse_line(line) 297 if setting is not None: 298 yield setting 299 self.current_section = None 300 301 #pylint: disable=no-self-use 302 def _format_template(self, setting, indent, middle): 303 """Build a line for the config file for the given setting. 304 305 The line has the form "<indent>#define <name> <value>" 306 where <middle> is "#define <name> ". 307 """ 308 309 value = setting.value 310 if value is None: 311 value = '' 312 # Normally the whitespace to separate the symbol name from the 313 # value is part of middle, and there's no whitespace for a symbol 314 # with no value. But if a symbol has been changed from having a 315 # value to not having one, the whitespace is wrong, so fix it. 316 if value: 317 if middle[-1] not in '\t ': 318 middle += ' ' 319 else: 320 middle = middle.rstrip() 321 return ''.join([indent, 322 '' if setting.active else '//', 323 middle, 324 value]).rstrip() 325 326 def write_to_stream(self, settings, output): 327 """Write the whole configuration to output.""" 328 329 for template in self.templates: 330 if isinstance(template, str): 331 line = template 332 else: 333 name, indent, middle = template 334 line = self._format_template(settings[name], indent, middle) 335 output.write(line + '\n') 336 337 def write(self, settings, filename=None): 338 """Write the whole configuration to the file it was read from. 339 340 If filename is specified, write to this file instead. 341 """ 342 343 if filename is None: 344 filename = self.filename 345 346 # Not modified so no need to write to the file 347 if not self.modified and filename == self.filename: 348 return 349 350 with open(filename, 'w', encoding='utf-8') as output: 351 self.write_to_stream(settings, output) 352 353 def backup(self, suffix='.bak'): 354 """Back up the configuration file. 355 356 If the backup file already exists, it is presumed to be the desired backup, 357 so don't make another backup. 358 """ 359 if self._backupname: 360 return 361 362 self._backupname = self.filename + suffix 363 if os.path.exists(self._backupname): 364 self._own_backup = False 365 else: 366 self._own_backup = True 367 shutil.copy(self.filename, self._backupname) 368 369 def restore(self): 370 """Restore the configuration file. 371 372 Only delete the backup file if it was created earlier. 373 """ 374 if not self._backupname: 375 return 376 377 if self._own_backup: 378 shutil.move(self._backupname, self.filename) 379 else: 380 shutil.copy(self._backupname, self.filename) 381 382 self._backupname = None 383 384 385 class ConfigTool(metaclass=ABCMeta): 386 """Command line config manipulation tool. 387 388 Custom parser options can be added by overriding 'custom_parser_options'. 389 """ 390 391 def __init__(self, default_file_path): 392 """Create parser for config manipulation tool. 393 394 :param default_file_path: Default configuration file path 395 """ 396 397 self.parser = argparse.ArgumentParser(description=""" 398 Configuration file manipulation tool.""") 399 self.subparsers = self.parser.add_subparsers(dest='command', 400 title='Commands') 401 self._common_parser_options(default_file_path) 402 self.custom_parser_options() 403 self.args = self.parser.parse_args() 404 self.config = Config() # Make the pylint happy 405 406 def add_adapter(self, name, function, description): 407 """Creates a command in the tool for a configuration adapter.""" 408 409 subparser = self.subparsers.add_parser(name, help=description) 410 subparser.set_defaults(adapter=function) 411 412 def _common_parser_options(self, default_file_path): 413 # pylint: disable=too-many-branches 414 """Common parser options for config manipulation tool.""" 415 416 self.parser.add_argument( 417 '--file', '-f', 418 help="""File to read (and modify if requested). Default: {}. 419 """.format(default_file_path)) 420 self.parser.add_argument( 421 '--force', '-o', 422 action='store_true', 423 help="""For the set command, if SYMBOL is not present, add a definition for it.""") 424 self.parser.add_argument( 425 '--write', '-w', 426 metavar='FILE', 427 help="""File to write to instead of the input file.""") 428 429 parser_get = self.subparsers.add_parser( 430 'get', 431 help="""Find the value of SYMBOL and print it. Exit with 432 status 0 if a #define for SYMBOL is found, 1 otherwise.""") 433 parser_get.add_argument('symbol', metavar='SYMBOL') 434 parser_set = self.subparsers.add_parser( 435 'set', 436 help="""Set SYMBOL to VALUE. If VALUE is omitted, just uncomment 437 the #define for SYMBOL. Error out of a line defining 438 SYMBOL (commented or not) is not found, unless --force is passed. """) 439 parser_set.add_argument('symbol', metavar='SYMBOL') 440 parser_set.add_argument('value', metavar='VALUE', nargs='?', default='') 441 parser_set_all = self.subparsers.add_parser( 442 'set-all', 443 help="""Uncomment all #define whose name contains a match for REGEX.""") 444 parser_set_all.add_argument('regexs', metavar='REGEX', nargs='*') 445 parser_unset = self.subparsers.add_parser( 446 'unset', 447 help="""Comment out the #define for SYMBOL. Do nothing if none is present.""") 448 parser_unset.add_argument('symbol', metavar='SYMBOL') 449 parser_unset_all = self.subparsers.add_parser( 450 'unset-all', 451 help="""Comment out all #define whose name contains a match for REGEX.""") 452 parser_unset_all.add_argument('regexs', metavar='REGEX', nargs='*') 453 parser_get_all = self.subparsers.add_parser( 454 'get-all', 455 help="""Get all #define whose name contains a match for REGEX.""") 456 parser_get_all.add_argument('regexs', metavar='REGEX', nargs='*') 457 parser_get_all_enabled = self.subparsers.add_parser( 458 'get-all-enabled', 459 help="""Get all enabled #define whose name contains a match for REGEX.""") 460 parser_get_all_enabled.add_argument('regexs', metavar='REGEX', nargs='*') 461 462 463 def custom_parser_options(self): 464 """Adds custom options for the parser. Designed for overridden by descendant.""" 465 pass 466 467 def main(self): 468 # pylint: disable=too-many-branches 469 """Common main fuction for config manipulation tool.""" 470 471 args = self.args 472 config = self.config 473 474 if args.command is None: 475 self.parser.print_help() 476 return 1 477 if args.command == 'get': 478 if args.symbol in config: 479 value = config[args.symbol] 480 if value: 481 sys.stdout.write(value + '\n') 482 return 0 if args.symbol in config else 1 483 elif args.command == 'get-all': 484 match_list = config.get_matching(args.regexs, False) 485 sys.stdout.write("\n".join(match_list)) 486 elif args.command == 'get-all-enabled': 487 match_list = config.get_matching(args.regexs, True) 488 sys.stdout.write("\n".join(match_list)) 489 elif args.command == 'set': 490 if not args.force and args.symbol not in config.settings: 491 sys.stderr.write( 492 "A #define for the symbol {} was not found in {}\n" 493 .format(args.symbol, 494 config.filename(args.symbol))) 495 return 1 496 config.set(args.symbol, value=args.value) 497 elif args.command == 'set-all': 498 config.change_matching(args.regexs, True) 499 elif args.command == 'unset': 500 config.unset(args.symbol) 501 elif args.command == 'unset-all': 502 config.change_matching(args.regexs, False) 503 else: 504 config.adapt(args.adapter) 505 config.write(args.write) 506 507 return 0