• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3#   Copyright 2017 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of the License at
8#
9#       http://www.apache.org/licenses/LICENSE-2.0
10#
11#   Unless required by applicable law or agreed to in writing, software
12#   distributed under the License is distributed on an "AS IS" BASIS,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17from __future__ import print_function
18from xml.dom import minidom
19
20import argparse
21import itertools
22import os
23import re
24import subprocess
25import sys
26import tempfile
27import shutil
28
29DEVICE_PREFIX = 'device:'
30ANDROID_NAME_REGEX = r'A: android:name\([\S]+\)=\"([\S]+)\"'
31ANDROID_PROTECTION_LEVEL_REGEX = \
32    r'A: android:protectionLevel\([^\)]+\)=\(type [\S]+\)0x([\S]+)'
33BASE_XML_FILENAME = 'privapp-permissions-platform.xml'
34
35HELP_MESSAGE = """\
36Generates privapp-permissions.xml file for priv-apps.
37
38Usage:
39    Specify which apk to generate priv-app permissions for. If no apk is \
40specified, this will default to all APKs under "<ANDROID_PRODUCT_OUT>/\
41<all the partitions>/priv-app/".
42
43    To specify a target partition(s), use "-p <PARTITION>," where <PARTITION> \
44can be "system", "product", "system/product", "system_ext", \
45"system/system_ext", "system,system/product,vendor,system_ext", etc.
46
47    When using adb, adb pull can take a long time. To see the adb pull \
48progress, use "-v"
49
50Examples:
51
52    For all APKs under $ANDROID_PRODUCT_OUT/<all partitions>/priv-app/:
53        # If the build environment has not been set up, do so:
54        . build/envsetup.sh
55        lunch product_name
56        m -j32
57        # then use:
58        cd development/tools/privapp_permissions/
59        ./privapp_permissions.py
60        # or to search for apks in "product" partition
61        ./privapp_permissions.py -p product
62        # or to search for apks in system, product, and vendor partitions
63        ./privapp_permissions.py -p system,product,vendor
64
65    For an APK against $ANDROID_PRODUCT_OUT/<all partitions>/etc/permissions/:
66        ./privapp_permissions.py path/to/the.apk
67        # or against /product/etc/permissions/
68        ./privapp_permissions.py path/to/the.apk -p product
69
70    For an APK already on the device against /<all partitions>/etc/permissions/:
71        ./privapp_permissions.py device:/device/path/to/the.apk
72        # or against /product/etc/permissions/
73        ./privapp_permissions.py path/to/the.apk -p product
74
75    For all APKs on a device under /<all partitions>/priv-app/:
76        ./privapp_permissions.py -d
77        # or if more than one device is attached
78        ./privapp_permissions.py -s <ANDROID_SERIAL>
79        # or for all APKs on the "system" partitions
80        ./privapp_permissions.py -d -p system
81"""
82
83# An array of all generated temp directories.
84temp_dirs = []
85# An array of all generated temp files.
86temp_files = []
87
88def vprint(enable, message, *args):
89    if enable:
90        # Use stderr to avoid poluting print_xml result
91        sys.stderr.write(message % args + '\n')
92
93class MissingResourceError(Exception):
94    """Raised when a dependency cannot be located."""
95
96
97class Adb(object):
98    """A small wrapper around ADB calls."""
99
100    def __init__(self, path, serial=None, verbose=False):
101        self.path = path
102        self.serial = serial
103        self.verbose = verbose
104
105    def pull(self, src, dst=None):
106        """A wrapper for `adb -s <SERIAL> pull <src> <dst>`.
107        Args:
108            src: The source path on the device
109            dst: The destination path on the host
110
111        Throws:
112            subprocess.CalledProcessError upon pull failure.
113        """
114        if not dst:
115            if self.call('shell \'if [ -d "%s" ]; then echo True; fi\'' % src):
116                dst = tempfile.mkdtemp()
117                temp_dirs.append(dst)
118            else:
119                _, dst = tempfile.mkstemp()
120                temp_files.append(dst)
121        self.call('pull %s %s' % (src, dst), False, self.verbose)
122        return dst
123
124    def call(self, cmdline, getoutput=True, verbose=False):
125        """Calls an adb command.
126
127        Throws:
128            subprocess.CalledProcessError upon command failure.
129        """
130        command = '%s -s %s %s' % (self.path, self.serial, cmdline)
131        if getoutput:
132            return get_output(command)
133        else:
134            # Handle verbose mode only when the output is not needed
135            # This is mainly for adb pull, which can take a long time
136            extracmd = ' > /dev/null 2>&1'
137            if verbose:
138                # Use stderr to avoid poluting print_xml result
139                extracmd = ' 1>&2'
140            os.system(command + extracmd)
141
142class Aapt(object):
143    def __init__(self, path):
144        self.path = path
145
146    def call(self, arguments):
147        """Run an aapt command with the given args.
148
149        Args:
150            arguments: a list of string arguments
151        Returns:
152            The output of the aapt command as a string.
153        """
154        output = subprocess.check_output([self.path] + arguments,
155                                         stderr=subprocess.STDOUT)
156        return output.decode(encoding='UTF-8')
157
158
159class Resources(object):
160    """A class that contains the resources needed to generate permissions.
161
162    Attributes:
163        adb: A wrapper class around ADB with a default serial. Only needed when
164             using -d, -s, or "device:"
165        _aapt_path: The path to aapt.
166    """
167
168    def __init__(self, adb_path=None, aapt_path=None, use_device=None,
169                 serial=None, partitions=None, verbose=False,
170                 writetodisk=None, systemfile=None, productfile=None,
171                 apks=None):
172        self.adb = Resources._resolve_adb(adb_path)
173        self.aapt = Resources._resolve_aapt(aapt_path)
174
175        self.verbose = self.adb.verbose = verbose
176        self.writetodisk = writetodisk
177        self.systemfile = systemfile;
178        self.productfile = productfile;
179
180        self._is_android_env = 'ANDROID_PRODUCT_OUT' in os.environ and \
181                               'ANDROID_HOST_OUT' in os.environ
182        use_device = use_device or serial or \
183                     (apks and DEVICE_PREFIX in '&'.join(apks))
184
185        self.adb.serial = self._resolve_serial(use_device, serial)
186
187        if self.adb.serial:
188            self.adb.call('root')
189            self.adb.call('wait-for-device')
190
191        if self.adb.serial is None and not self._is_android_env:
192            raise MissingResourceError(
193                'You must either set up your build environment, or specify a '
194                'device to run against. See --help for more info.')
195
196        if apks and (partitions == "all" or partitions.find(',') != -1):
197            # override the partition to "system
198            print('\n# Defaulting the target partition to "system". '
199                  'Use -p option to specify the target partition '
200                  '(must provide one target instead of a list).\n',
201                  file=sys.stderr)
202            partitions = "system"
203
204        if partitions == "all":
205            # This is the default scenario
206            # Find all the partitions where priv-app exists
207            self.partitions = self._get_partitions()
208        else:
209            # Initialize self.partitions with the specified partitions
210            self.partitions = []
211            for p in partitions.split(','):
212                if p.endswith('/'):
213                    p = p[:-1]
214                self.partitions.append(p)
215                # Check if the directory exists
216                self._check_dir(p + '/priv-app')
217
218        vprint(self.verbose,
219                '# Examining the partitions: ' + str(self.partitions))
220
221        # Create dictionary of array (partition as the key)
222        self.privapp_apks = self._resolve_apks(apks, self.partitions)
223        self.permissions_dirs = self._resolve_sys_paths('etc/permissions',
224                                                       self.partitions)
225        self.sysconfig_dirs = self._resolve_sys_paths('etc/sysconfig',
226                                                     self.partitions)
227
228        # Always use the one in /system partition,
229        # as that is the only place we will find framework-res.apk
230        self.framework_res_apk = self._resolve_sys_path('system/framework/'
231                                                        'framework-res.apk')
232    @staticmethod
233    def _resolve_adb(adb_path):
234        """Resolves ADB from either the cmdline argument or the os environment.
235
236        Args:
237            adb_path: The argument passed in for adb. Can be None.
238        Returns:
239            An Adb object.
240        Raises:
241            MissingResourceError if adb cannot be resolved.
242        """
243        if adb_path:
244            if os.path.isfile(adb_path):
245                adb = adb_path
246            else:
247                raise MissingResourceError('Cannot resolve adb: No such file '
248                                           '"%s" exists.' % adb_path)
249        else:
250            try:
251                adb = get_output('which adb').strip()
252            except subprocess.CalledProcessError as e:
253                print('Cannot resolve adb: ADB does not exist within path. '
254                      'Did you forget to setup the build environment or set '
255                      '--adb?',
256                      file=sys.stderr)
257                raise MissingResourceError(e)
258        # Start the adb server immediately so server daemon startup
259        # does not get added to the output of subsequent adb calls.
260        try:
261            get_output('%s start-server' % adb)
262            return Adb(adb)
263        except:
264            print('Unable to reach adb server daemon.', file=sys.stderr)
265            raise
266
267    @staticmethod
268    def _resolve_aapt(aapt_path):
269        """Resolves AAPT from either the cmdline argument or the os environment.
270
271        Returns:
272            An Aapt Object
273        """
274        if aapt_path:
275            if os.path.isfile(aapt_path):
276                return Aapt(aapt_path)
277            else:
278                raise MissingResourceError('Cannot resolve aapt: No such file '
279                                           '%s exists.' % aapt_path)
280        else:
281            try:
282                return Aapt(get_output('which aapt').strip())
283            except subprocess.CalledProcessError:
284                print('Cannot resolve aapt: AAPT does not exist within path. '
285                      'Did you forget to setup the build environment or set '
286                      '--aapt?',
287                      file=sys.stderr)
288                raise
289
290    def _resolve_serial(self, device, serial):
291        """Resolves the serial used for device files or generating permissions.
292
293        Returns:
294            If -s/--serial is specified, it will return that serial.
295            If -d or device: is found, it will grab the only available device.
296            If there are multiple devices, it will use $ANDROID_SERIAL.
297        Raises:
298            MissingResourceError if the resolved serial would not be usable.
299            subprocess.CalledProcessError if a command error occurs.
300        """
301        if device:
302            if serial:
303                try:
304                    output = get_output('%s -s %s get-state' %
305                                        (self.adb.path, serial))
306                except subprocess.CalledProcessError:
307                    raise MissingResourceError(
308                        'Received error when trying to get the state of '
309                        'device with serial "%s". Is it connected and in '
310                        'device mode?' % serial)
311                if 'device' not in output:
312                    raise MissingResourceError(
313                        'Device "%s" is not in device mode. Reboot the phone '
314                        'into device mode and try again.' % serial)
315                return serial
316
317            elif 'ANDROID_SERIAL' in os.environ:
318                serial = os.environ['ANDROID_SERIAL']
319                command = '%s -s %s get-state' % (self.adb, serial)
320                try:
321                    output = get_output(command)
322                except subprocess.CalledProcessError:
323                    raise MissingResourceError(
324                        'Device with serial $ANDROID_SERIAL ("%s") not '
325                        'found.' % serial)
326                if 'device' in output:
327                    return serial
328                raise MissingResourceError(
329                    'Device with serial $ANDROID_SERIAL ("%s") was '
330                    'found, but was not in the "device" state.')
331
332            # Parses `adb devices` so it only returns a string of serials.
333            get_serials_cmd = ('%s devices | tail -n +2 | head -n -1 | '
334                               'cut -f1' % self.adb.path)
335            try:
336                output = get_output(get_serials_cmd)
337                # If multiple serials appear in the output, raise an error.
338                if len(output.split()) > 1:
339                    raise MissingResourceError(
340                        'Multiple devices are connected. You must specify '
341                        'which device to run against with flag --serial.')
342                return output.strip()
343            except subprocess.CalledProcessError:
344                print('Unexpected error when querying for connected '
345                      'devices.', file=sys.stderr)
346                raise
347
348    def _get_partitions(self):
349        """Find all the partitions to examine
350
351        Returns:
352            The array of partitions where priv-app exists
353        Raises:
354            MissingResourceError find command over adb shell fails.
355        """
356        if not self.adb.serial:
357            privapp_dirs = get_output('cd  %s; find * -name "priv-app"'
358                                      % os.environ['ANDROID_PRODUCT_OUT']
359                                      + ' -type d | grep -v obj').split()
360        else:
361            try:
362                privapp_dirs = self.adb.call('shell find \'/!(proc)\' \
363                                           -name "priv-app" -type d').split()
364            except subprocess.CalledProcessError:
365                raise MissingResourceError(
366                    '"adb shell find / -name priv-app -type d" did not succeed'
367                    ' on device "%s".' % self.adb.serial)
368
369        # Remove 'priv-app' from the privapp_dirs
370        partitions = []
371        for i in range(len(privapp_dirs)):
372            partitions.append('/'.join(privapp_dirs[i].split('/')[:-1]))
373
374        return partitions
375
376    def _check_dir(self, directory):
377        """Check if a given directory is valid
378
379        Raises:
380            MissingResourceError if a given directory does not exist.
381        """
382        if not self.adb.serial:
383            if not os.path.isdir(os.environ['ANDROID_PRODUCT_OUT']
384                                 + '/' + directory):
385                raise MissingResourceError(
386                    '%s does not exist' % directory)
387        else:
388            try:
389                self.adb.call('shell ls %s' % directory)
390            except subprocess.CalledProcessError:
391                raise MissingResourceError(
392                    '"adb shell ls %s" did not succeed on '
393                    'device "%s".' % (directory, self.adb.serial))
394
395
396    def _resolve_apks(self, apks, partitions):
397        """Resolves all APKs to run against.
398
399        Returns:
400            If no apk is specified in the arguments, return all apks in
401            priv-app in all the partitions.
402            Otherwise, returns a list with the specified apk.
403        Throws:
404            MissingResourceError if the specified apk or
405            <partition>/priv-app cannot be found.
406        """
407        results = {}
408        if not apks:
409            for p in partitions:
410                results[p] = self._resolve_all_privapps(p)
411            return results
412
413        # The first element is what is passed via '-p' option
414        # (default is overwritten to 'system' when apk is specified)
415        p = partitions[0]
416        results[p] = []
417        for apk in apks:
418            if apk.startswith(DEVICE_PREFIX):
419                device_apk = apk[len(DEVICE_PREFIX):]
420                try:
421                    apk = self.adb.pull(device_apk)
422                except subprocess.CalledProcessError:
423                    raise MissingResourceError(
424                        'File "%s" could not be located on device "%s".' %
425                        (device_apk, self.adb.serial))
426                results[p].append(apk)
427            elif not os.path.isfile(apk):
428                raise MissingResourceError('File "%s" does not exist.' % apk)
429            else:
430               results[p].append(apk)
431        return results
432
433    def _resolve_all_privapps(self, partition):
434        """Resolves all APKs in <partition>/priv-app
435
436        Returns:
437            Return all apks in <partition>/priv-app
438        Throws:
439            MissingResourceError <partition>/priv-app cannot be found.
440        """
441        if not self.adb.serial:
442            priv_app_dir = os.path.join(os.environ['ANDROID_PRODUCT_OUT'],
443                                        partition + '/priv-app')
444        else:
445            try:
446                priv_app_dir = self.adb.pull(partition + '/priv-app/')
447            except subprocess.CalledProcessError:
448                raise MissingResourceError(
449                    'Directory "%s/priv-app" could not be pulled from on '
450                    'device "%s".' % (partition, self.adb.serial))
451        return get_output('find %s -name "*.apk"' % priv_app_dir).split()
452
453    def _resolve_sys_path(self, file_path):
454        """Resolves a path that is a part of an Android System Image."""
455        if not self.adb.serial:
456            return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], file_path)
457        else:
458            return self.adb.pull(file_path)
459
460    def _resolve_sys_paths(self, file_path, partitions):
461        """Resolves a path that is a part of an Android System Image, for the
462        specified partitions."""
463        results = {}
464        for p in partitions:
465            results[p] = self._resolve_sys_path(p + '/' + file_path)
466        return results
467
468
469def get_output(command):
470    """Returns the output of the command as a string.
471
472    Throws:
473        subprocess.CalledProcessError if exit status is non-zero.
474    """
475    output = subprocess.check_output(command, shell=True)
476    # For Python3.4, decode the byte string so it is usable.
477    return output.decode(encoding='UTF-8')
478
479
480def parse_args():
481    """Parses the CLI."""
482    parser = argparse.ArgumentParser(
483        description=HELP_MESSAGE,
484        formatter_class=argparse.RawDescriptionHelpFormatter)
485    parser.add_argument(
486        '-d',
487        '--device',
488        action='store_true',
489        default=False,
490        required=False,
491        help='Whether or not to generate the privapp_permissions file for the '
492             'build already on a device. See -s/--serial below for more '
493             'details.'
494    )
495    parser.add_argument(
496        '-v',
497        '--verbose',
498        action='store_true',
499        default=False,
500        required=False,
501        help='Whether or not to enable more verbose logs such as '
502             'adb pull progress to be shown'
503    )
504    parser.add_argument(
505        '--adb',
506        type=str,
507        required=False,
508        metavar='<ADB_PATH>',
509        help='Path to adb. If none specified, uses the environment\'s adb.'
510    )
511    parser.add_argument(
512        '--aapt',
513        type=str,
514        required=False,
515        metavar='<AAPT_PATH>',
516        help='Path to aapt. If none specified, uses the environment\'s aapt.'
517    )
518    parser.add_argument(
519        '-s',
520        '--serial',
521        type=str,
522        required=False,
523        metavar='<SERIAL>',
524        help='The serial of the device to generate permissions for. If no '
525             'serial is given, it will pick the only device connected over '
526             'adb. If multiple devices are found, it will default to '
527             '$ANDROID_SERIAL. Otherwise, the program will exit with error '
528             'code 1. If -s is given, -d is not needed.'
529    )
530    parser.add_argument(
531        '-p',
532        '--partitions',
533        type=str,
534        required=False,
535        default='all',
536        metavar='<PARTITION>',
537        help='The target partition(s) to examine permissions for. '
538             'It is set to "all" by default, which means all the partitions '
539             'where priv-app diectory exists will be examined'
540             'Use "," as a delimiter when specifying multiple partitions. '
541             'E.g. "system,product"'
542    )
543    parser.add_argument(
544        'apks',
545        nargs='*',
546        type=str,
547        help='A list of paths to priv-app APKs to generate permissions for. '
548             'To make a path device-side, prefix the path with "device:".'
549    )
550    parser.add_argument(
551        '-w',
552        '--writetodisk',
553        action='store_true',
554        default=False,
555        required=False,
556        help='Whether or not to store the generated permissions directly to '
557             'a file. See --systemfile/--productfile for more information.'
558    )
559    parser.add_argument(
560        '--systemfile',
561        default='./system.xml',
562        required=False,
563        help='Path to system permissions file. Default value is ./system.xml'
564    )
565    parser.add_argument(
566        '--productfile',
567        default='./product.xml',
568        required=False,
569        help='Path to system permissions file. Default value is ./product.xml'
570    )
571    cmd_args = parser.parse_args()
572
573    return cmd_args
574
575
576def create_permission_file(resources):
577    """Prints out/creates permission file with missing permissions."""
578    # First extract privileged permissions from framework-res.apk
579    priv_permissions = extract_priv_permissions(resources.aapt,
580                                                resources.framework_res_apk)
581
582    results = {}
583    for p in resources.partitions:
584        results[p], apps_redefine_base = \
585            generate_missing_permissions(resources, priv_permissions, p)
586        enable_print = True
587        vprint(enable_print, '#' * 80)
588        vprint(enable_print, '#')
589        if resources.writetodisk:
590            # Check if it is likely a product partition
591            if p.endswith('product'):
592                out_file_name = resources.productfile;
593            # Check if it is a system partition
594            elif p.endswith('system'):
595                out_file_name = resources.systemfile
596            # Fallback to the partition name itself
597            else:
598                out_file_name = str(p).replace('/', '_') + '.xml'
599
600            out_file = open(out_file_name, 'w')
601            vprint(enable_print, '# %s XML written to %s:', p, out_file_name)
602            vprint(enable_print, '#')
603            vprint(enable_print, '#' * 80)
604            print_xml(results[p], apps_redefine_base, p, out_file)
605            out_file.close()
606        else:
607            vprint(enable_print, '# %s XML:', p)
608            vprint(enable_print, '#')
609            vprint(enable_print, '#' * 80)
610
611        # Print it to stdout regardless of whether writing to a file or not
612        print_xml(results[p], apps_redefine_base, p)
613
614
615def generate_missing_permissions(resources, priv_permissions, partition):
616    """Generates the missing permissions for the specified partition."""
617    # Parse base XML files in /etc dir, permissions listed there don't have
618    # to be re-added
619    base_permissions = {}
620    base_xml_files = itertools.chain(
621        list_xml_files(resources.permissions_dirs[partition]),
622        list_xml_files(resources.sysconfig_dirs[partition]))
623
624    for xml_file in base_xml_files:
625        parse_config_xml(xml_file, base_permissions)
626
627    apps_redefine_base = []
628    results = {}
629    for priv_app in resources.privapp_apks[partition]:
630        pkg_info = extract_pkg_and_requested_permissions(resources.aapt,
631                                                         priv_app)
632        pkg_name = pkg_info['package_name']
633        # get intersection of what's requested by app and by framework
634        priv_perms = get_priv_permissions(pkg_info['permissions'],
635                                          priv_permissions)
636        # Compute diff against permissions defined in base file
637        if base_permissions and (pkg_name in base_permissions):
638            base_permissions_pkg = base_permissions[pkg_name]
639            priv_perms = remove_base_permissions(priv_perms,
640                                                 base_permissions_pkg)
641            if priv_perms:
642                apps_redefine_base.append(pkg_name)
643        if priv_perms:
644            results[pkg_name] = sorted(priv_perms)
645
646    return results, apps_redefine_base
647
648
649def print_xml(results, apps_redefine_base, partition, fd=sys.stdout):
650    """Print results to the given file."""
651    fd.write('<?xml version="1.0" encoding="utf-8"?>\n')
652    fd.write('<!-- for the partition: /%s -->\n' % partition)
653    fd.write('<permissions>\n')
654    for package_name in sorted(results):
655        if package_name in apps_redefine_base:
656            fd.write('    <!-- Additional permissions on top of %s -->\n' %
657                     BASE_XML_FILENAME)
658        fd.write('    <privapp-permissions package="%s">\n' % package_name)
659        for p in results[package_name]:
660            fd.write('        <permission name="%s"/>\n' % p)
661        fd.write('    </privapp-permissions>\n')
662        fd.write('\n')
663
664    fd.write('</permissions>\n')
665
666
667def remove_base_permissions(priv_perms, base_perms):
668    """Removes set of base_perms from set of priv_perms."""
669    if (not priv_perms) or (not base_perms):
670        return priv_perms
671    return set(priv_perms) - set(base_perms)
672
673
674def get_priv_permissions(requested_perms, priv_perms):
675    """Return only permissions that are in priv_perms set."""
676    return set(requested_perms).intersection(set(priv_perms))
677
678
679def list_xml_files(directory):
680    """Returns a list of all .xml files within a given directory.
681
682    Args:
683        directory: the directory to look for xml files in.
684    """
685    xml_files = []
686    for dirName, subdirList, file_list in os.walk(directory):
687        for file in file_list:
688            if file.endswith('.xml'):
689                file_path = os.path.join(dirName, file)
690                xml_files.append(file_path)
691    return xml_files
692
693
694def extract_pkg_and_requested_permissions(aapt, apk_path):
695    """
696    Extract package name and list of requested permissions from the
697    dump of manifest file
698    """
699    aapt_args = ['d', 'permissions', apk_path]
700    txt = aapt.call(aapt_args)
701
702    permissions = []
703    package_name = None
704    raw_lines = txt.split('\n')
705    for line in raw_lines:
706        regex = r"uses-permission.*: name='([\S]+)'"
707        matches = re.search(regex, line)
708        if matches:
709            name = matches.group(1)
710            permissions.append(name)
711        regex = r'package: ([\S]+)'
712        matches = re.search(regex, line)
713        if matches:
714            package_name = matches.group(1)
715
716    return {'package_name': package_name, 'permissions': permissions}
717
718
719def extract_priv_permissions(aapt, apk_path):
720    """Extract signature|privileged permissions from dump of manifest file."""
721    aapt_args = ['d', 'xmltree', apk_path, 'AndroidManifest.xml']
722    txt = aapt.call(aapt_args)
723    raw_lines = txt.split('\n')
724    n = len(raw_lines)
725    i = 0
726    permissions_list = []
727    while i < n:
728        line = raw_lines[i]
729        if line.find('E: permission (') != -1:
730            i += 1
731            name = None
732            level = None
733            while i < n:
734                line = raw_lines[i]
735                if line.find('E: ') != -1:
736                    break
737                matches = re.search(ANDROID_NAME_REGEX, line)
738                if matches:
739                    name = matches.group(1)
740                    i += 1
741                    continue
742                matches = re.search(ANDROID_PROTECTION_LEVEL_REGEX, line)
743                if matches:
744                    level = int(matches.group(1), 16)
745                    i += 1
746                    continue
747                i += 1
748            if name and level and level & 0x12 == 0x12:
749                permissions_list.append(name)
750        else:
751            i += 1
752
753    return permissions_list
754
755
756def parse_config_xml(base_xml, results):
757    """Parse an XML file that will be used as base."""
758    dom = minidom.parse(base_xml)
759    nodes = dom.getElementsByTagName('privapp-permissions')
760    for node in nodes:
761        permissions = (node.getElementsByTagName('permission') +
762                       node.getElementsByTagName('deny-permission'))
763        package_name = node.getAttribute('package')
764        plist = []
765        if package_name in results:
766            plist = results[package_name]
767        for p in permissions:
768            perm_name = p.getAttribute('name')
769            if perm_name:
770                plist.append(perm_name)
771        results[package_name] = plist
772    return results
773
774
775def cleanup():
776    """Cleans up temp files."""
777    for directory in temp_dirs:
778        shutil.rmtree(directory, ignore_errors=True)
779    for file in temp_files:
780        os.remove(file)
781    del temp_dirs[:]
782    del temp_files[:]
783
784if __name__ == '__main__':
785    args = parse_args()
786    try:
787        tool_resources = Resources(
788            aapt_path=args.aapt,
789            adb_path=args.adb,
790            use_device=args.device,
791            serial=args.serial,
792            partitions=args.partitions,
793            verbose=args.verbose,
794            writetodisk=args.writetodisk,
795            systemfile=args.systemfile,
796            productfile=args.productfile,
797            apks=args.apks
798        )
799        create_permission_file(tool_resources)
800    except MissingResourceError as e:
801        print(str(e), file=sys.stderr)
802        exit(1)
803    finally:
804        cleanup()
805