quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

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