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