github.com/westcoastroms/westcoastroms-build@v0.0.0-20190928114312-2350e5a73030/build/make/tools/fs_config/fs_config_generator.py (about)

     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()