• 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    _OEM_START_KW = 'START'
316    _OEM_END_KW = 'END'
317    _OEM_RANGE = re.compile('%sOEM_RESERVED_[0-9]*_{0,1}(%s|%s)' %
318                            (AID.PREFIX, _OEM_START_KW, _OEM_END_KW))
319    # AID lines cannot end with _START or _END, ie AID_FOO is OK
320    # but AID_FOO_START is skiped. Note that AID_FOOSTART is NOT skipped.
321    _AID_SKIP_RANGE = ['_' + _OEM_START_KW, '_' + _OEM_END_KW]
322    _COLLISION_OK = ['AID_APP', 'AID_APP_START', 'AID_USER', 'AID_USER_OFFSET']
323
324    def __init__(self, aid_header):
325        """
326        Args:
327            aid_header (str): file name for the header
328                file containing AID entries.
329        """
330        self._aid_header = aid_header
331        self._aid_name_to_value = {}
332        self._aid_value_to_name = {}
333        self._oem_ranges = {}
334
335        with open(aid_header) as open_file:
336            self._parse(open_file)
337
338        try:
339            self._process_and_check()
340        except ValueError as exception:
341            sys.exit('Error processing parsed data: "%s"' % (str(exception)))
342
343    def _parse(self, aid_file):
344        """Parses an AID header file. Internal use only.
345
346        Args:
347            aid_file (file): The open AID header file to parse.
348        """
349
350        for lineno, line in enumerate(aid_file):
351
352            def error_message(msg):
353                """Creates an error message with the current parsing state."""
354                # pylint: disable=cell-var-from-loop
355                return 'Error "{}" in file: "{}" on line: {}'.format(
356                    msg, self._aid_header, str(lineno))
357
358            if AIDHeaderParser._AID_DEFINE.match(line):
359                chunks = line.split()
360                identifier = chunks[1]
361                value = chunks[2]
362
363                if any(
364                        x.match(identifier)
365                        for x in AIDHeaderParser._SKIP_AIDS):
366                    continue
367
368                try:
369                    if AIDHeaderParser._is_oem_range(identifier):
370                        self._handle_oem_range(identifier, value)
371                    elif not any(
372                            identifier.endswith(x)
373                            for x in AIDHeaderParser._AID_SKIP_RANGE):
374                        self._handle_aid(identifier, value)
375                except ValueError as exception:
376                    sys.exit(
377                        error_message('{} for "{}"'.format(
378                            exception, identifier)))
379
380    def _handle_aid(self, identifier, value):
381        """Handle an AID C #define.
382
383        Handles an AID, sanity checking, generating the friendly name and
384        adding it to the internal maps. Internal use only.
385
386        Args:
387            identifier (str): The name of the #define identifier. ie AID_FOO.
388            value (str): The value associated with the identifier.
389
390        Raises:
391            ValueError: With message set to indicate the error.
392        """
393
394        aid = AID(identifier, value, self._aid_header, '/system/bin/sh')
395
396        # duplicate name
397        if aid.friendly in self._aid_name_to_value:
398            raise ValueError('Duplicate aid "%s"' % identifier)
399
400        if value in self._aid_value_to_name and aid.identifier not in AIDHeaderParser._COLLISION_OK:
401            raise ValueError(
402                'Duplicate aid value "%s" for %s' % (value, identifier))
403
404        self._aid_name_to_value[aid.friendly] = aid
405        self._aid_value_to_name[value] = aid.friendly
406
407    def _handle_oem_range(self, identifier, value):
408        """Handle an OEM range C #define.
409
410        When encountering special AID defines, notably for the OEM ranges
411        this method handles sanity checking and adding them to the internal
412        maps. For internal use only.
413
414        Args:
415            identifier (str): The name of the #define identifier.
416                ie AID_OEM_RESERVED_START/END.
417            value (str): The value associated with the identifier.
418
419        Raises:
420            ValueError: With message set to indicate the error.
421        """
422
423        try:
424            int_value = int(value, 0)
425        except ValueError:
426            raise ValueError(
427                'Could not convert "%s" to integer value, got: "%s"' %
428                (identifier, value))
429
430        # convert AID_OEM_RESERVED_START or AID_OEM_RESERVED_<num>_START
431        # to AID_OEM_RESERVED or AID_OEM_RESERVED_<num>
432        is_start = identifier.endswith(AIDHeaderParser._OEM_START_KW)
433
434        if is_start:
435            tostrip = len(AIDHeaderParser._OEM_START_KW)
436        else:
437            tostrip = len(AIDHeaderParser._OEM_END_KW)
438
439        # ending _
440        tostrip = tostrip + 1
441
442        strip = identifier[:-tostrip]
443        if strip not in self._oem_ranges:
444            self._oem_ranges[strip] = []
445
446        if len(self._oem_ranges[strip]) > 2:
447            raise ValueError('Too many same OEM Ranges "%s"' % identifier)
448
449        if len(self._oem_ranges[strip]) == 1:
450            tmp = self._oem_ranges[strip][0]
451
452            if tmp == int_value:
453                raise ValueError('START and END values equal %u' % int_value)
454            elif is_start and tmp < int_value:
455                raise ValueError(
456                    'END value %u less than START value %u' % (tmp, int_value))
457            elif not is_start and tmp > int_value:
458                raise ValueError(
459                    'END value %u less than START value %u' % (int_value, tmp))
460
461        # Add START values to the head of the list and END values at the end.
462        # Thus, the list is ordered with index 0 as START and index 1 as END.
463        if is_start:
464            self._oem_ranges[strip].insert(0, int_value)
465        else:
466            self._oem_ranges[strip].append(int_value)
467
468    def _process_and_check(self):
469        """Process, check and populate internal data structures.
470
471        After parsing and generating the internal data structures, this method
472        is responsible for sanity checking ALL of the acquired data.
473
474        Raises:
475            ValueError: With the message set to indicate the specific error.
476        """
477
478        # tuplefy the lists since range() does not like them mutable.
479        self._oem_ranges = [
480            AIDHeaderParser._convert_lst_to_tup(k, v)
481            for k, v in self._oem_ranges.iteritems()
482        ]
483
484        # Check for overlapping ranges
485        for i, range1 in enumerate(self._oem_ranges):
486            for range2 in self._oem_ranges[i + 1:]:
487                if AIDHeaderParser._is_overlap(range1, range2):
488                    raise ValueError("Overlapping OEM Ranges found %s and %s" %
489                                     (str(range1), str(range2)))
490
491        # No core AIDs should be within any oem range.
492        for aid in self._aid_value_to_name:
493
494            if Utils.in_any_range(aid, self._oem_ranges):
495                name = self._aid_value_to_name[aid]
496                raise ValueError(
497                    'AID "%s" value: %u within reserved OEM Range: "%s"' %
498                    (name, aid, str(self._oem_ranges)))
499
500    @property
501    def oem_ranges(self):
502        """Retrieves the OEM closed ranges as a list of tuples.
503
504        Returns:
505            A list of closed range tuples: [ (0, 42), (50, 105) ... ]
506        """
507        return self._oem_ranges
508
509    @property
510    def aids(self):
511        """Retrieves the list of found AIDs.
512
513        Returns:
514            A list of AID() objects.
515        """
516        return self._aid_name_to_value.values()
517
518    @staticmethod
519    def _convert_lst_to_tup(name, lst):
520        """Converts a mutable list to a non-mutable tuple.
521
522        Used ONLY for ranges and thus enforces a length of 2.
523
524        Args:
525            lst (List): list that should be "tuplefied".
526
527        Raises:
528            ValueError if lst is not a list or len is not 2.
529
530        Returns:
531            Tuple(lst)
532        """
533        if not lst or len(lst) != 2:
534            raise ValueError('Mismatched range for "%s"' % name)
535
536        return tuple(lst)
537
538    @staticmethod
539    def _is_oem_range(aid):
540        """Detects if a given aid is within the reserved OEM range.
541
542        Args:
543            aid (int): The aid to test
544
545        Returns:
546            True if it is within the range, False otherwise.
547        """
548
549        return AIDHeaderParser._OEM_RANGE.match(aid)
550
551    @staticmethod
552    def _is_overlap(range_a, range_b):
553        """Calculates the overlap of two range tuples.
554
555        A range tuple is a closed range. A closed range includes its endpoints.
556        Note that python tuples use () notation which collides with the
557        mathematical notation for open ranges.
558
559        Args:
560            range_a: The first tuple closed range eg (0, 5).
561            range_b: The second tuple closed range eg (3, 7).
562
563        Returns:
564            True if they overlap, False otherwise.
565        """
566
567        return max(range_a[0], range_b[0]) <= min(range_a[1], range_b[1])
568
569
570class FSConfigFileParser(object):
571    """Parses a config.fs ini format file.
572
573    This class is responsible for parsing the config.fs ini format files.
574    It collects and checks all the data in these files and makes it available
575    for consumption post processed.
576    """
577
578    # These _AID vars work together to ensure that an AID section name
579    # cannot contain invalid characters for a C define or a passwd/group file.
580    # Since _AID_PREFIX is within the set of _AID_MATCH the error logic only
581    # checks end, if you change this, you may have to update the error
582    # detection code.
583    _AID_MATCH = re.compile('%s[A-Z0-9_]+' % AID.PREFIX)
584    _AID_ERR_MSG = 'Expecting upper case, a number or underscore'
585
586    # list of handler to required options, used to identify the
587    # parsing section
588    _SECTIONS = [('_handle_aid', ('value', )),
589                 ('_handle_path', ('mode', 'user', 'group', 'caps'))]
590
591    def __init__(self, config_files, oem_ranges):
592        """
593        Args:
594            config_files ([str]): The list of config.fs files to parse.
595                Note the filename is not important.
596            oem_ranges ([(),()]): range tuples indicating reserved OEM ranges.
597        """
598
599        self._files = []
600        self._dirs = []
601        self._aids = []
602
603        self._seen_paths = {}
604        # (name to file, value to aid)
605        self._seen_aids = ({}, {})
606
607        self._oem_ranges = oem_ranges
608
609        self._config_files = config_files
610
611        for config_file in self._config_files:
612            self._parse(config_file)
613
614    def _parse(self, file_name):
615        """Parses and verifies config.fs files. Internal use only.
616
617        Args:
618            file_name (str): The config.fs (PythonConfigParser file format)
619                file to parse.
620
621        Raises:
622            Anything raised by ConfigParser.read()
623        """
624
625        # Separate config parsers for each file found. If you use
626        # read(filenames...) later files can override earlier files which is
627        # not what we want. Track state across files and enforce with
628        # _handle_dup(). Note, strict ConfigParser is set to true in
629        # Python >= 3.2, so in previous versions same file sections can
630        # override previous
631        # sections.
632
633        config = ConfigParser.ConfigParser()
634        config.read(file_name)
635
636        for section in config.sections():
637
638            found = False
639
640            for test in FSConfigFileParser._SECTIONS:
641                handler = test[0]
642                options = test[1]
643
644                if all([config.has_option(section, item) for item in options]):
645                    handler = getattr(self, handler)
646                    handler(file_name, section, config)
647                    found = True
648                    break
649
650            if not found:
651                sys.exit('Invalid section "%s" in file: "%s"' % (section,
652                                                                 file_name))
653
654            # sort entries:
655            # * specified path before prefix match
656            # ** ie foo before f*
657            # * lexicographical less than before other
658            # ** ie boo before foo
659            # Given these paths:
660            # paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*']
661            # The sort order would be:
662            # paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*']
663            # Thus the fs_config tools will match on specified paths before
664            # attempting prefix, and match on the longest matching prefix.
665            self._files.sort(key=FSConfigFileParser._file_key)
666
667            # sort on value of (file_name, name, value, strvalue)
668            # This is only cosmetic so AIDS are arranged in ascending order
669            # within the generated file.
670            self._aids.sort(key=lambda item: item.normalized_value)
671
672    def _handle_aid(self, file_name, section_name, config):
673        """Verifies an AID entry and adds it to the aid list.
674
675        Calls sys.exit() with a descriptive message of the failure.
676
677        Args:
678            file_name (str): The filename of the config file being parsed.
679            section_name (str): The section name currently being parsed.
680            config (ConfigParser): The ConfigParser section being parsed that
681                the option values will come from.
682        """
683
684        def error_message(msg):
685            """Creates an error message with current parsing state."""
686            return '{} for: "{}" file: "{}"'.format(msg, section_name,
687                                                    file_name)
688
689        FSConfigFileParser._handle_dup_and_add('AID', file_name, section_name,
690                                               self._seen_aids[0])
691
692        match = FSConfigFileParser._AID_MATCH.match(section_name)
693        invalid = match.end() if match else len(AID.PREFIX)
694        if invalid != len(section_name):
695            tmp_errmsg = ('Invalid characters in AID section at "%d" for: "%s"'
696                          % (invalid, FSConfigFileParser._AID_ERR_MSG))
697            sys.exit(error_message(tmp_errmsg))
698
699        value = config.get(section_name, 'value')
700
701        if not value:
702            sys.exit(error_message('Found specified but unset "value"'))
703
704        try:
705            aid = AID(section_name, value, file_name, '/vendor/bin/sh')
706        except ValueError as exception:
707            sys.exit(error_message(exception))
708
709        # Values must be within OEM range
710        if not Utils.in_any_range(int(aid.value, 0), self._oem_ranges):
711            emsg = '"value" not in valid range %s, got: %s'
712            emsg = emsg % (str(self._oem_ranges), value)
713            sys.exit(error_message(emsg))
714
715        # use the normalized int value in the dict and detect
716        # duplicate definitions of the same value
717        FSConfigFileParser._handle_dup_and_add(
718            'AID', file_name, aid.normalized_value, self._seen_aids[1])
719
720        # Append aid tuple of (AID_*, base10(value), _path(value))
721        # We keep the _path version of value so we can print that out in the
722        # generated header so investigating parties can identify parts.
723        # We store the base10 value for sorting, so everything is ascending
724        # later.
725        self._aids.append(aid)
726
727    def _handle_path(self, file_name, section_name, config):
728        """Add a file capability entry to the internal list.
729
730        Handles a file capability entry, verifies it, and adds it to
731        to the internal dirs or files list based on path. If it ends
732        with a / its a dir. Internal use only.
733
734        Calls sys.exit() on any validation error with message set.
735
736        Args:
737            file_name (str): The current name of the file being parsed.
738            section_name (str): The name of the section to parse.
739            config (str): The config parser.
740        """
741
742        FSConfigFileParser._handle_dup_and_add('path', file_name, section_name,
743                                               self._seen_paths)
744
745        mode = config.get(section_name, 'mode')
746        user = config.get(section_name, 'user')
747        group = config.get(section_name, 'group')
748        caps = config.get(section_name, 'caps')
749
750        errmsg = ('Found specified but unset option: \"%s" in file: \"' +
751                  file_name + '\"')
752
753        if not mode:
754            sys.exit(errmsg % 'mode')
755
756        if not user:
757            sys.exit(errmsg % 'user')
758
759        if not group:
760            sys.exit(errmsg % 'group')
761
762        if not caps:
763            sys.exit(errmsg % 'caps')
764
765        caps = caps.split()
766
767        tmp = []
768        for cap in caps:
769            try:
770                # test if string is int, if it is, use as is.
771                int(cap, 0)
772                tmp.append(cap)
773            except ValueError:
774                tmp.append('CAP_' + cap.upper())
775
776        caps = tmp
777
778        if len(mode) == 3:
779            mode = '0' + mode
780
781        try:
782            int(mode, 8)
783        except ValueError:
784            sys.exit('Mode must be octal characters, got: "%s"' % mode)
785
786        if len(mode) != 4:
787            sys.exit('Mode must be 3 or 4 characters, got: "%s"' % mode)
788
789        caps_str = ','.join(caps)
790
791        entry = FSConfig(mode, user, group, caps_str, section_name, file_name)
792        if section_name[-1] == '/':
793            self._dirs.append(entry)
794        else:
795            self._files.append(entry)
796
797    @property
798    def files(self):
799        """Get the list of FSConfig file entries.
800
801        Returns:
802             a list of FSConfig() objects for file paths.
803        """
804        return self._files
805
806    @property
807    def dirs(self):
808        """Get the list of FSConfig dir entries.
809
810        Returns:
811            a list of FSConfig() objects for directory paths.
812        """
813        return self._dirs
814
815    @property
816    def aids(self):
817        """Get the list of AID entries.
818
819        Returns:
820            a list of AID() objects.
821        """
822        return self._aids
823
824    @staticmethod
825    def _file_key(fs_config):
826        """Used as the key paramter to sort.
827
828        This is used as a the function to the key parameter of a sort.
829        it wraps the string supplied in a class that implements the
830        appropriate __lt__ operator for the sort on path strings. See
831        StringWrapper class for more details.
832
833        Args:
834            fs_config (FSConfig): A FSConfig entry.
835
836        Returns:
837            A StringWrapper object
838        """
839
840        # Wrapper class for custom prefix matching strings
841        class StringWrapper(object):
842            """Wrapper class used for sorting prefix strings.
843
844            The algorithm is as follows:
845              - specified path before prefix match
846                - ie foo before f*
847              - lexicographical less than before other
848                - ie boo before foo
849
850            Given these paths:
851            paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*']
852            The sort order would be:
853            paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*']
854            Thus the fs_config tools will match on specified paths before
855            attempting prefix, and match on the longest matching prefix.
856            """
857
858            def __init__(self, path):
859                """
860                Args:
861                    path (str): the path string to wrap.
862                """
863                self.is_prefix = path[-1] == '*'
864                if self.is_prefix:
865                    self.path = path[:-1]
866                else:
867                    self.path = path
868
869            def __lt__(self, other):
870
871                # if were both suffixed the smallest string
872                # is 'bigger'
873                if self.is_prefix and other.is_prefix:
874                    result = len(self.path) > len(other.path)
875                # If I am an the suffix match, im bigger
876                elif self.is_prefix:
877                    result = False
878                # If other is the suffix match, he's bigger
879                elif other.is_prefix:
880                    result = True
881                # Alphabetical
882                else:
883                    result = self.path < other.path
884                return result
885
886        return StringWrapper(fs_config.path)
887
888    @staticmethod
889    def _handle_dup_and_add(name, file_name, section_name, seen):
890        """Tracks and detects duplicates. Internal use only.
891
892        Calls sys.exit() on a duplicate.
893
894        Args:
895            name (str): The name to use in the error reporting. The pretty
896                name for the section.
897            file_name (str): The file currently being parsed.
898            section_name (str): The name of the section. This would be path
899                or identifier depending on what's being parsed.
900            seen (dict): The dictionary of seen things to check against.
901        """
902        if section_name in seen:
903            dups = '"' + seen[section_name] + '" and '
904            dups += file_name
905            sys.exit('Duplicate %s "%s" found in files: %s' %
906                     (name, section_name, dups))
907
908        seen[section_name] = file_name
909
910
911class BaseGenerator(object):
912    """Interface for Generators.
913
914    Base class for generators, generators should implement
915    these method stubs.
916    """
917
918    def add_opts(self, opt_group):
919        """Used to add per-generator options to the command line.
920
921        Args:
922            opt_group (argument group object): The argument group to append to.
923                See the ArgParse docs for more details.
924        """
925
926        raise NotImplementedError("Not Implemented")
927
928    def __call__(self, args):
929        """This is called to do whatever magic the generator does.
930
931        Args:
932            args (dict): The arguments from ArgParse as a dictionary.
933                ie if you specified an argument of foo in add_opts, access
934                it via args['foo']
935        """
936
937        raise NotImplementedError("Not Implemented")
938
939
940@generator('fsconfig')
941class FSConfigGen(BaseGenerator):
942    """Generates the android_filesystem_config.h file.
943
944    Output is  used in generating fs_config_files and fs_config_dirs.
945    """
946
947    def __init__(self, *args, **kwargs):
948        BaseGenerator.__init__(args, kwargs)
949
950        self._oem_parser = None
951        self._base_parser = None
952        self._friendly_to_aid = None
953        self._id_to_aid = None
954        self._capability_parser = None
955
956        self._partition = None
957        self._all_partitions = None
958        self._out_file = None
959        self._generate_files = False
960        self._generate_dirs = False
961
962    def add_opts(self, opt_group):
963
964        opt_group.add_argument(
965            'fsconfig', nargs='+', help='The list of fsconfig files to parse')
966
967        opt_group.add_argument(
968            '--aid-header',
969            required=True,
970            help='An android_filesystem_config.h file'
971            ' to parse AIDs and OEM Ranges from')
972
973        opt_group.add_argument(
974            '--capability-header',
975            required=True,
976            help='A capability.h file to parse capability defines from')
977
978        opt_group.add_argument(
979            '--partition',
980            required=True,
981            help='Partition to generate contents for')
982
983        opt_group.add_argument(
984            '--all-partitions',
985            help='Comma separated list of all possible partitions, used to'
986            ' ignore these partitions when generating the output for the system partition'
987        )
988
989        opt_group.add_argument(
990            '--files', action='store_true', help='Output fs_config_files')
991
992        opt_group.add_argument(
993            '--dirs', action='store_true', help='Output fs_config_dirs')
994
995        opt_group.add_argument('--out_file', required=True, help='Output file')
996
997    def __call__(self, args):
998
999        self._capability_parser = CapabilityHeaderParser(
1000            args['capability_header'])
1001        self._base_parser = AIDHeaderParser(args['aid_header'])
1002        self._oem_parser = FSConfigFileParser(args['fsconfig'],
1003                                              self._base_parser.oem_ranges)
1004
1005        self._partition = args['partition']
1006        self._all_partitions = args['all_partitions']
1007        if self._partition == 'system' and self._all_partitions is None:
1008            sys.exit(
1009                'All other partitions must be provided if generating output'
1010                ' for the system partition')
1011
1012        self._out_file = args['out_file']
1013
1014        self._generate_files = args['files']
1015        self._generate_dirs = args['dirs']
1016
1017        if self._generate_files and self._generate_dirs:
1018            sys.exit('Only one of --files or --dirs can be provided')
1019
1020        if not self._generate_files and not self._generate_dirs:
1021            sys.exit('One of --files or --dirs must be provided')
1022
1023        base_aids = self._base_parser.aids
1024        oem_aids = self._oem_parser.aids
1025
1026        # Detect name collisions on AIDs. Since friendly works as the
1027        # identifier for collision testing and we need friendly later on for
1028        # name resolution, just calculate and use friendly.
1029        # {aid.friendly: aid for aid in base_aids}
1030        base_friendly = {aid.friendly: aid for aid in base_aids}
1031        oem_friendly = {aid.friendly: aid for aid in oem_aids}
1032
1033        base_set = set(base_friendly.keys())
1034        oem_set = set(oem_friendly.keys())
1035
1036        common = base_set & oem_set
1037
1038        if common:
1039            emsg = 'Following AID Collisions detected for: \n'
1040            for friendly in common:
1041                base = base_friendly[friendly]
1042                oem = oem_friendly[friendly]
1043                emsg += (
1044                    'Identifier: "%s" Friendly Name: "%s" '
1045                    'found in file "%s" and "%s"' %
1046                    (base.identifier, base.friendly, base.found, oem.found))
1047                sys.exit(emsg)
1048
1049        self._friendly_to_aid = oem_friendly
1050        self._friendly_to_aid.update(base_friendly)
1051
1052        self._id_to_aid = {aid.identifier: aid for aid in base_aids}
1053        self._id_to_aid.update({aid.identifier: aid for aid in oem_aids})
1054
1055        self._generate()
1056
1057    def _to_fs_entry(self, fs_config, out_file):
1058        """Converts an FSConfig entry to an fs entry.
1059
1060        Writes the fs_config contents to the output file.
1061
1062        Calls sys.exit() on error.
1063
1064        Args:
1065            fs_config (FSConfig): The entry to convert to write to file.
1066            file (File): The file to write to.
1067        """
1068
1069        # Get some short names
1070        mode = fs_config.mode
1071        user = fs_config.user
1072        group = fs_config.group
1073        caps = fs_config.caps
1074        path = fs_config.path
1075
1076        emsg = 'Cannot convert "%s" to identifier!'
1077
1078        # convert mode from octal string to integer
1079        mode = int(mode, 8)
1080
1081        # remap names to values
1082        if AID.is_friendly(user):
1083            if user not in self._friendly_to_aid:
1084                sys.exit(emsg % user)
1085            user = self._friendly_to_aid[user].value
1086        else:
1087            if user not in self._id_to_aid:
1088                sys.exit(emsg % user)
1089            user = self._id_to_aid[user].value
1090
1091        if AID.is_friendly(group):
1092            if group not in self._friendly_to_aid:
1093                sys.exit(emsg % group)
1094            group = self._friendly_to_aid[group].value
1095        else:
1096            if group not in self._id_to_aid:
1097                sys.exit(emsg % group)
1098            group = self._id_to_aid[group].value
1099
1100        caps_dict = self._capability_parser.caps
1101
1102        caps_value = 0
1103
1104        try:
1105            # test if caps is an int
1106            caps_value = int(caps, 0)
1107        except ValueError:
1108            caps_split = caps.split(',')
1109            for cap in caps_split:
1110                if cap not in caps_dict:
1111                    sys.exit('Unkonwn cap "%s" found!' % cap)
1112                caps_value += 1 << caps_dict[cap]
1113
1114        path_length_with_null = len(path) + 1
1115        path_length_aligned_64 = (path_length_with_null + 7) & ~7
1116        # 16 bytes of header plus the path length with alignment
1117        length = 16 + path_length_aligned_64
1118
1119        length_binary = bytearray(ctypes.c_uint16(length))
1120        mode_binary = bytearray(ctypes.c_uint16(mode))
1121        user_binary = bytearray(ctypes.c_uint16(int(user, 0)))
1122        group_binary = bytearray(ctypes.c_uint16(int(group, 0)))
1123        caps_binary = bytearray(ctypes.c_uint64(caps_value))
1124        path_binary = ctypes.create_string_buffer(path,
1125                                                  path_length_aligned_64).raw
1126
1127        out_file.write(length_binary)
1128        out_file.write(mode_binary)
1129        out_file.write(user_binary)
1130        out_file.write(group_binary)
1131        out_file.write(caps_binary)
1132        out_file.write(path_binary)
1133
1134    def _emit_entry(self, fs_config):
1135        """Returns a boolean whether or not to emit the input fs_config"""
1136
1137        path = fs_config.path
1138
1139        if self._partition == 'system':
1140            for skip_partition in self._all_partitions.split(','):
1141                if path.startswith(skip_partition) or path.startswith(
1142                        'system/' + skip_partition):
1143                    return False
1144            return True
1145        else:
1146            if path.startswith(
1147                    self._partition) or path.startswith('system/' +
1148                                                        self._partition):
1149                return True
1150            return False
1151
1152    def _generate(self):
1153        """Generates an OEM android_filesystem_config.h header file to stdout.
1154
1155        Args:
1156            files ([FSConfig]): A list of FSConfig objects for file entries.
1157            dirs ([FSConfig]): A list of FSConfig objects for directory
1158                entries.
1159            aids ([AIDS]): A list of AID objects for Android Id entries.
1160        """
1161        dirs = self._oem_parser.dirs
1162        files = self._oem_parser.files
1163
1164        if self._generate_files:
1165            with open(self._out_file, 'wb') as open_file:
1166                for fs_config in files:
1167                    if self._emit_entry(fs_config):
1168                        self._to_fs_entry(fs_config, open_file)
1169
1170        if self._generate_dirs:
1171            with open(self._out_file, 'wb') as open_file:
1172                for dir_entry in dirs:
1173                    if self._emit_entry(dir_entry):
1174                        self._to_fs_entry(dir_entry, open_file)
1175
1176
1177@generator('aidarray')
1178class AIDArrayGen(BaseGenerator):
1179    """Generates the android_id static array."""
1180
1181    _GENERATED = ('/*\n'
1182                  ' * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY!\n'
1183                  ' */')
1184
1185    _INCLUDE = '#include <private/android_filesystem_config.h>'
1186
1187    # Note that the android_id name field is of type 'const char[]' instead of
1188    # 'const char*'.  While this seems less straightforward as we need to
1189    # calculate the max length of all names, this allows the entire android_ids
1190    # table to be placed in .rodata section instead of .data.rel.ro section,
1191    # resulting in less memory pressure.
1192    _STRUCT_FS_CONFIG = textwrap.dedent("""
1193                         struct android_id_info {
1194                             const char name[%d];
1195                             unsigned aid;
1196                         };""")
1197
1198    _OPEN_ID_ARRAY = 'static const struct android_id_info android_ids[] = {'
1199
1200    _ID_ENTRY = '    { "%s", %s },'
1201
1202    _CLOSE_FILE_STRUCT = '};'
1203
1204    _COUNT = ('#define android_id_count \\\n'
1205              '    (sizeof(android_ids) / sizeof(android_ids[0]))')
1206
1207    def add_opts(self, opt_group):
1208
1209        opt_group.add_argument(
1210            'hdrfile', help='The android_filesystem_config.h'
1211            'file to parse')
1212
1213    def __call__(self, args):
1214
1215        hdr = AIDHeaderParser(args['hdrfile'])
1216        max_name_length = max(len(aid.friendly) + 1 for aid in hdr.aids)
1217
1218        print AIDArrayGen._GENERATED
1219        print
1220        print AIDArrayGen._INCLUDE
1221        print
1222        print AIDArrayGen._STRUCT_FS_CONFIG % max_name_length
1223        print
1224        print AIDArrayGen._OPEN_ID_ARRAY
1225
1226        for aid in hdr.aids:
1227            print AIDArrayGen._ID_ENTRY % (aid.friendly, aid.identifier)
1228
1229        print AIDArrayGen._CLOSE_FILE_STRUCT
1230        print
1231        print AIDArrayGen._COUNT
1232        print
1233
1234
1235@generator('oemaid')
1236class OEMAidGen(BaseGenerator):
1237    """Generates the OEM AID_<name> value header file."""
1238
1239    _GENERATED = ('/*\n'
1240                  ' * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY!\n'
1241                  ' */')
1242
1243    _GENERIC_DEFINE = "#define %s\t%s"
1244
1245    _FILE_COMMENT = '// Defined in file: \"%s\"'
1246
1247    # Intentional trailing newline for readability.
1248    _FILE_IFNDEF_DEFINE = ('#ifndef GENERATED_OEM_AIDS_H_\n'
1249                           '#define GENERATED_OEM_AIDS_H_\n')
1250
1251    _FILE_ENDIF = '#endif'
1252
1253    def __init__(self):
1254
1255        self._old_file = None
1256
1257    def add_opts(self, opt_group):
1258
1259        opt_group.add_argument(
1260            'fsconfig', nargs='+', help='The list of fsconfig files to parse.')
1261
1262        opt_group.add_argument(
1263            '--aid-header',
1264            required=True,
1265            help='An android_filesystem_config.h file'
1266            'to parse AIDs and OEM Ranges from')
1267
1268    def __call__(self, args):
1269
1270        hdr_parser = AIDHeaderParser(args['aid_header'])
1271
1272        parser = FSConfigFileParser(args['fsconfig'], hdr_parser.oem_ranges)
1273
1274        print OEMAidGen._GENERATED
1275
1276        print OEMAidGen._FILE_IFNDEF_DEFINE
1277
1278        for aid in parser.aids:
1279            self._print_aid(aid)
1280            print
1281
1282        print OEMAidGen._FILE_ENDIF
1283
1284    def _print_aid(self, aid):
1285        """Prints a valid #define AID identifier to stdout.
1286
1287        Args:
1288            aid to print
1289        """
1290
1291        # print the source file location of the AID
1292        found_file = aid.found
1293        if found_file != self._old_file:
1294            print OEMAidGen._FILE_COMMENT % found_file
1295            self._old_file = found_file
1296
1297        print OEMAidGen._GENERIC_DEFINE % (aid.identifier, aid.value)
1298
1299
1300@generator('passwd')
1301class PasswdGen(BaseGenerator):
1302    """Generates the /etc/passwd file per man (5) passwd."""
1303
1304    def __init__(self):
1305
1306        self._old_file = None
1307
1308    def add_opts(self, opt_group):
1309
1310        opt_group.add_argument(
1311            'fsconfig', nargs='+', help='The list of fsconfig files to parse.')
1312
1313        opt_group.add_argument(
1314            '--aid-header',
1315            required=True,
1316            help='An android_filesystem_config.h file'
1317            'to parse AIDs and OEM Ranges from')
1318
1319        opt_group.add_argument(
1320            '--required-prefix',
1321            required=False,
1322            help='A prefix that the names are required to contain.')
1323
1324    def __call__(self, args):
1325
1326        hdr_parser = AIDHeaderParser(args['aid_header'])
1327
1328        parser = FSConfigFileParser(args['fsconfig'], hdr_parser.oem_ranges)
1329
1330        required_prefix = args['required_prefix']
1331
1332        aids = parser.aids
1333
1334        # nothing to do if no aids defined
1335        if not aids:
1336            return
1337
1338        for aid in aids:
1339            if required_prefix is None or aid.friendly.startswith(
1340                    required_prefix):
1341                self._print_formatted_line(aid)
1342            else:
1343                sys.exit("%s: AID '%s' must start with '%s'" %
1344                         (args['fsconfig'], aid.friendly, required_prefix))
1345
1346    def _print_formatted_line(self, aid):
1347        """Prints the aid to stdout in the passwd format. Internal use only.
1348
1349        Colon delimited:
1350            login name, friendly name
1351            encrypted password (optional)
1352            uid (int)
1353            gid (int)
1354            User name or comment field
1355            home directory
1356            interpreter (optional)
1357
1358        Args:
1359            aid (AID): The aid to print.
1360        """
1361        if self._old_file != aid.found:
1362            self._old_file = aid.found
1363
1364        try:
1365            logon, uid = Utils.get_login_and_uid_cleansed(aid)
1366        except ValueError as exception:
1367            sys.exit(exception)
1368
1369        print "%s::%s:%s::/:%s" % (logon, uid, uid, aid.login_shell)
1370
1371
1372@generator('group')
1373class GroupGen(PasswdGen):
1374    """Generates the /etc/group file per man (5) group."""
1375
1376    # Overrides parent
1377    def _print_formatted_line(self, aid):
1378        """Prints the aid to stdout in the group format. Internal use only.
1379
1380        Formatted (per man 5 group) like:
1381            group_name:password:GID:user_list
1382
1383        Args:
1384            aid (AID): The aid to print.
1385        """
1386        if self._old_file != aid.found:
1387            self._old_file = aid.found
1388
1389        try:
1390            logon, uid = Utils.get_login_and_uid_cleansed(aid)
1391        except ValueError as exception:
1392            sys.exit(exception)
1393
1394        print "%s::%s:" % (logon, uid)
1395
1396
1397@generator('print')
1398class PrintGen(BaseGenerator):
1399    """Prints just the constants and values, separated by spaces, in an easy to
1400    parse format for use by other scripts.
1401
1402    Each line is just the identifier and the value, separated by a space.
1403    """
1404
1405    def add_opts(self, opt_group):
1406        opt_group.add_argument(
1407            'aid-header', help='An android_filesystem_config.h file.')
1408
1409    def __call__(self, args):
1410
1411        hdr_parser = AIDHeaderParser(args['aid-header'])
1412        aids = hdr_parser.aids
1413
1414        aids.sort(key=lambda item: int(item.normalized_value))
1415
1416        for aid in aids:
1417            print '%s %s' % (aid.identifier, aid.normalized_value)
1418
1419
1420def main():
1421    """Main entry point for execution."""
1422
1423    opt_parser = argparse.ArgumentParser(
1424        description='A tool for parsing fsconfig config files and producing' +
1425        'digestable outputs.')
1426    subparser = opt_parser.add_subparsers(help='generators')
1427
1428    gens = generator.get()
1429
1430    # for each gen, instantiate and add them as an option
1431    for name, gen in gens.iteritems():
1432
1433        generator_option_parser = subparser.add_parser(name, help=gen.__doc__)
1434        generator_option_parser.set_defaults(which=name)
1435
1436        opt_group = generator_option_parser.add_argument_group(name +
1437                                                               ' options')
1438        gen.add_opts(opt_group)
1439
1440    args = opt_parser.parse_args()
1441
1442    args_as_dict = vars(args)
1443    which = args_as_dict['which']
1444    del args_as_dict['which']
1445
1446    gens[which](args_as_dict)
1447
1448
1449if __name__ == '__main__':
1450    main()
1451