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