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