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