Home | History | Annotate | Download | only in fs_config
      1 #!/usr/bin/env python
      2 """Generates config files for Android file system properties.
      3 
      4 This script is used for generating configuration files for configuring
      5 Android filesystem properties. Internally, its composed of a plug-able
      6 interface to support the understanding of new input and output parameters.
      7 
      8 Run the help for a list of supported plugins and their capabilities.
      9 
     10 Further documentation can be found in the README.
     11 """
     12 
     13 import argparse
     14 import ConfigParser
     15 import ctypes
     16 import re
     17 import sys
     18 import textwrap
     19 
     20 # Keep the tool in one file to make it easy to run.
     21 # pylint: disable=too-many-lines
     22 
     23 
     24 # Lowercase generator used to be inline with @staticmethod.
     25 class generator(object):  # pylint: disable=invalid-name
     26     """A decorator class to add commandlet plugins.
     27 
     28     Used as a decorator to classes to add them to
     29     the internal plugin interface. Plugins added
     30     with @generator() are automatically added to
     31     the command line.
     32 
     33     For instance, to add a new generator
     34     called foo and have it added just do this:
     35 
     36         @generator("foo")
     37         class FooGen(object):
     38             ...
     39     """
     40     _generators = {}
     41 
     42     def __init__(self, gen):
     43         """
     44         Args:
     45             gen (str): The name of the generator to add.
     46 
     47         Raises:
     48             ValueError: If there is a similarly named generator already added.
     49 
     50         """
     51         self._gen = gen
     52 
     53         if gen in generator._generators:
     54             raise ValueError('Duplicate generator name: ' + gen)
     55 
     56         generator._generators[gen] = None
     57 
     58     def __call__(self, cls):
     59 
     60         generator._generators[self._gen] = cls()
     61         return cls
     62 
     63     @staticmethod
     64     def get():
     65         """Gets the list of generators.
     66 
     67         Returns:
     68            The list of registered generators.
     69         """
     70         return generator._generators
     71 
     72 
     73 class Utils(object):
     74     """Various assorted static utilities."""
     75 
     76     @staticmethod
     77     def in_any_range(value, ranges):
     78         """Tests if a value is in a list of given closed range tuples.
     79 
     80         A range tuple is a closed range. That means it's inclusive of its
     81         start and ending values.
     82 
     83         Args:
     84             value (int): The value to test.
     85             range [(int, int)]: The closed range list to test value within.
     86 
     87         Returns:
     88             True if value is within the closed range, false otherwise.
     89         """
     90 
     91         return any(lower <= value <= upper for (lower, upper) in ranges)
     92 
     93     @staticmethod
     94     def get_login_and_uid_cleansed(aid):
     95         """Returns a passwd/group file safe logon and uid.
     96 
     97         This checks that the logon and uid of the AID do not
     98         contain the delimiter ":" for a passwd/group file.
     99 
    100         Args:
    101             aid (AID): The aid to check
    102 
    103         Returns:
    104             logon, uid of the AID after checking its safe.
    105 
    106         Raises:
    107             ValueError: If there is a delimiter charcter found.
    108         """
    109         logon = aid.friendly
    110         uid = aid.normalized_value
    111         if ':' in uid:
    112             raise ValueError(
    113                 'Cannot specify delimiter character ":" in uid: "%s"' % uid)
    114         if ':' in logon:
    115             raise ValueError(
    116                 'Cannot specify delimiter character ":" in logon: "%s"' %
    117                 logon)
    118         return logon, uid
    119 
    120 
    121 class AID(object):
    122     """This class represents an Android ID or an AID.
    123 
    124     Attributes:
    125         identifier (str): The identifier name for a #define.
    126         value (str) The User Id (uid) of the associate define.
    127         found (str) The file it was found in, can be None.
    128         normalized_value (str): Same as value, but base 10.
    129         friendly (str): The friendly name of aid.
    130     """
    131 
    132     PREFIX = 'AID_'
    133 
    134     # Some of the AIDS like AID_MEDIA_EX had names like mediaex
    135     # list a map of things to fixup until we can correct these
    136     # at a later date.
    137     _FIXUPS = {
    138         'media_drm': 'mediadrm',
    139         'media_ex': 'mediaex',
    140         'media_codec': 'mediacodec'
    141     }
    142 
    143     def __init__(self, identifier, value, found, login_shell):
    144         """
    145         Args:
    146             identifier: The identifier name for a #define <identifier>.
    147             value: The value of the AID, aka the uid.
    148             found (str): The file found in, not required to be specified.
    149             login_shell (str): The shell field per man (5) passwd file.
    150         Raises:
    151             ValueError: if the friendly name is longer than 31 characters as
    152                 that is bionic's internal buffer size for name.
    153             ValueError: if value is not a valid string number as processed by
    154                 int(x, 0)
    155         """
    156         self.identifier = identifier
    157         self.value = value
    158         self.found = found
    159         self.login_shell = login_shell
    160 
    161         try:
    162             self.normalized_value = str(int(value, 0))
    163         except ValueError:
    164             raise ValueError(
    165                 'Invalid "value", not aid number, got: \"%s\"' % value)
    166 
    167         # Where we calculate the friendly name
    168         friendly = identifier[len(AID.PREFIX):].lower()
    169         self.friendly = AID._fixup_friendly(friendly)
    170 
    171         if len(self.friendly) > 31:
    172             raise ValueError(
    173                 'AID names must be under 32 characters "%s"' % self.friendly)
    174 
    175     def __eq__(self, other):
    176 
    177         return self.identifier == other.identifier \
    178             and self.value == other.value and self.found == other.found \
    179             and self.normalized_value == other.normalized_value \
    180             and self.login_shell == other.login_shell
    181 
    182     @staticmethod
    183     def is_friendly(name):
    184         """Determines if an AID is a freindly name or C define.
    185 
    186         For example if name is AID_SYSTEM it returns false, if name
    187         was system, it would return true.
    188 
    189         Returns:
    190             True if name is a friendly name False otherwise.
    191         """
    192 
    193         return not name.startswith(AID.PREFIX)
    194 
    195     @staticmethod
    196     def _fixup_friendly(friendly):
    197         """Fixup friendly names that historically don't follow the convention.
    198 
    199         Args:
    200             friendly (str): The friendly name.
    201 
    202         Returns:
    203             The fixedup friendly name as a str.
    204         """
    205 
    206         if friendly in AID._FIXUPS:
    207             return AID._FIXUPS[friendly]
    208 
    209         return friendly
    210 
    211 
    212 class FSConfig(object):
    213     """Represents a filesystem config array entry.
    214 
    215     Represents a file system configuration entry for specifying
    216     file system capabilities.
    217 
    218     Attributes:
    219         mode (str): The mode of the file or directory.
    220         user (str): The uid or #define identifier (AID_SYSTEM)
    221         group (str): The gid or #define identifier (AID_SYSTEM)
    222         caps (str): The capability set.
    223         path (str): The path of the file or directory.
    224         filename (str): The file it was found in.
    225     """
    226 
    227     def __init__(self, mode, user, group, caps, path, filename):
    228         """
    229         Args:
    230             mode (str): The mode of the file or directory.
    231             user (str): The uid or #define identifier (AID_SYSTEM)
    232             group (str): The gid or #define identifier (AID_SYSTEM)
    233             caps (str): The capability set as a list.
    234             path (str): The path of the file or directory.
    235             filename (str): The file it was found in.
    236         """
    237         self.mode = mode
    238         self.user = user
    239         self.group = group
    240         self.caps = caps
    241         self.path = path
    242         self.filename = filename
    243 
    244     def __eq__(self, other):
    245 
    246         return self.mode == other.mode and self.user == other.user \
    247             and self.group == other.group and self.caps == other.caps \
    248             and self.path == other.path and self.filename == other.filename
    249 
    250     def __repr__(self):
    251         return 'FSConfig(%r, %r, %r, %r, %r, %r)' % (self.mode, self.user,
    252                                                      self.group, self.caps,
    253                                                      self.path, self.filename)
    254 
    255 
    256 class CapabilityHeaderParser(object):
    257     """Parses capability.h file
    258 
    259     Parses a C header file and extracts lines starting with #define CAP_<name>.
    260     """
    261 
    262     _CAP_DEFINE = re.compile(r'\s*#define\s+(CAP_\S+)\s+(\S+)')
    263     _SKIP_CAPS = ['CAP_LAST_CAP', 'CAP_TO_INDEX(x)', 'CAP_TO_MASK(x)']
    264 
    265     def __init__(self, capability_header):
    266         """
    267         Args:
    268             capability_header (str): file name for the header file containing AID entries.
    269         """
    270 
    271         self.caps = {}
    272         with open(capability_header) as open_file:
    273             self._parse(open_file)
    274 
    275     def _parse(self, capability_file):
    276         """Parses a capability header file. Internal use only.
    277 
    278         Args:
    279             capability_file (file): The open capability header file to parse.
    280         """
    281 
    282         for line in capability_file:
    283             match = CapabilityHeaderParser._CAP_DEFINE.match(line)
    284             if match:
    285                 cap = match.group(1)
    286                 value = match.group(2)
    287 
    288                 if not cap in self._SKIP_CAPS:
    289                     try:
    290                         self.caps[cap] = int(value, 0)
    291                     except ValueError:
    292                         sys.exit('Could not parse capability define "%s":"%s"'
    293                                  % (cap, value))
    294 
    295 
    296 class AIDHeaderParser(object):
    297     """Parses an android_filesystem_config.h file.
    298 
    299     Parses a C header file and extracts lines starting with #define AID_<name>
    300     while capturing the OEM defined ranges and ignoring other ranges. It also
    301     skips some hardcoded AIDs it doesn't need to generate a mapping for.
    302     It provides some basic sanity checks. The information extracted from this
    303     file can later be used to sanity check other things (like oem ranges) as
    304     well as generating a mapping of names to uids. It was primarily designed to
    305     parse the private/android_filesystem_config.h, but any C header should
    306     work.
    307     """
    308 
    309     _SKIP_AIDS = [
    310         re.compile(r'%sUNUSED[0-9].*' % AID.PREFIX),
    311         re.compile(r'%sAPP' % AID.PREFIX),
    312         re.compile(r'%sUSER' % AID.PREFIX)
    313     ]
    314     _AID_DEFINE = re.compile(r'\s*#define\s+%s.*' % AID.PREFIX)
    315     _OEM_START_KW = 'START'
    316     _OEM_END_KW = 'END'
    317     _OEM_RANGE = re.compile('%sOEM_RESERVED_[0-9]*_{0,1}(%s|%s)' %
    318                             (AID.PREFIX, _OEM_START_KW, _OEM_END_KW))
    319     # AID lines cannot end with _START or _END, ie AID_FOO is OK
    320     # but AID_FOO_START is skiped. Note that AID_FOOSTART is NOT skipped.
    321     _AID_SKIP_RANGE = ['_' + _OEM_START_KW, '_' + _OEM_END_KW]
    322     _COLLISION_OK = ['AID_APP', 'AID_APP_START', 'AID_USER', 'AID_USER_OFFSET']
    323 
    324     def __init__(self, aid_header):
    325         """
    326         Args:
    327             aid_header (str): file name for the header
    328                 file containing AID entries.
    329         """
    330         self._aid_header = aid_header
    331         self._aid_name_to_value = {}
    332         self._aid_value_to_name = {}
    333         self._oem_ranges = {}
    334 
    335         with open(aid_header) as open_file:
    336             self._parse(open_file)
    337 
    338         try:
    339             self._process_and_check()
    340         except ValueError as exception:
    341             sys.exit('Error processing parsed data: "%s"' % (str(exception)))
    342 
    343     def _parse(self, aid_file):
    344         """Parses an AID header file. Internal use only.
    345 
    346         Args:
    347             aid_file (file): The open AID header file to parse.
    348         """
    349 
    350         for lineno, line in enumerate(aid_file):
    351 
    352             def error_message(msg):
    353                 """Creates an error message with the current parsing state."""
    354                 # pylint: disable=cell-var-from-loop
    355                 return 'Error "{}" in file: "{}" on line: {}'.format(
    356                     msg, self._aid_header, str(lineno))
    357 
    358             if AIDHeaderParser._AID_DEFINE.match(line):
    359                 chunks = line.split()
    360                 identifier = chunks[1]
    361                 value = chunks[2]
    362 
    363                 if any(
    364                         x.match(identifier)
    365                         for x in AIDHeaderParser._SKIP_AIDS):
    366                     continue
    367 
    368                 try:
    369                     if AIDHeaderParser._is_oem_range(identifier):
    370                         self._handle_oem_range(identifier, value)
    371                     elif not any(
    372                             identifier.endswith(x)
    373                             for x in AIDHeaderParser._AID_SKIP_RANGE):
    374                         self._handle_aid(identifier, value)
    375                 except ValueError as exception:
    376                     sys.exit(
    377                         error_message('{} for "{}"'.format(
    378                             exception, identifier)))
    379 
    380     def _handle_aid(self, identifier, value):
    381         """Handle an AID C #define.
    382 
    383         Handles an AID, sanity checking, generating the friendly name and
    384         adding it to the internal maps. Internal use only.
    385 
    386         Args:
    387             identifier (str): The name of the #define identifier. ie AID_FOO.
    388             value (str): The value associated with the identifier.
    389 
    390         Raises:
    391             ValueError: With message set to indicate the error.
    392         """
    393 
    394         aid = AID(identifier, value, self._aid_header, '/system/bin/sh')
    395 
    396         # duplicate name
    397         if aid.friendly in self._aid_name_to_value:
    398             raise ValueError('Duplicate aid "%s"' % identifier)
    399 
    400         if value in self._aid_value_to_name and aid.identifier not in AIDHeaderParser._COLLISION_OK:
    401             raise ValueError(
    402                 'Duplicate aid value "%s" for %s' % (value, identifier))
    403 
    404         self._aid_name_to_value[aid.friendly] = aid
    405         self._aid_value_to_name[value] = aid.friendly
    406 
    407     def _handle_oem_range(self, identifier, value):
    408         """Handle an OEM range C #define.
    409 
    410         When encountering special AID defines, notably for the OEM ranges
    411         this method handles sanity checking and adding them to the internal
    412         maps. For internal use only.
    413 
    414         Args:
    415             identifier (str): The name of the #define identifier.
    416                 ie AID_OEM_RESERVED_START/END.
    417             value (str): The value associated with the identifier.
    418 
    419         Raises:
    420             ValueError: With message set to indicate the error.
    421         """
    422 
    423         try:
    424             int_value = int(value, 0)
    425         except ValueError:
    426             raise ValueError(
    427                 'Could not convert "%s" to integer value, got: "%s"' %
    428                 (identifier, value))
    429 
    430         # convert AID_OEM_RESERVED_START or AID_OEM_RESERVED_<num>_START
    431         # to AID_OEM_RESERVED or AID_OEM_RESERVED_<num>
    432         is_start = identifier.endswith(AIDHeaderParser._OEM_START_KW)
    433 
    434         if is_start:
    435             tostrip = len(AIDHeaderParser._OEM_START_KW)
    436         else:
    437             tostrip = len(AIDHeaderParser._OEM_END_KW)
    438 
    439         # ending _
    440         tostrip = tostrip + 1
    441 
    442         strip = identifier[:-tostrip]
    443         if strip not in self._oem_ranges:
    444             self._oem_ranges[strip] = []
    445 
    446         if len(self._oem_ranges[strip]) > 2:
    447             raise ValueError('Too many same OEM Ranges "%s"' % identifier)
    448 
    449         if len(self._oem_ranges[strip]) == 1:
    450             tmp = self._oem_ranges[strip][0]
    451 
    452             if tmp == int_value:
    453                 raise ValueError('START and END values equal %u' % int_value)
    454             elif is_start and tmp < int_value:
    455                 raise ValueError(
    456                     'END value %u less than START value %u' % (tmp, int_value))
    457             elif not is_start and tmp > int_value:
    458                 raise ValueError(
    459                     'END value %u less than START value %u' % (int_value, tmp))
    460 
    461         # Add START values to the head of the list and END values at the end.
    462         # Thus, the list is ordered with index 0 as START and index 1 as END.
    463         if is_start:
    464             self._oem_ranges[strip].insert(0, int_value)
    465         else:
    466             self._oem_ranges[strip].append(int_value)
    467 
    468     def _process_and_check(self):
    469         """Process, check and populate internal data structures.
    470 
    471         After parsing and generating the internal data structures, this method
    472         is responsible for sanity checking ALL of the acquired data.
    473 
    474         Raises:
    475             ValueError: With the message set to indicate the specific error.
    476         """
    477 
    478         # tuplefy the lists since range() does not like them mutable.
    479         self._oem_ranges = [
    480             AIDHeaderParser._convert_lst_to_tup(k, v)
    481             for k, v in self._oem_ranges.iteritems()
    482         ]
    483 
    484         # Check for overlapping ranges
    485         for i, range1 in enumerate(self._oem_ranges):
    486             for range2 in self._oem_ranges[i + 1:]:
    487                 if AIDHeaderParser._is_overlap(range1, range2):
    488                     raise ValueError("Overlapping OEM Ranges found %s and %s" %
    489                                      (str(range1), str(range2)))
    490 
    491         # No core AIDs should be within any oem range.
    492         for aid in self._aid_value_to_name:
    493 
    494             if Utils.in_any_range(aid, self._oem_ranges):
    495                 name = self._aid_value_to_name[aid]
    496                 raise ValueError(
    497                     'AID "%s" value: %u within reserved OEM Range: "%s"' %
    498                     (name, aid, str(self._oem_ranges)))
    499 
    500     @property
    501     def oem_ranges(self):
    502         """Retrieves the OEM closed ranges as a list of tuples.
    503 
    504         Returns:
    505             A list of closed range tuples: [ (0, 42), (50, 105) ... ]
    506         """
    507         return self._oem_ranges
    508 
    509     @property
    510     def aids(self):
    511         """Retrieves the list of found AIDs.
    512 
    513         Returns:
    514             A list of AID() objects.
    515         """
    516         return self._aid_name_to_value.values()
    517 
    518     @staticmethod
    519     def _convert_lst_to_tup(name, lst):
    520         """Converts a mutable list to a non-mutable tuple.
    521 
    522         Used ONLY for ranges and thus enforces a length of 2.
    523 
    524         Args:
    525             lst (List): list that should be "tuplefied".
    526 
    527         Raises:
    528             ValueError if lst is not a list or len is not 2.
    529 
    530         Returns:
    531             Tuple(lst)
    532         """
    533         if not lst or len(lst) != 2:
    534             raise ValueError('Mismatched range for "%s"' % name)
    535 
    536         return tuple(lst)
    537 
    538     @staticmethod
    539     def _is_oem_range(aid):
    540         """Detects if a given aid is within the reserved OEM range.
    541 
    542         Args:
    543             aid (int): The aid to test
    544 
    545         Returns:
    546             True if it is within the range, False otherwise.
    547         """
    548 
    549         return AIDHeaderParser._OEM_RANGE.match(aid)
    550 
    551     @staticmethod
    552     def _is_overlap(range_a, range_b):
    553         """Calculates the overlap of two range tuples.
    554 
    555         A range tuple is a closed range. A closed range includes its endpoints.
    556         Note that python tuples use () notation which collides with the
    557         mathematical notation for open ranges.
    558 
    559         Args:
    560             range_a: The first tuple closed range eg (0, 5).
    561             range_b: The second tuple closed range eg (3, 7).
    562 
    563         Returns:
    564             True if they overlap, False otherwise.
    565         """
    566 
    567         return max(range_a[0], range_b[0]) <= min(range_a[1], range_b[1])
    568 
    569 
    570 class FSConfigFileParser(object):
    571     """Parses a config.fs ini format file.
    572 
    573     This class is responsible for parsing the config.fs ini format files.
    574     It collects and checks all the data in these files and makes it available
    575     for consumption post processed.
    576     """
    577 
    578     # These _AID vars work together to ensure that an AID section name
    579     # cannot contain invalid characters for a C define or a passwd/group file.
    580     # Since _AID_PREFIX is within the set of _AID_MATCH the error logic only
    581     # checks end, if you change this, you may have to update the error
    582     # detection code.
    583     _AID_MATCH = re.compile('%s[A-Z0-9_]+' % AID.PREFIX)
    584     _AID_ERR_MSG = 'Expecting upper case, a number or underscore'
    585 
    586     # list of handler to required options, used to identify the
    587     # parsing section
    588     _SECTIONS = [('_handle_aid', ('value', )),
    589                  ('_handle_path', ('mode', 'user', 'group', 'caps'))]
    590 
    591     def __init__(self, config_files, oem_ranges):
    592         """
    593         Args:
    594             config_files ([str]): The list of config.fs files to parse.
    595                 Note the filename is not important.
    596             oem_ranges ([(),()]): range tuples indicating reserved OEM ranges.
    597         """
    598 
    599         self._files = []
    600         self._dirs = []
    601         self._aids = []
    602 
    603         self._seen_paths = {}
    604         # (name to file, value to aid)
    605         self._seen_aids = ({}, {})
    606 
    607         self._oem_ranges = oem_ranges
    608 
    609         self._config_files = config_files
    610 
    611         for config_file in self._config_files:
    612             self._parse(config_file)
    613 
    614     def _parse(self, file_name):
    615         """Parses and verifies config.fs files. Internal use only.
    616 
    617         Args:
    618             file_name (str): The config.fs (PythonConfigParser file format)
    619                 file to parse.
    620 
    621         Raises:
    622             Anything raised by ConfigParser.read()
    623         """
    624 
    625         # Separate config parsers for each file found. If you use
    626         # read(filenames...) later files can override earlier files which is
    627         # not what we want. Track state across files and enforce with
    628         # _handle_dup(). Note, strict ConfigParser is set to true in
    629         # Python >= 3.2, so in previous versions same file sections can
    630         # override previous
    631         # sections.
    632 
    633         config = ConfigParser.ConfigParser()
    634         config.read(file_name)
    635 
    636         for section in config.sections():
    637 
    638             found = False
    639 
    640             for test in FSConfigFileParser._SECTIONS:
    641                 handler = test[0]
    642                 options = test[1]
    643 
    644                 if all([config.has_option(section, item) for item in options]):
    645                     handler = getattr(self, handler)
    646                     handler(file_name, section, config)
    647                     found = True
    648                     break
    649 
    650             if not found:
    651                 sys.exit('Invalid section "%s" in file: "%s"' % (section,
    652                                                                  file_name))
    653 
    654             # sort entries:
    655             # * specified path before prefix match
    656             # ** ie foo before f*
    657             # * lexicographical less than before other
    658             # ** ie boo before foo
    659             # Given these paths:
    660             # paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*']
    661             # The sort order would be:
    662             # paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*']
    663             # Thus the fs_config tools will match on specified paths before
    664             # attempting prefix, and match on the longest matching prefix.
    665             self._files.sort(key=FSConfigFileParser._file_key)
    666 
    667             # sort on value of (file_name, name, value, strvalue)
    668             # This is only cosmetic so AIDS are arranged in ascending order
    669             # within the generated file.
    670             self._aids.sort(key=lambda item: item.normalized_value)
    671 
    672     def _handle_aid(self, file_name, section_name, config):
    673         """Verifies an AID entry and adds it to the aid list.
    674 
    675         Calls sys.exit() with a descriptive message of the failure.
    676 
    677         Args:
    678             file_name (str): The filename of the config file being parsed.
    679             section_name (str): The section name currently being parsed.
    680             config (ConfigParser): The ConfigParser section being parsed that
    681                 the option values will come from.
    682         """
    683 
    684         def error_message(msg):
    685             """Creates an error message with current parsing state."""
    686             return '{} for: "{}" file: "{}"'.format(msg, section_name,
    687                                                     file_name)
    688 
    689         FSConfigFileParser._handle_dup_and_add('AID', file_name, section_name,
    690                                                self._seen_aids[0])
    691 
    692         match = FSConfigFileParser._AID_MATCH.match(section_name)
    693         invalid = match.end() if match else len(AID.PREFIX)
    694         if invalid != len(section_name):
    695             tmp_errmsg = ('Invalid characters in AID section at "%d" for: "%s"'
    696                           % (invalid, FSConfigFileParser._AID_ERR_MSG))
    697             sys.exit(error_message(tmp_errmsg))
    698 
    699         value = config.get(section_name, 'value')
    700 
    701         if not value:
    702             sys.exit(error_message('Found specified but unset "value"'))
    703 
    704         try:
    705             aid = AID(section_name, value, file_name, '/vendor/bin/sh')
    706         except ValueError as exception:
    707             sys.exit(error_message(exception))
    708 
    709         # Values must be within OEM range
    710         if not Utils.in_any_range(int(aid.value, 0), self._oem_ranges):
    711             emsg = '"value" not in valid range %s, got: %s'
    712             emsg = emsg % (str(self._oem_ranges), value)
    713             sys.exit(error_message(emsg))
    714 
    715         # use the normalized int value in the dict and detect
    716         # duplicate definitions of the same value
    717         FSConfigFileParser._handle_dup_and_add(
    718             'AID', file_name, aid.normalized_value, self._seen_aids[1])
    719 
    720         # Append aid tuple of (AID_*, base10(value), _path(value))
    721         # We keep the _path version of value so we can print that out in the
    722         # generated header so investigating parties can identify parts.
    723         # We store the base10 value for sorting, so everything is ascending
    724         # later.
    725         self._aids.append(aid)
    726 
    727     def _handle_path(self, file_name, section_name, config):
    728         """Add a file capability entry to the internal list.
    729 
    730         Handles a file capability entry, verifies it, and adds it to
    731         to the internal dirs or files list based on path. If it ends
    732         with a / its a dir. Internal use only.
    733 
    734         Calls sys.exit() on any validation error with message set.
    735 
    736         Args:
    737             file_name (str): The current name of the file being parsed.
    738             section_name (str): The name of the section to parse.
    739             config (str): The config parser.
    740         """
    741 
    742         FSConfigFileParser._handle_dup_and_add('path', file_name, section_name,
    743                                                self._seen_paths)
    744 
    745         mode = config.get(section_name, 'mode')
    746         user = config.get(section_name, 'user')
    747         group = config.get(section_name, 'group')
    748         caps = config.get(section_name, 'caps')
    749 
    750         errmsg = ('Found specified but unset option: \"%s" in file: \"' +
    751                   file_name + '\"')
    752 
    753         if not mode:
    754             sys.exit(errmsg % 'mode')
    755 
    756         if not user:
    757             sys.exit(errmsg % 'user')
    758 
    759         if not group:
    760             sys.exit(errmsg % 'group')
    761 
    762         if not caps:
    763             sys.exit(errmsg % 'caps')
    764 
    765         caps = caps.split()
    766 
    767         tmp = []
    768         for cap in caps:
    769             try:
    770                 # test if string is int, if it is, use as is.
    771                 int(cap, 0)
    772                 tmp.append(cap)
    773             except ValueError:
    774                 tmp.append('CAP_' + cap.upper())
    775 
    776         caps = tmp
    777 
    778         if len(mode) == 3:
    779             mode = '0' + mode
    780 
    781         try:
    782             int(mode, 8)
    783         except ValueError:
    784             sys.exit('Mode must be octal characters, got: "%s"' % mode)
    785 
    786         if len(mode) != 4:
    787             sys.exit('Mode must be 3 or 4 characters, got: "%s"' % mode)
    788 
    789         caps_str = ','.join(caps)
    790 
    791         entry = FSConfig(mode, user, group, caps_str, section_name, file_name)
    792         if section_name[-1] == '/':
    793             self._dirs.append(entry)
    794         else:
    795             self._files.append(entry)
    796 
    797     @property
    798     def files(self):
    799         """Get the list of FSConfig file entries.
    800 
    801         Returns:
    802              a list of FSConfig() objects for file paths.
    803         """
    804         return self._files
    805 
    806     @property
    807     def dirs(self):
    808         """Get the list of FSConfig dir entries.
    809 
    810         Returns:
    811             a list of FSConfig() objects for directory paths.
    812         """
    813         return self._dirs
    814 
    815     @property
    816     def aids(self):
    817         """Get the list of AID entries.
    818 
    819         Returns:
    820             a list of AID() objects.
    821         """
    822         return self._aids
    823 
    824     @staticmethod
    825     def _file_key(fs_config):
    826         """Used as the key paramter to sort.
    827 
    828         This is used as a the function to the key parameter of a sort.
    829         it wraps the string supplied in a class that implements the
    830         appropriate __lt__ operator for the sort on path strings. See
    831         StringWrapper class for more details.
    832 
    833         Args:
    834             fs_config (FSConfig): A FSConfig entry.
    835 
    836         Returns:
    837             A StringWrapper object
    838         """
    839 
    840         # Wrapper class for custom prefix matching strings
    841         class StringWrapper(object):
    842             """Wrapper class used for sorting prefix strings.
    843 
    844             The algorithm is as follows:
    845               - specified path before prefix match
    846                 - ie foo before f*
    847               - lexicographical less than before other
    848                 - ie boo before foo
    849 
    850             Given these paths:
    851             paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*']
    852             The sort order would be:
    853             paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*']
    854             Thus the fs_config tools will match on specified paths before
    855             attempting prefix, and match on the longest matching prefix.
    856             """
    857 
    858             def __init__(self, path):
    859                 """
    860                 Args:
    861                     path (str): the path string to wrap.
    862                 """
    863                 self.is_prefix = path[-1] == '*'
    864                 if self.is_prefix:
    865                     self.path = path[:-1]
    866                 else:
    867                     self.path = path
    868 
    869             def __lt__(self, other):
    870 
    871                 # if were both suffixed the smallest string
    872                 # is 'bigger'
    873                 if self.is_prefix and other.is_prefix:
    874                     result = len(self.path) > len(other.path)
    875                 # If I am an the suffix match, im bigger
    876                 elif self.is_prefix:
    877                     result = False
    878                 # If other is the suffix match, he's bigger
    879                 elif other.is_prefix:
    880                     result = True
    881                 # Alphabetical
    882                 else:
    883                     result = self.path < other.path
    884                 return result
    885 
    886         return StringWrapper(fs_config.path)
    887 
    888     @staticmethod
    889     def _handle_dup_and_add(name, file_name, section_name, seen):
    890         """Tracks and detects duplicates. Internal use only.
    891 
    892         Calls sys.exit() on a duplicate.
    893 
    894         Args:
    895             name (str): The name to use in the error reporting. The pretty
    896                 name for the section.
    897             file_name (str): The file currently being parsed.
    898             section_name (str): The name of the section. This would be path
    899                 or identifier depending on what's being parsed.
    900             seen (dict): The dictionary of seen things to check against.
    901         """
    902         if section_name in seen:
    903             dups = '"' + seen[section_name] + '" and '
    904             dups += file_name
    905             sys.exit('Duplicate %s "%s" found in files: %s' %
    906                      (name, section_name, dups))
    907 
    908         seen[section_name] = file_name
    909 
    910 
    911 class BaseGenerator(object):
    912     """Interface for Generators.
    913 
    914     Base class for generators, generators should implement
    915     these method stubs.
    916     """
    917 
    918     def add_opts(self, opt_group):
    919         """Used to add per-generator options to the command line.
    920 
    921         Args:
    922             opt_group (argument group object): The argument group to append to.
    923                 See the ArgParse docs for more details.
    924         """
    925 
    926         raise NotImplementedError("Not Implemented")
    927 
    928     def __call__(self, args):
    929         """This is called to do whatever magic the generator does.
    930 
    931         Args:
    932             args (dict): The arguments from ArgParse as a dictionary.
    933                 ie if you specified an argument of foo in add_opts, access
    934                 it via args['foo']
    935         """
    936 
    937         raise NotImplementedError("Not Implemented")
    938 
    939 
    940 @generator('fsconfig')
    941 class FSConfigGen(BaseGenerator):
    942     """Generates the android_filesystem_config.h file.
    943 
    944     Output is  used in generating fs_config_files and fs_config_dirs.
    945     """
    946 
    947     def __init__(self, *args, **kwargs):
    948         BaseGenerator.__init__(args, kwargs)
    949 
    950         self._oem_parser = None
    951         self._base_parser = None
    952         self._friendly_to_aid = None
    953         self._id_to_aid = None
    954         self._capability_parser = None
    955 
    956         self._partition = None
    957         self._all_partitions = None
    958         self._out_file = None
    959         self._generate_files = False
    960         self._generate_dirs = False
    961 
    962     def add_opts(self, opt_group):
    963 
    964         opt_group.add_argument(
    965             'fsconfig', nargs='+', help='The list of fsconfig files to parse')
    966 
    967         opt_group.add_argument(
    968             '--aid-header',
    969             required=True,
    970             help='An android_filesystem_config.h file'
    971             ' to parse AIDs and OEM Ranges from')
    972 
    973         opt_group.add_argument(
    974             '--capability-header',
    975             required=True,
    976             help='A capability.h file to parse capability defines from')
    977 
    978         opt_group.add_argument(
    979             '--partition',
    980             required=True,
    981             help='Partition to generate contents for')
    982 
    983         opt_group.add_argument(
    984             '--all-partitions',
    985             help='Comma separated list of all possible partitions, used to'
    986             ' ignore these partitions when generating the output for the system partition'
    987         )
    988 
    989         opt_group.add_argument(
    990             '--files', action='store_true', help='Output fs_config_files')
    991 
    992         opt_group.add_argument(
    993             '--dirs', action='store_true', help='Output fs_config_dirs')
    994 
    995         opt_group.add_argument('--out_file', required=True, help='Output file')
    996 
    997     def __call__(self, args):
    998 
    999         self._capability_parser = CapabilityHeaderParser(
   1000             args['capability_header'])
   1001         self._base_parser = AIDHeaderParser(args['aid_header'])
   1002         self._oem_parser = FSConfigFileParser(args['fsconfig'],
   1003                                               self._base_parser.oem_ranges)
   1004 
   1005         self._partition = args['partition']
   1006         self._all_partitions = args['all_partitions']
   1007         if self._partition == 'system' and self._all_partitions is None:
   1008             sys.exit(
   1009                 'All other partitions must be provided if generating output'
   1010                 ' for the system partition')
   1011 
   1012         self._out_file = args['out_file']
   1013 
   1014         self._generate_files = args['files']
   1015         self._generate_dirs = args['dirs']
   1016 
   1017         if self._generate_files and self._generate_dirs:
   1018             sys.exit('Only one of --files or --dirs can be provided')
   1019 
   1020         if not self._generate_files and not self._generate_dirs:
   1021             sys.exit('One of --files or --dirs must be provided')
   1022 
   1023         base_aids = self._base_parser.aids
   1024         oem_aids = self._oem_parser.aids
   1025 
   1026         # Detect name collisions on AIDs. Since friendly works as the
   1027         # identifier for collision testing and we need friendly later on for
   1028         # name resolution, just calculate and use friendly.
   1029         # {aid.friendly: aid for aid in base_aids}
   1030         base_friendly = {aid.friendly: aid for aid in base_aids}
   1031         oem_friendly = {aid.friendly: aid for aid in oem_aids}
   1032 
   1033         base_set = set(base_friendly.keys())
   1034         oem_set = set(oem_friendly.keys())
   1035 
   1036         common = base_set & oem_set
   1037 
   1038         if common:
   1039             emsg = 'Following AID Collisions detected for: \n'
   1040             for friendly in common:
   1041                 base = base_friendly[friendly]
   1042                 oem = oem_friendly[friendly]
   1043                 emsg += (
   1044                     'Identifier: "%s" Friendly Name: "%s" '
   1045                     'found in file "%s" and "%s"' %
   1046                     (base.identifier, base.friendly, base.found, oem.found))
   1047                 sys.exit(emsg)
   1048 
   1049         self._friendly_to_aid = oem_friendly
   1050         self._friendly_to_aid.update(base_friendly)
   1051 
   1052         self._id_to_aid = {aid.identifier: aid for aid in base_aids}
   1053         self._id_to_aid.update({aid.identifier: aid for aid in oem_aids})
   1054 
   1055         self._generate()
   1056 
   1057     def _to_fs_entry(self, fs_config, out_file):
   1058         """Converts an FSConfig entry to an fs entry.
   1059 
   1060         Writes the fs_config contents to the output file.
   1061 
   1062         Calls sys.exit() on error.
   1063 
   1064         Args:
   1065             fs_config (FSConfig): The entry to convert to write to file.
   1066             file (File): The file to write to.
   1067         """
   1068 
   1069         # Get some short names
   1070         mode = fs_config.mode
   1071         user = fs_config.user
   1072         group = fs_config.group
   1073         caps = fs_config.caps
   1074         path = fs_config.path
   1075 
   1076         emsg = 'Cannot convert "%s" to identifier!'
   1077 
   1078         # convert mode from octal string to integer
   1079         mode = int(mode, 8)
   1080 
   1081         # remap names to values
   1082         if AID.is_friendly(user):
   1083             if user not in self._friendly_to_aid:
   1084                 sys.exit(emsg % user)
   1085             user = self._friendly_to_aid[user].value
   1086         else:
   1087             if user not in self._id_to_aid:
   1088                 sys.exit(emsg % user)
   1089             user = self._id_to_aid[user].value
   1090 
   1091         if AID.is_friendly(group):
   1092             if group not in self._friendly_to_aid:
   1093                 sys.exit(emsg % group)
   1094             group = self._friendly_to_aid[group].value
   1095         else:
   1096             if group not in self._id_to_aid:
   1097                 sys.exit(emsg % group)
   1098             group = self._id_to_aid[group].value
   1099 
   1100         caps_dict = self._capability_parser.caps
   1101 
   1102         caps_value = 0
   1103 
   1104         try:
   1105             # test if caps is an int
   1106             caps_value = int(caps, 0)
   1107         except ValueError:
   1108             caps_split = caps.split(',')
   1109             for cap in caps_split:
   1110                 if cap not in caps_dict:
   1111                     sys.exit('Unkonwn cap "%s" found!' % cap)
   1112                 caps_value += 1 << caps_dict[cap]
   1113 
   1114         path_length_with_null = len(path) + 1
   1115         path_length_aligned_64 = (path_length_with_null + 7) & ~7
   1116         # 16 bytes of header plus the path length with alignment
   1117         length = 16 + path_length_aligned_64
   1118 
   1119         length_binary = bytearray(ctypes.c_uint16(length))
   1120         mode_binary = bytearray(ctypes.c_uint16(mode))
   1121         user_binary = bytearray(ctypes.c_uint16(int(user, 0)))
   1122         group_binary = bytearray(ctypes.c_uint16(int(group, 0)))
   1123         caps_binary = bytearray(ctypes.c_uint64(caps_value))
   1124         path_binary = ctypes.create_string_buffer(path,
   1125                                                   path_length_aligned_64).raw
   1126 
   1127         out_file.write(length_binary)
   1128         out_file.write(mode_binary)
   1129         out_file.write(user_binary)
   1130         out_file.write(group_binary)
   1131         out_file.write(caps_binary)
   1132         out_file.write(path_binary)
   1133 
   1134     def _emit_entry(self, fs_config):
   1135         """Returns a boolean whether or not to emit the input fs_config"""
   1136 
   1137         path = fs_config.path
   1138 
   1139         if self._partition == 'system':
   1140             for skip_partition in self._all_partitions.split(','):
   1141                 if path.startswith(skip_partition) or path.startswith(
   1142                         'system/' + skip_partition):
   1143                     return False
   1144             return True
   1145         else:
   1146             if path.startswith(
   1147                     self._partition) or path.startswith('system/' +
   1148                                                         self._partition):
   1149                 return True
   1150             return False
   1151 
   1152     def _generate(self):
   1153         """Generates an OEM android_filesystem_config.h header file to stdout.
   1154 
   1155         Args:
   1156             files ([FSConfig]): A list of FSConfig objects for file entries.
   1157             dirs ([FSConfig]): A list of FSConfig objects for directory
   1158                 entries.
   1159             aids ([AIDS]): A list of AID objects for Android Id entries.
   1160         """
   1161         dirs = self._oem_parser.dirs
   1162         files = self._oem_parser.files
   1163 
   1164         if self._generate_files:
   1165             with open(self._out_file, 'wb') as open_file:
   1166                 for fs_config in files:
   1167                     if self._emit_entry(fs_config):
   1168                         self._to_fs_entry(fs_config, open_file)
   1169 
   1170         if self._generate_dirs:
   1171             with open(self._out_file, 'wb') as open_file:
   1172                 for dir_entry in dirs:
   1173                     if self._emit_entry(dir_entry):
   1174                         self._to_fs_entry(dir_entry, open_file)
   1175 
   1176 
   1177 @generator('aidarray')
   1178 class AIDArrayGen(BaseGenerator):
   1179     """Generates the android_id static array."""
   1180 
   1181     _GENERATED = ('/*\n'
   1182                   ' * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY!\n'
   1183                   ' */')
   1184 
   1185     _INCLUDE = '#include <private/android_filesystem_config.h>'
   1186 
   1187     # Note that the android_id name field is of type 'const char[]' instead of
   1188     # 'const char*'.  While this seems less straightforward as we need to
   1189     # calculate the max length of all names, this allows the entire android_ids
   1190     # table to be placed in .rodata section instead of .data.rel.ro section,
   1191     # resulting in less memory pressure.
   1192     _STRUCT_FS_CONFIG = textwrap.dedent("""
   1193                          struct android_id_info {
   1194                              const char name[%d];
   1195                              unsigned aid;
   1196                          };""")
   1197 
   1198     _OPEN_ID_ARRAY = 'static const struct android_id_info android_ids[] = {'
   1199 
   1200     _ID_ENTRY = '    { "%s", %s },'
   1201 
   1202     _CLOSE_FILE_STRUCT = '};'
   1203 
   1204     _COUNT = ('#define android_id_count \\\n'
   1205               '    (sizeof(android_ids) / sizeof(android_ids[0]))')
   1206 
   1207     def add_opts(self, opt_group):
   1208 
   1209         opt_group.add_argument(
   1210             'hdrfile', help='The android_filesystem_config.h'
   1211             'file to parse')
   1212 
   1213     def __call__(self, args):
   1214 
   1215         hdr = AIDHeaderParser(args['hdrfile'])
   1216         max_name_length = max(len(aid.friendly) + 1 for aid in hdr.aids)
   1217 
   1218         print AIDArrayGen._GENERATED
   1219         print
   1220         print AIDArrayGen._INCLUDE
   1221         print
   1222         print AIDArrayGen._STRUCT_FS_CONFIG % max_name_length
   1223         print
   1224         print AIDArrayGen._OPEN_ID_ARRAY
   1225 
   1226         for aid in hdr.aids:
   1227             print AIDArrayGen._ID_ENTRY % (aid.friendly, aid.identifier)
   1228 
   1229         print AIDArrayGen._CLOSE_FILE_STRUCT
   1230         print
   1231         print AIDArrayGen._COUNT
   1232         print
   1233 
   1234 
   1235 @generator('oemaid')
   1236 class OEMAidGen(BaseGenerator):
   1237     """Generates the OEM AID_<name> value header file."""
   1238 
   1239     _GENERATED = ('/*\n'
   1240                   ' * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY!\n'
   1241                   ' */')
   1242 
   1243     _GENERIC_DEFINE = "#define %s\t%s"
   1244 
   1245     _FILE_COMMENT = '// Defined in file: \"%s\"'
   1246 
   1247     # Intentional trailing newline for readability.
   1248     _FILE_IFNDEF_DEFINE = ('#ifndef GENERATED_OEM_AIDS_H_\n'
   1249                            '#define GENERATED_OEM_AIDS_H_\n')
   1250 
   1251     _FILE_ENDIF = '#endif'
   1252 
   1253     def __init__(self):
   1254 
   1255         self._old_file = None
   1256 
   1257     def add_opts(self, opt_group):
   1258 
   1259         opt_group.add_argument(
   1260             'fsconfig', nargs='+', help='The list of fsconfig files to parse.')
   1261 
   1262         opt_group.add_argument(
   1263             '--aid-header',
   1264             required=True,
   1265             help='An android_filesystem_config.h file'
   1266             'to parse AIDs and OEM Ranges from')
   1267 
   1268     def __call__(self, args):
   1269 
   1270         hdr_parser = AIDHeaderParser(args['aid_header'])
   1271 
   1272         parser = FSConfigFileParser(args['fsconfig'], hdr_parser.oem_ranges)
   1273 
   1274         print OEMAidGen._GENERATED
   1275 
   1276         print OEMAidGen._FILE_IFNDEF_DEFINE
   1277 
   1278         for aid in parser.aids:
   1279             self._print_aid(aid)
   1280             print
   1281 
   1282         print OEMAidGen._FILE_ENDIF
   1283 
   1284     def _print_aid(self, aid):
   1285         """Prints a valid #define AID identifier to stdout.
   1286 
   1287         Args:
   1288             aid to print
   1289         """
   1290 
   1291         # print the source file location of the AID
   1292         found_file = aid.found
   1293         if found_file != self._old_file:
   1294             print OEMAidGen._FILE_COMMENT % found_file
   1295             self._old_file = found_file
   1296 
   1297         print OEMAidGen._GENERIC_DEFINE % (aid.identifier, aid.value)
   1298 
   1299 
   1300 @generator('passwd')
   1301 class PasswdGen(BaseGenerator):
   1302     """Generates the /etc/passwd file per man (5) passwd."""
   1303 
   1304     def __init__(self):
   1305 
   1306         self._old_file = None
   1307 
   1308     def add_opts(self, opt_group):
   1309 
   1310         opt_group.add_argument(
   1311             'fsconfig', nargs='+', help='The list of fsconfig files to parse.')
   1312 
   1313         opt_group.add_argument(
   1314             '--aid-header',
   1315             required=True,
   1316             help='An android_filesystem_config.h file'
   1317             'to parse AIDs and OEM Ranges from')
   1318 
   1319         opt_group.add_argument(
   1320             '--required-prefix',
   1321             required=False,
   1322             help='A prefix that the names are required to contain.')
   1323 
   1324     def __call__(self, args):
   1325 
   1326         hdr_parser = AIDHeaderParser(args['aid_header'])
   1327 
   1328         parser = FSConfigFileParser(args['fsconfig'], hdr_parser.oem_ranges)
   1329 
   1330         required_prefix = args['required_prefix']
   1331 
   1332         aids = parser.aids
   1333 
   1334         # nothing to do if no aids defined
   1335         if not aids:
   1336             return
   1337 
   1338         for aid in aids:
   1339             if required_prefix is None or aid.friendly.startswith(
   1340                     required_prefix):
   1341                 self._print_formatted_line(aid)
   1342             else:
   1343                 sys.exit("%s: AID '%s' must start with '%s'" %
   1344                          (args['fsconfig'], aid.friendly, required_prefix))
   1345 
   1346     def _print_formatted_line(self, aid):
   1347         """Prints the aid to stdout in the passwd format. Internal use only.
   1348 
   1349         Colon delimited:
   1350             login name, friendly name
   1351             encrypted password (optional)
   1352             uid (int)
   1353             gid (int)
   1354             User name or comment field
   1355             home directory
   1356             interpreter (optional)
   1357 
   1358         Args:
   1359             aid (AID): The aid to print.
   1360         """
   1361         if self._old_file != aid.found:
   1362             self._old_file = aid.found
   1363 
   1364         try:
   1365             logon, uid = Utils.get_login_and_uid_cleansed(aid)
   1366         except ValueError as exception:
   1367             sys.exit(exception)
   1368 
   1369         print "%s::%s:%s::/:%s" % (logon, uid, uid, aid.login_shell)
   1370 
   1371 
   1372 @generator('group')
   1373 class GroupGen(PasswdGen):
   1374     """Generates the /etc/group file per man (5) group."""
   1375 
   1376     # Overrides parent
   1377     def _print_formatted_line(self, aid):
   1378         """Prints the aid to stdout in the group format. Internal use only.
   1379 
   1380         Formatted (per man 5 group) like:
   1381             group_name:password:GID:user_list
   1382 
   1383         Args:
   1384             aid (AID): The aid to print.
   1385         """
   1386         if self._old_file != aid.found:
   1387             self._old_file = aid.found
   1388 
   1389         try:
   1390             logon, uid = Utils.get_login_and_uid_cleansed(aid)
   1391         except ValueError as exception:
   1392             sys.exit(exception)
   1393 
   1394         print "%s::%s:" % (logon, uid)
   1395 
   1396 
   1397 @generator('print')
   1398 class PrintGen(BaseGenerator):
   1399     """Prints just the constants and values, separated by spaces, in an easy to
   1400     parse format for use by other scripts.
   1401 
   1402     Each line is just the identifier and the value, separated by a space.
   1403     """
   1404 
   1405     def add_opts(self, opt_group):
   1406         opt_group.add_argument(
   1407             'aid-header', help='An android_filesystem_config.h file.')
   1408 
   1409     def __call__(self, args):
   1410 
   1411         hdr_parser = AIDHeaderParser(args['aid-header'])
   1412         aids = hdr_parser.aids
   1413 
   1414         aids.sort(key=lambda item: int(item.normalized_value))
   1415 
   1416         for aid in aids:
   1417             print '%s %s' % (aid.identifier, aid.normalized_value)
   1418 
   1419 
   1420 def main():
   1421     """Main entry point for execution."""
   1422 
   1423     opt_parser = argparse.ArgumentParser(
   1424         description='A tool for parsing fsconfig config files and producing' +
   1425         'digestable outputs.')
   1426     subparser = opt_parser.add_subparsers(help='generators')
   1427 
   1428     gens = generator.get()
   1429 
   1430     # for each gen, instantiate and add them as an option
   1431     for name, gen in gens.iteritems():
   1432 
   1433         generator_option_parser = subparser.add_parser(name, help=gen.__doc__)
   1434         generator_option_parser.set_defaults(which=name)
   1435 
   1436         opt_group = generator_option_parser.add_argument_group(name +
   1437                                                                ' options')
   1438         gen.add_opts(opt_group)
   1439 
   1440     args = opt_parser.parse_args()
   1441 
   1442     args_as_dict = vars(args)
   1443     which = args_as_dict['which']
   1444     del args_as_dict['which']
   1445 
   1446     gens[which](args_as_dict)
   1447 
   1448 
   1449 if __name__ == '__main__':
   1450     main()
   1451