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