#!/usr/bin/env python

#
# Copyright 2016, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""
  privapp_permission.py: Generates privapp-permissions.xml file for
  apps in system/priv-app directory

  Usage:
  . build/envsetup.sh
  lunch product_name
  m -j32
  development/tools/privapp_permissions/privapp_permissions.py

"""

import os
import re
import subprocess
from xml.dom import minidom

try:
    ANDROID_PRODUCT_OUT = os.environ['ANDROID_PRODUCT_OUT']
    ANDROID_HOST_OUT = os.environ['ANDROID_HOST_OUT']
except KeyError as e:
    exit("Build environment not set up - " + str(e))
BASE_XML_FNAME = "privapp-permissions-platform.xml"

def main():
    # Parse base XML files in /etc dir, permissions listed there don't have to be re-added
    base_permissions = {}
    for xml_file in list_config_xml_files():
        parse_config_xml(xml_file, base_permissions)

    # Extract signature|privileged permissions available in the platform
    framework_apk = os.path.join(ANDROID_PRODUCT_OUT, 'system/framework/framework-res.apk')
    platform_priv_permissions = extract_priv_permissions(framework_apk)

    priv_apps = list_privapps()
    apps_redefine_base = []
    results = {}
    for priv_app in priv_apps:
        pkg_info = extract_pkg_and_requested_permissions(priv_app)
        pkg_name = pkg_info['package_name']
        priv_perms = get_priv_permissions(pkg_info['permissions'], platform_priv_permissions)
        # Compute diff against permissions defined in base file
        if base_permissions and (pkg_name in base_permissions):
            base_permissions_pkg = base_permissions[pkg_name]
            priv_perms = remove_base_permissions(priv_perms, base_permissions_pkg)
            if priv_perms:
                apps_redefine_base.append(pkg_name)
        if priv_perms:
            results[pkg_name] = sorted(priv_perms)

    print_xml(results, apps_redefine_base)

def print_xml(results, apps_redefine_base):
    """
    Print results to xml file
    """
    print """\
<?xml version="1.0" encoding="utf-8"?>
<permissions>"""
    for package_name in sorted(results):
        if package_name in apps_redefine_base:
            print '    <!-- Additional permissions on top of %s -->' % BASE_XML_FNAME
        print '    <privapp-permissions package="%s">' % package_name
        for p in results[package_name]:
            print '        <permission name="%s"/>' % p
        print '    </privapp-permissions>'
        print

    print "</permissions>"

def remove_base_permissions(priv_perms, base_perms):
    """
    Removes set of base_perms from set of priv_perms
    """
    if (not priv_perms) or (not base_perms): return priv_perms
    return set(priv_perms) - set(base_perms)

def get_priv_permissions(requested_perms, priv_perms):
    """
    Return only permissions that are in priv_perms set
    """
    return set(requested_perms).intersection(set(priv_perms))

def list_privapps():
    """
    Extract package name and requested permissions.
    """
    priv_app_dir = os.path.join(ANDROID_PRODUCT_OUT, 'system/priv-app')
    apks = []
    for dirName, subdirList, fileList in os.walk(priv_app_dir):
        for fname in fileList:
            if fname.endswith(".apk"):
                file_path = os.path.join(dirName, fname)
                apks.append(file_path)

    return apks

def list_config_xml_files():
    """
    Extract package name and requested permissions.
    """
    perm_dir = os.path.join(ANDROID_PRODUCT_OUT, 'system/etc/permissions')
    conf_dir = os.path.join(ANDROID_PRODUCT_OUT, 'system/etc/sysconfig')

    xml_files = []
    for root_dir in [perm_dir, conf_dir]:
        for dirName, subdirList, fileList in os.walk(root_dir):
            for fname in fileList:
                if fname.endswith(".xml"):
                    file_path = os.path.join(dirName, fname);
                    xml_files.append(file_path)
    return xml_files


def extract_pkg_and_requested_permissions(apk_path):
    """
    Extract package name and list of requested permissions from the
    dump of manifest file
    """
    aapt_args = ["d", "permissions", apk_path]
    txt = aapt(aapt_args)

    permissions = []
    package_name = None
    rawLines = txt.split('\n')
    for line in rawLines:
        regex = r"uses-permission: name='([\S]+)'"
        matches = re.search(regex, line)
        if matches:
            name = matches.group(1)
            permissions.append(name)
        regex = r"package: ([\S]+)"
        matches = re.search(regex, line)
        if matches:
            package_name = matches.group(1)

    return {'package_name': package_name, 'permissions' : permissions}

def extract_priv_permissions(apk_path):
    """
    Extract list signature|privileged permissions from the dump of
    manifest file
    """
    aapt_args = ["d", "xmltree", apk_path, "AndroidManifest.xml"]
    txt = aapt(aapt_args)
    rawLines = txt.split('\n')
    n = len(rawLines)
    i = 0
    permissions_list = []
    while i<n:
        line = rawLines[i]
        if line.find("E: permission (") != -1:
            i+=1
            name = None
            level = None
            while i<n:
                line = rawLines[i];
                if line.find("E: ") != -1:
                    break
                regex = r'A: android:name\([\S]+\)=\"([\S]+)\"';
                matches = re.search(regex, line);
                if matches:
                    name = matches.group(1)
                    i+=1
                    continue
                regex = r'A: android:protectionLevel\([^\)]+\)=\(type [\S]+\)0x([\S]+)';
                matches = re.search(regex, line);
                if matches:
                    level = int(matches.group(1), 16)
                    i+=1
                    continue
                i+=1
            if name and level and level & 0x12 == 0x12:
                    permissions_list.append(name)
        else:
            i+=1

    return permissions_list

def parse_config_xml(base_xml, results):
    """
    Parse an XML file that will be used as base.
    """
    dom = minidom.parse(base_xml)
    nodes =  dom.getElementsByTagName("privapp-permissions")
    for node in nodes:
        permissions = node.getElementsByTagName("permission")
        package_name = node.getAttribute('package');
        plist = []
        if package_name in results:
            plist = results[package_name]
        for p in permissions:
            perm_name = p.getAttribute('name')
            if perm_name:
                plist.append(perm_name)
        results[package_name] = plist
    return results

def aapt(args):
    """
    Run aapt command
    """
    return subprocess.check_output([ANDROID_HOST_OUT + '/bin/aapt'] + args,
        stderr=subprocess.STDOUT)

if __name__ == '__main__':
    main()