chiark / gitweb /
use androguard if aapt isn't found
authorthez3ro <io@thezero.org>
Thu, 13 Apr 2017 12:18:48 +0000 (14:18 +0200)
committerthez3ro <io@thezero.org>
Thu, 4 May 2017 21:35:17 +0000 (23:35 +0200)
fdroidserver/build.py
fdroidserver/common.py
fdroidserver/update.py
tests/androguard_test.py [new file with mode: 0644]
tests/metadata/apk/info.guardianproject.urzip.yaml [new file with mode: 0644]
tests/metadata/apk/org.dyndns.fules.ck.yaml [new file with mode: 0644]
tests/org.dyndns.fules.ck_20.apk [new file with mode: 0644]
tests/update.TestCase

index f0f027be98a375286e81b62300065ee48da400ee..5b21939b630c124731e99ed24f568a6d46c2e40b 100644 (file)
@@ -483,15 +483,23 @@ def capitalize_intact(string):
     return string[0].upper() + string[1:]
 
 
-def get_metadata_from_apk(app, build, apkfile):
-    """get the required metadata from the built APK"""
+def has_native_code(apkobj):
+    """aapt checks if there are architecture folders under the lib/ folder
+    so we are simulating the same behaviour"""
+    arch_re = re.compile("^lib/(.*)/.*$")
+    arch = [file for file in apkobj.get_files() if arch_re.match(file)]
+    return False if not arch else True
 
-    p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
 
+def get_apk_metadata_aapt(apkfile):
+    """aapt function to extract versionCode, versionName, packageName and nativecode"""
     vercode = None
     version = None
     foundid = None
     nativecode = None
+
+    p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
+
     for line in p.output.splitlines():
         if line.startswith("package:"):
             pat = re.compile(".*name='([a-zA-Z0-9._]*)'.*")
@@ -509,6 +517,38 @@ def get_metadata_from_apk(app, build, apkfile):
         elif line.startswith("native-code:"):
             nativecode = line[12:]
 
+    return vercode, version, foundid, nativecode
+
+
+def get_apk_metadata_androguard(apkfile):
+    """androguard function to extract versionCode, versionName, packageName and nativecode"""
+    try:
+        from androguard.core.bytecodes.apk import APK
+        apkobject = APK(apkfile)
+    except ImportError:
+        raise BuildException("androguard library is not installed and aapt binary not found")
+    except FileNotFoundError:
+        raise BuildException("Could not open apk file for metadata analysis")
+
+    if not apkobject.is_valid_APK():
+        raise BuildException("Invalid APK provided")
+
+    foundid = apkobject.get_package()
+    vercode = apkobject.get_androidversion_code()
+    version = apkobject.get_androidversion_name()
+    nativecode = has_native_code(apkobject)
+
+    return vercode, version, foundid, nativecode
+
+
+def get_metadata_from_apk(app, build, apkfile):
+    """get the required metadata from the built APK"""
+
+    if common.set_command_in_config('aapt'):
+        vercode, version, foundid, nativecode = get_apk_metadata_aapt(apkfile)
+    else:
+        vercode, version, foundid, nativecode = get_apk_metadata_androguard(apkfile)
+
     # Ignore empty strings or any kind of space/newline chars that we don't
     # care about
     if nativecode is not None:
@@ -533,7 +573,6 @@ def get_metadata_from_apk(app, build, apkfile):
 
 def build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, extlib_dir, tmp_dir, force, onserver, refresh):
     """Do a build locally."""
-
     ndk_path = build.ndk_path()
     if build.ndk or (build.buildjni and build.buildjni != ['no']):
         if not ndk_path:
index 2d3864d855a46c6bf4bead2c8f886e265006ebd3..df90e5f4be8f403147b1e3b6cdede1480e8f1ccd 100644 (file)
@@ -46,6 +46,8 @@ from pyasn1.codec.der import decoder, encoder
 from pyasn1_modules import rfc2315
 from pyasn1.error import PyAsn1Error
 
+from distutils.util import strtobool
+
 import fdroidserver.metadata
 from .asynchronousfilereader import AsynchronousFileReader
 
@@ -1690,14 +1692,7 @@ def get_file_extension(filename):
     return os.path.splitext(filename)[1].lower()[1:]
 
 
-def isApkAndDebuggable(apkfile, config):
-    """Returns True if the given file is an APK and is debuggable
-
-    :param apkfile: full path to the apk to check"""
-
-    if get_file_extension(apkfile) != 'apk':
-        return False
-
+def get_apk_debuggable_aapt(apkfile):
     p = SdkToolsPopen(['aapt', 'dump', 'xmltree', apkfile, 'AndroidManifest.xml'],
                       output=False)
     if p.returncode != 0:
@@ -1709,6 +1704,35 @@ def isApkAndDebuggable(apkfile, config):
     return False
 
 
+def get_apk_debuggable_androguard(apkfile):
+    try:
+        from androguard.core.bytecodes.apk import APK
+    except ImportError:
+        logging.critical("androguard library is not installed and aapt not present")
+        sys.exit(1)
+
+    apkobject = APK(apkfile)
+    if apkobject.is_valid_APK():
+        debuggable = apkobject.get_element("application", "debuggable")
+        if debuggable is not None:
+            return bool(strtobool(debuggable))
+    return False
+
+
+def isApkAndDebuggable(apkfile, config):
+    """Returns True if the given file is an APK and is debuggable
+
+    :param apkfile: full path to the apk to check"""
+
+    if get_file_extension(apkfile) != 'apk':
+        return False
+
+    if set_command_in_config('aapt'):
+        return get_apk_debuggable_aapt(apkfile)
+    else:
+        return get_apk_debuggable_androguard(apkfile)
+
+
 class PopenResult:
     def __init__(self):
         self.returncode = None
index f89513d62d3336c3fcaa661aae7ad16dd853cce0..1cb2fc4e9acf0a5af22a6853904e404b361bc6cd 100644 (file)
@@ -41,7 +41,7 @@ from . import btlog
 from . import common
 from . import index
 from . import metadata
-from .common import SdkToolsPopen
+from .common import BuildException, SdkToolsPopen
 
 METADATA_VERSION = 18
 
@@ -60,6 +60,17 @@ APK_PERMISSION_PAT = \
 APK_FEATURE_PAT = re.compile(".*name='([^']*)'.*")
 
 screen_densities = ['640', '480', '320', '240', '160', '120']
+screen_resolutions = {
+    "xxxhdpi": '640',
+    "xxhdpi": '480',
+    "xhdpi": '320',
+    "hdpi": '240',
+    "mdpi": '160',
+    "ldpi": '120',
+    "undefined": '-1',
+    "anydpi": '65534',
+    "nodpi": '65535'
+}
 
 all_screen_densities = ['0'] + screen_densities
 
@@ -871,6 +882,196 @@ def scan_repo_files(apkcache, repodir, knownapks, use_date_from_file=False):
     return repo_files, cachechanged
 
 
+def scan_apk_aapt(apk, apkfile):
+    p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
+    if p.returncode != 0:
+        if options.delete_unknown:
+            if os.path.exists(apkfile):
+                logging.error("Failed to get apk information, deleting " + apkfile)
+                os.remove(apkfile)
+            else:
+                logging.error("Could not find {0} to remove it".format(apkfile))
+        else:
+            logging.error("Failed to get apk information, skipping " + apkfile)
+        raise BuildException("Invaild APK")
+    for line in p.output.splitlines():
+        if line.startswith("package:"):
+            try:
+                apk['packageName'] = re.match(APK_NAME_PAT, line).group(1)
+                apk['versionCode'] = int(re.match(APK_VERCODE_PAT, line).group(1))
+                apk['versionName'] = re.match(APK_VERNAME_PAT, line).group(1)
+            except Exception as e:
+                logging.error("Package matching failed: " + str(e))
+                logging.info("Line was: " + line)
+                sys.exit(1)
+        elif line.startswith("application:"):
+            apk['name'] = re.match(APK_LABEL_PAT, line).group(1)
+            # Keep path to non-dpi icon in case we need it
+            match = re.match(APK_ICON_PAT_NODPI, line)
+            if match:
+                apk['icons_src']['-1'] = match.group(1)
+        elif line.startswith("launchable-activity:"):
+            # Only use launchable-activity as fallback to application
+            if not apk['name']:
+                apk['name'] = re.match(APK_LABEL_PAT, line).group(1)
+            if '-1' not in apk['icons_src']:
+                match = re.match(APK_ICON_PAT_NODPI, line)
+                if match:
+                    apk['icons_src']['-1'] = match.group(1)
+        elif line.startswith("application-icon-"):
+            match = re.match(APK_ICON_PAT, line)
+            if match:
+                density = match.group(1)
+                path = match.group(2)
+                apk['icons_src'][density] = path
+        elif line.startswith("sdkVersion:"):
+            m = re.match(APK_SDK_VERSION_PAT, line)
+            if m is None:
+                logging.error(line.replace('sdkVersion:', '')
+                              + ' is not a valid minSdkVersion!')
+            else:
+                apk['minSdkVersion'] = m.group(1)
+                # if target not set, default to min
+                if 'targetSdkVersion' not in apk:
+                    apk['targetSdkVersion'] = m.group(1)
+        elif line.startswith("targetSdkVersion:"):
+            m = re.match(APK_SDK_VERSION_PAT, line)
+            if m is None:
+                logging.error(line.replace('targetSdkVersion:', '')
+                              + ' is not a valid targetSdkVersion!')
+            else:
+                apk['targetSdkVersion'] = m.group(1)
+        elif line.startswith("maxSdkVersion:"):
+            apk['maxSdkVersion'] = re.match(APK_SDK_VERSION_PAT, line).group(1)
+        elif line.startswith("native-code:"):
+            apk['nativecode'] = []
+            for arch in line[13:].split(' '):
+                apk['nativecode'].append(arch[1:-1])
+        elif line.startswith('uses-permission:'):
+            perm_match = re.match(APK_PERMISSION_PAT, line).groupdict()
+            if perm_match['maxSdkVersion']:
+                perm_match['maxSdkVersion'] = int(perm_match['maxSdkVersion'])
+            permission = UsesPermission(
+                perm_match['name'],
+                perm_match['maxSdkVersion']
+            )
+
+            apk['uses-permission'].append(permission)
+        elif line.startswith('uses-permission-sdk-23:'):
+            perm_match = re.match(APK_PERMISSION_PAT, line).groupdict()
+            if perm_match['maxSdkVersion']:
+                perm_match['maxSdkVersion'] = int(perm_match['maxSdkVersion'])
+            permission_sdk_23 = UsesPermissionSdk23(
+                perm_match['name'],
+                perm_match['maxSdkVersion']
+            )
+
+            apk['uses-permission-sdk-23'].append(permission_sdk_23)
+
+        elif line.startswith('uses-feature:'):
+            feature = re.match(APK_FEATURE_PAT, line).group(1)
+            # Filter out this, it's only added with the latest SDK tools and
+            # causes problems for lots of apps.
+            if feature != "android.hardware.screen.portrait" \
+                    and feature != "android.hardware.screen.landscape":
+                if feature.startswith("android.feature."):
+                    feature = feature[16:]
+                apk['features'].add(feature)
+
+
+def scan_apk_androguard(apk, apkfile):
+    try:
+        from androguard.core.bytecodes.apk import APK
+        apkobject = APK(apkfile)
+        if apkobject.is_valid_APK():
+            arsc = apkobject.get_android_resources()
+        else:
+            if options.delete_unknown:
+                if os.path.exists(apkfile):
+                    logging.error("Failed to get apk information, deleting " + apkfile)
+                    os.remove(apkfile)
+                else:
+                    logging.error("Could not find {0} to remove it".format(apkfile))
+            else:
+                logging.error("Failed to get apk information, skipping " + apkfile)
+            raise BuildException("Invaild APK")
+    except ImportError:
+        logging.critical("androguard library is not installed and aapt not present")
+        sys.exit(1)
+    except FileNotFoundError:
+        logging.error("Could not open apk file for analysis")
+        raise BuildException("Invaild APK")
+
+    apk['packageName'] = apkobject.get_package()
+    apk['versionCode'] = int(apkobject.get_androidversion_code())
+    apk['versionName'] = apkobject.get_androidversion_name()
+    if apk['versionName'][0] == "@":
+        version_id = int(apk['versionName'].replace("@", "0x"), 16)
+        version_id = arsc.get_id(apk['packageName'], version_id)[1]
+        apk['versionName'] = arsc.get_string(apk['packageName'], version_id)[1]
+    apk['name'] = apkobject.get_app_name()
+
+    if apkobject.get_max_sdk_version() is not None:
+        apk['maxSdkVersion'] = apkobject.get_max_sdk_version()
+    apk['minSdkVersion'] = apkobject.get_min_sdk_version()
+    apk['targetSdkVersion'] = apkobject.get_target_sdk_version()
+
+    icon_id = int(apkobject.get_element("application", "icon").replace("@", "0x"), 16)
+    icon_name = arsc.get_id(apk['packageName'], icon_id)[1]
+
+    density_re = re.compile("^res/(.*)/" + icon_name + ".*$")
+
+    for file in apkobject.get_files():
+        d_re = density_re.match(file)
+        if d_re:
+            folder = d_re.group(1).split('-')
+            if len(folder) > 1:
+                resolution = folder[1]
+            else:
+                resolution = 'mdpi'
+            density = screen_resolutions[resolution]
+            apk['icons_src'][density] = d_re.group(0)
+
+    if apk['icons_src'].get('-1') is None:
+        apk['icons_src']['-1'] = apk['icons_src']['160']
+
+    arch_re = re.compile("^lib/(.*)/.*$")
+    arch = set([arch_re.match(file).group(1) for file in apkobject.get_files() if arch_re.match(file)])
+    if len(arch) >= 1:
+        apk['nativecode'] = []
+        apk['nativecode'].extend(sorted(list(arch)))
+
+    xml = apkobject.get_android_manifest_xml()
+
+    for item in xml.getElementsByTagName('uses-permission'):
+        name = str(item.getAttribute("android:name"))
+        maxSdkVersion = item.getAttribute("android:maxSdkVersion")
+        maxSdkVersion = None if maxSdkVersion is '' else int(maxSdkVersion)
+        permission = UsesPermission(
+            name,
+            maxSdkVersion
+        )
+        apk['uses-permission'].append(permission)
+
+    for item in xml.getElementsByTagName('uses-permission-sdk-23'):
+        name = str(item.getAttribute("android:name"))
+        maxSdkVersion = item.getAttribute("android:maxSdkVersion")
+        maxSdkVersion = None if maxSdkVersion is '' else int(maxSdkVersion)
+        permission_sdk_23 = UsesPermissionSdk23(
+            name,
+            maxSdkVersion
+        )
+        apk['uses-permission-sdk-23'].append(permission_sdk_23)
+
+    for item in xml.getElementsByTagName('uses-feature'):
+        feature = str(item.getAttribute("android:name"))
+        if feature != "android.hardware.screen.portrait" \
+                and feature != "android.hardware.screen.landscape":
+            if feature.startswith("android.feature."):
+                feature = feature[16:]
+        apk['features'].append(feature)
+
+
 def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk):
     """Scan the apk with the given filename in the given repo directory.
 
@@ -888,7 +1089,7 @@ def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk):
 
     if ' ' in apkfilename:
         logging.critical("Spaces in filenames are not allowed.")
-        sys.exit(1)
+        return True, None, False
 
     apkfile = os.path.join(repodir, apkfilename)
     shasum = sha256sum(apkfile)
@@ -921,100 +1122,16 @@ def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk):
         apk['antiFeatures'] = set()
         if has_old_openssl(apkfile):
             apk['antiFeatures'].add('KnownVuln')
-        p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
-        if p.returncode != 0:
-            if options.delete_unknown:
-                if os.path.exists(apkfile):
-                    logging.error("Failed to get apk information, deleting " + apkfile)
-                    os.remove(apkfile)
-                else:
-                    logging.error("Could not find {0} to remove it".format(apkfile))
+
+        try:
+            if common.set_command_in_config('aapt'):
+                logging.warning("Using AAPT for metadata")
+                scan_apk_aapt(apk, apkfile)
             else:
-                logging.error("Failed to get apk information, skipping " + apkfile)
+                logging.warning("Using androguard for metadata")
+                scan_apk_androguard(apk, apkfile)
+        except BuildException:
             return True, None, False
-        for line in p.output.splitlines():
-            if line.startswith("package:"):
-                try:
-                    apk['packageName'] = re.match(APK_NAME_PAT, line).group(1)
-                    apk['versionCode'] = int(re.match(APK_VERCODE_PAT, line).group(1))
-                    apk['versionName'] = re.match(APK_VERNAME_PAT, line).group(1)
-                except Exception as e:
-                    logging.error("Package matching failed: " + str(e))
-                    logging.info("Line was: " + line)
-                    sys.exit(1)
-            elif line.startswith("application:"):
-                apk['name'] = re.match(APK_LABEL_PAT, line).group(1)
-                # Keep path to non-dpi icon in case we need it
-                match = re.match(APK_ICON_PAT_NODPI, line)
-                if match:
-                    apk['icons_src']['-1'] = match.group(1)
-            elif line.startswith("launchable-activity:"):
-                # Only use launchable-activity as fallback to application
-                if not apk['name']:
-                    apk['name'] = re.match(APK_LABEL_PAT, line).group(1)
-                if '-1' not in apk['icons_src']:
-                    match = re.match(APK_ICON_PAT_NODPI, line)
-                    if match:
-                        apk['icons_src']['-1'] = match.group(1)
-            elif line.startswith("application-icon-"):
-                match = re.match(APK_ICON_PAT, line)
-                if match:
-                    density = match.group(1)
-                    path = match.group(2)
-                    apk['icons_src'][density] = path
-            elif line.startswith("sdkVersion:"):
-                m = re.match(APK_SDK_VERSION_PAT, line)
-                if m is None:
-                    logging.error(line.replace('sdkVersion:', '')
-                                  + ' is not a valid minSdkVersion!')
-                else:
-                    apk['minSdkVersion'] = m.group(1)
-                    # if target not set, default to min
-                    if 'targetSdkVersion' not in apk:
-                        apk['targetSdkVersion'] = m.group(1)
-            elif line.startswith("targetSdkVersion:"):
-                m = re.match(APK_SDK_VERSION_PAT, line)
-                if m is None:
-                    logging.error(line.replace('targetSdkVersion:', '')
-                                  + ' is not a valid targetSdkVersion!')
-                else:
-                    apk['targetSdkVersion'] = m.group(1)
-            elif line.startswith("maxSdkVersion:"):
-                apk['maxSdkVersion'] = re.match(APK_SDK_VERSION_PAT, line).group(1)
-            elif line.startswith("native-code:"):
-                apk['nativecode'] = []
-                for arch in line[13:].split(' '):
-                    apk['nativecode'].append(arch[1:-1])
-            elif line.startswith('uses-permission:'):
-                perm_match = re.match(APK_PERMISSION_PAT, line).groupdict()
-                if perm_match['maxSdkVersion']:
-                    perm_match['maxSdkVersion'] = int(perm_match['maxSdkVersion'])
-                permission = UsesPermission(
-                    perm_match['name'],
-                    perm_match['maxSdkVersion']
-                )
-
-                apk['uses-permission'].append(permission)
-            elif line.startswith('uses-permission-sdk-23:'):
-                perm_match = re.match(APK_PERMISSION_PAT, line).groupdict()
-                if perm_match['maxSdkVersion']:
-                    perm_match['maxSdkVersion'] = int(perm_match['maxSdkVersion'])
-                permission_sdk_23 = UsesPermissionSdk23(
-                    perm_match['name'],
-                    perm_match['maxSdkVersion']
-                )
-
-                apk['uses-permission-sdk-23'].append(permission_sdk_23)
-
-            elif line.startswith('uses-feature:'):
-                feature = re.match(APK_FEATURE_PAT, line).group(1)
-                # Filter out this, it's only added with the latest SDK tools and
-                # causes problems for lots of apps.
-                if feature != "android.hardware.screen.portrait" \
-                        and feature != "android.hardware.screen.landscape":
-                    if feature.startswith("android.feature."):
-                        feature = feature[16:]
-                    apk['features'].add(feature)
 
         if 'minSdkVersion' not in apk:
             logging.warn("No SDK version information found in {0}".format(apkfile))
@@ -1029,7 +1146,7 @@ def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk):
         apk['sig'] = getsig(os.path.join(os.getcwd(), apkfile))
         if not apk['sig']:
             logging.critical("Failed to get apk signature")
-            sys.exit(1)
+            return True, None, False
 
         apkzip = zipfile.ZipFile(apkfile, 'r')
 
@@ -1068,10 +1185,8 @@ def scan_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk):
                 with open(icondest, 'wb') as f:
                     f.write(get_icon_bytes(apkzip, iconsrc))
                 apk['icons'][density] = iconfilename
-
-            except Exception as e:
-                logging.warn("Error retrieving icon file: %s" % (e))
-                del apk['icons'][density]
+            except (zipfile.BadZipFile, ValueError, KeyError) as e:
+                logging.warning("Error retrieving icon file: %s" % (icondest))
                 del apk['icons_src'][density]
                 empty_densities.append(density)
 
diff --git a/tests/androguard_test.py b/tests/androguard_test.py
new file mode 100644 (file)
index 0000000..9e5d845
--- /dev/null
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+
+import inspect
+import logging
+import optparse
+import os
+import shutil
+import sys
+import tempfile
+import unittest
+import yaml
+from binascii import unhexlify
+
+localmodule = os.path.realpath(
+    os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..'))
+print('localmodule: ' + localmodule)
+if localmodule not in sys.path:
+    sys.path.insert(0, localmodule)
+
+import fdroidserver.common
+import fdroidserver.metadata
+import fdroidserver.update
+
+
+class UpdateTest(unittest.TestCase):
+    '''fdroid androguard manual tests'''
+
+    def testScanMetadataAndroguardAAPT(self):
+
+        def _create_apkmetadata_object(apkName):
+            '''Create an empty apk metadata object'''
+            apk = {}
+            apk['apkName'] = apkName
+            apk['uses-permission'] = []
+            apk['uses-permission-sdk-23'] = []
+            apk['features'] = []
+            apk['icons_src'] = {}
+            return apk
+        
+        config = dict()
+        fdroidserver.common.fill_config_defaults(config)
+        fdroidserver.update.config = config
+        os.chdir(os.path.dirname(__file__))
+        if os.path.basename(os.getcwd()) != 'tests':
+            raise Exception('This test must be run in the "tests/" subdir')
+
+        config['ndk_paths'] = dict()
+        config['accepted_formats'] = ['json', 'txt', 'yml']
+        fdroidserver.common.config = config
+        fdroidserver.update.config = config
+
+        fdroidserver.update.options = type('', (), {})()
+        fdroidserver.update.options.clean = True
+        fdroidserver.update.options.delete_unknown = True
+
+        self.assertTrue(fdroidserver.common.set_command_in_config('aapt'))
+        try:
+            from androguard.core.bytecodes.apk import APK
+        except ImportError:
+            raise Exception("androguard not installed!")
+
+        apkList = ['../info.guardianproject.urzip.apk', '../org.dyndns.fules.ck_20.apk']
+
+        for apkName in apkList:
+            logging.debug("Processing " + apkName)
+            apkfile = os.path.join('repo', apkName)
+
+            apkaapt = _create_apkmetadata_object(apkName)
+            logging.debug("Using AAPT for metadata")
+            fdroidserver.update.scan_apk_aapt(apkaapt, apkfile)
+            # avoid AAPT application name bug
+            del apkaapt['name']
+
+            apkandroguard = _create_apkmetadata_object(apkName)
+            logging.debug("Using androguard for metadata")
+            fdroidserver.update.scan_apk_androguard(apkandroguard, apkfile)
+            # avoid AAPT application name bug
+            del apkandroguard['name']
+
+            self.maxDiff = None
+            self.assertEqual(apkaapt, apkandroguard)
+
+
+if __name__ == "__main__":
+    parser = optparse.OptionParser()
+    parser.add_option("-v", "--verbose", action="store_true", default=False,
+                      help="Spew out even more information than normal")
+    (fdroidserver.common.options, args) = parser.parse_args(['--verbose'])
+
+    newSuite = unittest.TestSuite()
+    newSuite.addTest(unittest.makeSuite(UpdateTest))
+    unittest.main()
\ No newline at end of file
diff --git a/tests/metadata/apk/info.guardianproject.urzip.yaml b/tests/metadata/apk/info.guardianproject.urzip.yaml
new file mode 100644 (file)
index 0000000..2234cf1
--- /dev/null
@@ -0,0 +1,20 @@
+antiFeatures: !!set {}
+features: []
+hash: abfb3adb7496611749e7abfb014c5c789e3a02489e48a5c3665110d1b1acd931
+hashType: sha256
+icon: info.guardianproject.urzip.100.png
+icons:
+  '0': info.guardianproject.urzip.100.png
+  '160': info.guardianproject.urzip.100.png
+icons_src:
+  '-1': res/drawable/ic_launcher.png
+  '160': res/drawable/ic_launcher.png
+minSdkVersion: '4'
+packageName: info.guardianproject.urzip
+sig: e0ecb5fc2d63088e4a07ae410a127722
+size: 9969
+targetSdkVersion: '18'
+uses-permission: []
+uses-permission-sdk-23: []
+versionCode: 100
+versionName: '0.1'
diff --git a/tests/metadata/apk/org.dyndns.fules.ck.yaml b/tests/metadata/apk/org.dyndns.fules.ck.yaml
new file mode 100644 (file)
index 0000000..cbb6283
--- /dev/null
@@ -0,0 +1,41 @@
+antiFeatures: !!set {}
+features: []
+hash: 897486e1f857c6c0ee32ccbad0e1b8cd82f6d0e65a44a23f13f852d2b63a18c8
+hashType: sha256
+icon: org.dyndns.fules.ck.20.png
+icons:
+  '0': org.dyndns.fules.ck.20.png
+  '120': org.dyndns.fules.ck.20.png
+  '160': org.dyndns.fules.ck.20.png
+  '240': org.dyndns.fules.ck.20.png
+icons_src:
+  '-1': res/drawable-mdpi-v4/icon_launcher.png
+  '120': res/drawable-ldpi-v4/icon_launcher.png
+  '160': res/drawable-mdpi-v4/icon_launcher.png
+  '240': res/drawable-hdpi-v4/icon_launcher.png
+minSdkVersion: '7'
+nativecode:
+- arm64-v8a
+- armeabi
+- armeabi-v7a
+- mips
+- mips64
+- x86
+- x86_64
+packageName: org.dyndns.fules.ck
+sig: 9bf7a6a67f95688daec75eab4b1436ac
+size: 132453
+targetSdkVersion: '8'
+uses-permission:
+- !!python/object/new:fdroidserver.update.UsesPermission
+  - android.permission.BIND_INPUT_METHOD
+  - null
+- !!python/object/new:fdroidserver.update.UsesPermission
+  - android.permission.READ_EXTERNAL_STORAGE
+  - null
+- !!python/object/new:fdroidserver.update.UsesPermission
+  - android.permission.VIBRATE
+  - null
+uses-permission-sdk-23: []
+versionCode: 20
+versionName: v1.6pre2
diff --git a/tests/org.dyndns.fules.ck_20.apk b/tests/org.dyndns.fules.ck_20.apk
new file mode 100644 (file)
index 0000000..a7ccf76
Binary files /dev/null and b/tests/org.dyndns.fules.ck_20.apk differ
index 1d6dd84602475e27cb39816fa8a368648d9b2532..4e204ae6b127fc26ace8b1965183a663260e62df 100755 (executable)
@@ -224,6 +224,54 @@ class UpdateTest(unittest.TestCase):
                 self.assertIsNone(apk.get('obbMainFile'))
                 self.assertIsNone(apk.get('obbPatchFile'))
 
+    def testScanApkMetadata(self):
+
+        def _build_yaml_representer(dumper, data):
+            '''Creates a YAML representation of a Build instance'''
+            return dumper.represent_dict(data)
+
+        config = dict()
+        fdroidserver.common.fill_config_defaults(config)
+        fdroidserver.update.config = config
+        os.chdir(os.path.dirname(__file__))
+        if os.path.basename(os.getcwd()) != 'tests':
+            raise Exception('This test must be run in the "tests/" subdir')
+
+        config['ndk_paths'] = dict()
+        config['accepted_formats'] = ['json', 'txt', 'yml']
+        fdroidserver.common.config = config
+        fdroidserver.update.config = config
+
+        fdroidserver.update.options = type('', (), {})()
+        fdroidserver.update.options.clean = True
+        fdroidserver.update.options.delete_unknown = True
+
+        for icon_dir in fdroidserver.update.get_all_icon_dirs('repo'):
+            if not os.path.exists(icon_dir):
+                os.makedirs(icon_dir)
+
+        knownapks = fdroidserver.common.KnownApks()
+        apkList = ['../urzip.apk', '../org.dyndns.fules.ck_20.apk']
+
+        for apkName in apkList:
+            _, apk, cachechanged = fdroidserver.update.scan_apk({}, apkName, 'repo', knownapks, False)
+            # Don't care about the date added to the repo and relative apkName
+            del apk['added']
+            del apk['apkName']
+            # avoid AAPT application name bug
+            del apk['name']
+
+            savepath = os.path.join('metadata', 'apk', apk['packageName'] + '.yaml')
+            # Uncomment to save APK metadata
+            # with open(savepath, 'w') as f:
+            #     yaml.add_representer(fdroidserver.metadata.Build, _build_yaml_representer)
+            #     yaml.dump(apk, f, default_flow_style=False)
+
+            with open(savepath, 'r') as f:
+                frompickle = yaml.load(f)
+            self.maxDiff = None
+            self.assertEqual(apk, frompickle)
+
     def test_scan_invalid_apk(self):
         os.chdir(os.path.join(localmodule, 'tests'))
         if os.path.basename(os.getcwd()) != 'tests':