mirror of
https://gitlab.com/fdroid/fdroidserver.git
synced 2024-09-11 23:23:27 +02:00
This is useful for parsing APK files, which can include packageName, versionCode, and optionally 7 char signing key ID (i.e. <sig>). This also can set the packageName and versionCoe for non APK files, so that it is easy to assign them to metadata files, and to allow for upgrades by setting the versionCode in the filename.
358 lines
15 KiB
Python
Executable File
358 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
|
||
# http://www.drdobbs.com/testing/unit-testing-with-python/240165163
|
||
|
||
import inspect
|
||
import optparse
|
||
import os
|
||
import re
|
||
import shutil
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
import textwrap
|
||
from zipfile import ZipFile
|
||
|
||
|
||
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.signindex
|
||
import fdroidserver.common
|
||
import fdroidserver.metadata
|
||
|
||
|
||
class CommonTest(unittest.TestCase):
|
||
'''fdroidserver/common.py'''
|
||
|
||
def _set_build_tools(self):
|
||
build_tools = os.path.join(fdroidserver.common.config['sdk_path'], 'build-tools')
|
||
if os.path.exists(build_tools):
|
||
fdroidserver.common.config['build_tools'] = ''
|
||
for f in sorted(os.listdir(build_tools), reverse=True):
|
||
versioned = os.path.join(build_tools, f)
|
||
if os.path.isdir(versioned) \
|
||
and os.path.isfile(os.path.join(versioned, 'aapt')):
|
||
fdroidserver.common.config['build_tools'] = versioned
|
||
break
|
||
return True
|
||
else:
|
||
print('no build-tools found: ' + build_tools)
|
||
return False
|
||
|
||
def _find_all(self):
|
||
for cmd in ('aapt', 'adb', 'android', 'zipalign'):
|
||
path = fdroidserver.common.find_sdk_tools_cmd(cmd)
|
||
if path is not None:
|
||
self.assertTrue(os.path.exists(path))
|
||
self.assertTrue(os.path.isfile(path))
|
||
|
||
def test_find_sdk_tools_cmd(self):
|
||
fdroidserver.common.config = dict()
|
||
# TODO add this once everything works without sdk_path set in config
|
||
# self._find_all()
|
||
sdk_path = os.getenv('ANDROID_HOME')
|
||
if os.path.exists(sdk_path):
|
||
fdroidserver.common.config['sdk_path'] = sdk_path
|
||
if os.path.exists('/usr/bin/aapt'):
|
||
# this test only works when /usr/bin/aapt is installed
|
||
self._find_all()
|
||
build_tools = os.path.join(sdk_path, 'build-tools')
|
||
if self._set_build_tools():
|
||
self._find_all()
|
||
else:
|
||
print('no build-tools found: ' + build_tools)
|
||
|
||
def testIsApkDebuggable(self):
|
||
config = dict()
|
||
fdroidserver.common.fill_config_defaults(config)
|
||
fdroidserver.common.config = config
|
||
self._set_build_tools()
|
||
config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt')
|
||
# these are set debuggable
|
||
testfiles = []
|
||
testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip.apk'))
|
||
testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip-badsig.apk'))
|
||
testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip-badcert.apk'))
|
||
for apkfile in testfiles:
|
||
debuggable = fdroidserver.common.isApkAndDebuggable(apkfile)
|
||
self.assertTrue(debuggable,
|
||
"debuggable APK state was not properly parsed!")
|
||
# these are set NOT debuggable
|
||
testfiles = []
|
||
testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip-release.apk'))
|
||
testfiles.append(os.path.join(os.path.dirname(__file__), 'urzip-release-unsigned.apk'))
|
||
for apkfile in testfiles:
|
||
debuggable = fdroidserver.common.isApkAndDebuggable(apkfile)
|
||
self.assertFalse(debuggable,
|
||
"debuggable APK state was not properly parsed!")
|
||
|
||
def testPackageNameValidity(self):
|
||
for name in ["org.fdroid.fdroid",
|
||
"org.f_droid.fdr0ID"]:
|
||
self.assertTrue(fdroidserver.common.is_valid_package_name(name),
|
||
"{0} should be a valid package name".format(name))
|
||
for name in ["0rg.fdroid.fdroid",
|
||
".f_droid.fdr0ID",
|
||
"org.fdroid/fdroid",
|
||
"/org.fdroid.fdroid"]:
|
||
self.assertFalse(fdroidserver.common.is_valid_package_name(name),
|
||
"{0} should not be a valid package name".format(name))
|
||
|
||
def test_prepare_sources(self):
|
||
testint = 99999999
|
||
teststr = 'FAKE_STR_FOR_TESTING'
|
||
|
||
tmpdir = os.path.join(os.path.dirname(__file__), '..', '.testfiles')
|
||
if not os.path.exists(tmpdir):
|
||
os.makedirs(tmpdir)
|
||
tmptestsdir = tempfile.mkdtemp(prefix='test_prepare_sources', dir=tmpdir)
|
||
shutil.copytree(os.path.join(os.path.dirname(__file__), 'source-files'),
|
||
os.path.join(tmptestsdir, 'source-files'))
|
||
|
||
testdir = os.path.join(tmptestsdir, 'source-files', 'fdroid', 'fdroidclient')
|
||
|
||
config = dict()
|
||
config['sdk_path'] = os.getenv('ANDROID_HOME')
|
||
config['ndk_paths'] = {'r10d': os.getenv('ANDROID_NDK_HOME')}
|
||
config['build_tools'] = 'FAKE_BUILD_TOOLS_VERSION'
|
||
fdroidserver.common.config = config
|
||
app = fdroidserver.metadata.App()
|
||
app.id = 'org.fdroid.froid'
|
||
build = fdroidserver.metadata.Build()
|
||
build.commit = 'master'
|
||
build.forceversion = True
|
||
build.forcevercode = True
|
||
build.gradle = ['yes']
|
||
build.target = 'android-' + str(testint)
|
||
build.versionName = teststr
|
||
build.versionCode = testint
|
||
|
||
class FakeVcs():
|
||
# no need to change to the correct commit here
|
||
def gotorevision(self, rev, refresh=True):
|
||
pass
|
||
|
||
# no srclib info needed, but it could be added...
|
||
def getsrclib(self):
|
||
return None
|
||
|
||
fdroidserver.common.prepare_source(FakeVcs(), app, build, testdir, testdir, testdir)
|
||
|
||
with open(os.path.join(testdir, 'build.gradle'), 'r') as f:
|
||
filedata = f.read()
|
||
self.assertIsNotNone(re.search("\s+compileSdkVersion %s\s+" % testint, filedata))
|
||
|
||
with open(os.path.join(testdir, 'AndroidManifest.xml')) as f:
|
||
filedata = f.read()
|
||
self.assertIsNone(re.search('android:debuggable', filedata))
|
||
self.assertIsNotNone(re.search('android:versionName="%s"' % build.versionName, filedata))
|
||
self.assertIsNotNone(re.search('android:versionCode="%s"' % build.versionCode, filedata))
|
||
|
||
def test_fdroid_popen_stderr_redirect(self):
|
||
commands = ['sh', '-c', 'echo stdout message && echo stderr message 1>&2']
|
||
|
||
p = fdroidserver.common.FDroidPopen(commands)
|
||
self.assertEqual(p.output, 'stdout message\nstderr message\n')
|
||
|
||
p = fdroidserver.common.FDroidPopen(commands, stderr_to_stdout=False)
|
||
self.assertEqual(p.output, 'stdout message\n')
|
||
|
||
def test_signjar(self):
|
||
fdroidserver.common.config = None
|
||
config = fdroidserver.common.read_config(fdroidserver.common.options)
|
||
config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
|
||
fdroidserver.common.config = config
|
||
fdroidserver.signindex.config = config
|
||
|
||
basedir = os.path.dirname(__file__)
|
||
tmpdir = os.path.join(basedir, '..', '.testfiles')
|
||
if not os.path.exists(tmpdir):
|
||
os.makedirs(tmpdir)
|
||
sourcedir = os.path.join(basedir, 'signindex')
|
||
testsdir = tempfile.mkdtemp(prefix='test_signjar', dir=tmpdir)
|
||
for f in ('testy.jar', 'guardianproject.jar',):
|
||
sourcefile = os.path.join(sourcedir, f)
|
||
testfile = os.path.join(testsdir, f)
|
||
shutil.copy(sourcefile, testsdir)
|
||
fdroidserver.signindex.sign_jar(testfile)
|
||
# these should be resigned, and therefore different
|
||
self.assertNotEqual(open(sourcefile, 'rb').read(), open(testfile, 'rb').read())
|
||
|
||
def test_verify_apk_signature(self):
|
||
fdroidserver.common.config = None
|
||
config = fdroidserver.common.read_config(fdroidserver.common.options)
|
||
config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
|
||
fdroidserver.common.config = config
|
||
|
||
self.assertTrue(fdroidserver.common.verify_apk_signature('urzip.apk'))
|
||
self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-badcert.apk'))
|
||
self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-badsig.apk'))
|
||
self.assertTrue(fdroidserver.common.verify_apk_signature('urzip-release.apk'))
|
||
self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-release-unsigned.apk'))
|
||
|
||
def test_verify_apks(self):
|
||
fdroidserver.common.config = None
|
||
config = fdroidserver.common.read_config(fdroidserver.common.options)
|
||
config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
|
||
fdroidserver.common.config = config
|
||
|
||
basedir = os.path.dirname(__file__)
|
||
sourceapk = os.path.join(basedir, 'urzip.apk')
|
||
|
||
tmpdir = os.path.join(basedir, '..', '.testfiles')
|
||
if not os.path.exists(tmpdir):
|
||
os.makedirs(tmpdir)
|
||
testdir = tempfile.mkdtemp(prefix='test_verify_apks', dir=tmpdir)
|
||
print('testdir', testdir)
|
||
|
||
copyapk = os.path.join(testdir, 'urzip-copy.apk')
|
||
shutil.copy(sourceapk, copyapk)
|
||
self.assertTrue(fdroidserver.common.verify_apk_signature(copyapk))
|
||
self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, copyapk, tmpdir))
|
||
|
||
unsignedapk = os.path.join(testdir, 'urzip-unsigned.apk')
|
||
with ZipFile(sourceapk, 'r') as apk:
|
||
with ZipFile(unsignedapk, 'w') as testapk:
|
||
for info in apk.infolist():
|
||
if not info.filename.startswith('META-INF/'):
|
||
testapk.writestr(info, apk.read(info.filename))
|
||
self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, unsignedapk, tmpdir))
|
||
|
||
twosigapk = os.path.join(testdir, 'urzip-twosig.apk')
|
||
otherapk = ZipFile(os.path.join(basedir, 'urzip-release.apk'), 'r')
|
||
with ZipFile(sourceapk, 'r') as apk:
|
||
with ZipFile(twosigapk, 'w') as testapk:
|
||
for info in apk.infolist():
|
||
testapk.writestr(info, apk.read(info.filename))
|
||
if info.filename.startswith('META-INF/'):
|
||
testapk.writestr(info, otherapk.read(info.filename))
|
||
otherapk.close()
|
||
self.assertFalse(fdroidserver.common.verify_apk_signature(twosigapk))
|
||
self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, twosigapk, tmpdir))
|
||
|
||
def test_write_to_config(self):
|
||
with tempfile.TemporaryDirectory() as tmpPath:
|
||
cfgPath = os.path.join(tmpPath, 'config.py')
|
||
with open(cfgPath, 'w', encoding='utf-8') as f:
|
||
f.write(textwrap.dedent("""\
|
||
# abc
|
||
# test = 'example value'
|
||
default_me= '%%%'
|
||
|
||
# comment
|
||
do_not_touch = "good value"
|
||
default_me="!!!"
|
||
|
||
key="123" # inline"""))
|
||
|
||
cfg = {'key': '111', 'default_me_orig': 'orig'}
|
||
fdroidserver.common.write_to_config(cfg, 'key', config_file=cfgPath)
|
||
fdroidserver.common.write_to_config(cfg, 'default_me', config_file=cfgPath)
|
||
fdroidserver.common.write_to_config(cfg, 'test', value='test value', config_file=cfgPath)
|
||
fdroidserver.common.write_to_config(cfg, 'new_key', value='new', config_file=cfgPath)
|
||
|
||
with open(cfgPath, 'r', encoding='utf-8') as f:
|
||
self.assertEqual(f.read(), textwrap.dedent("""\
|
||
# abc
|
||
test = 'test value'
|
||
default_me = 'orig'
|
||
|
||
# comment
|
||
do_not_touch = "good value"
|
||
|
||
key = "111" # inline
|
||
|
||
new_key = "new"
|
||
"""))
|
||
|
||
def test_write_to_config_when_empty(self):
|
||
with tempfile.TemporaryDirectory() as tmpPath:
|
||
cfgPath = os.path.join(tmpPath, 'config.py')
|
||
with open(cfgPath, 'w') as f:
|
||
pass
|
||
fdroidserver.common.write_to_config({}, 'key', 'val', cfgPath)
|
||
with open(cfgPath, 'r', encoding='utf-8') as f:
|
||
self.assertEqual(f.read(), textwrap.dedent("""\
|
||
|
||
key = "val"
|
||
"""))
|
||
|
||
def test_apk_name_regex(self):
|
||
good = [
|
||
'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_-123456.apk',
|
||
'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_123456_abcdef0.apk',
|
||
'urzip_-123456.apk',
|
||
'a0_0.apk',
|
||
'Z0_0.apk',
|
||
'a0_0_abcdef0.apk',
|
||
'a_a_a_a_0_abcdef0.apk',
|
||
'a_____0.apk',
|
||
'a_____123456_abcdef0.apk',
|
||
'org.fdroid.fdroid_123456.apk',
|
||
# valid, but "_99999" is part of packageName rather than versionCode
|
||
'org.fdroid.fdroid_99999_123456.apk',
|
||
# should be valid, but I can't figure out the regex since \w includes digits
|
||
# 'πÇÇπÇÇ现代汉语通用字българскиعربي1234ö_0_123bafd.apk',
|
||
]
|
||
for name in good:
|
||
m = fdroidserver.common.APK_NAME_REGEX.match(name)
|
||
self.assertIsNotNone(m)
|
||
self.assertIn(m.group(2), ('-123456', '0', '123456'))
|
||
self.assertIn(m.group(3), ('abcdef0', None))
|
||
|
||
bad = [
|
||
'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_123456_abcdefg.apk',
|
||
'urzip-_-198274.apk',
|
||
'urzip-_0_123bafd.apk',
|
||
'no spaces allowed_123.apk',
|
||
'0_0.apk',
|
||
'0_0_abcdef0.apk',
|
||
]
|
||
for name in bad:
|
||
self.assertIsNone(fdroidserver.common.APK_NAME_REGEX.match(name))
|
||
|
||
def test_standard_file_name_regex(self):
|
||
good = [
|
||
'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_-123456.mp3',
|
||
'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_123456.mov',
|
||
'Document_-123456.pdf',
|
||
'WTF_0.MOV',
|
||
'Z0_0.ebk',
|
||
'a_a_a_a_0.txt',
|
||
'org.fdroid.fdroid.privileged.ota_123456.zip',
|
||
'πÇÇπÇÇ现代汉语通用字българскиعربي1234ö_0.jpeg',
|
||
'a_____0.PNG',
|
||
# valid, but "_99999" is part of packageName rather than versionCode
|
||
'a_____99999_123456.zip',
|
||
'org.fdroid.fdroid_99999_123456.zip',
|
||
]
|
||
for name in good:
|
||
m = fdroidserver.common.STANDARD_FILE_NAME_REGEX.match(name)
|
||
self.assertIsNotNone(m)
|
||
self.assertIn(m.group(2), ('-123456', '0', '123456'))
|
||
|
||
bad = [
|
||
'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_abcdefg.JPEG',
|
||
'urzip-_-198274.zip',
|
||
'urzip-_123bafd.pdf',
|
||
'no spaces allowed_123.foobar',
|
||
'a_____0.',
|
||
]
|
||
for name in bad:
|
||
self.assertIsNone(fdroidserver.common.STANDARD_FILE_NAME_REGEX.match(name))
|
||
|
||
|
||
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(CommonTest))
|
||
unittest.main()
|