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