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 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
206class 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
243class 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
515class 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
856class 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')
886class 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')
1099class 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')
1151class 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')
1216class 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')
1287class 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
1311def 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
1340if __name__ == '__main__':
1341    main()
1342