1
0
mirror of https://gitlab.com/fdroid/fdroidserver.git synced 2024-11-18 20:50:10 +01:00

verify: add --output-json option to generate status reports

This has to do custom dict/encoder/decoder tricks in order to make sure
that each run of `fdroid verify` on the same files doesn't add duplicate
entries.
This commit is contained in:
Hans-Christoph Steiner 2018-12-07 13:13:10 +01:00
parent e85256e248
commit 13016c5d63

View File

@ -19,19 +19,108 @@
import sys import sys
import os import os
import glob import glob
import json
import logging
import requests import requests
from argparse import ArgumentParser from argparse import ArgumentParser
import logging from collections import OrderedDict
from . import _ from . import _
from . import common from . import common
from . import net from . import net
from . import update
from .exception import FDroidException from .exception import FDroidException
options = None options = None
config = None config = None
class hashabledict(OrderedDict):
def __key(self):
return tuple((k, self[k]) for k in sorted(self))
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return self.__key() == other.__key()
def __lt__(self, other):
return self.__key() < other.__key()
def __qt__(self, other):
return self.__key() > other.__key()
class Decoder(json.JSONDecoder):
def __init__(self, **kwargs):
json.JSONDecoder.__init__(self, **kwargs)
self.parse_array = self.JSONArray
# Use the python implemenation of the scanner
self.scan_once = json.scanner.py_make_scanner(self)
def JSONArray(self, s_and_end, scan_once, **kwargs):
values, end = json.decoder.JSONArray(s_and_end, scan_once, **kwargs)
return set(values), end
class Encoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, set):
return sorted(obj)
return super().default(obj)
def write_json_report(url, remote_apk, unsigned_apk, compare_result):
"""write out the results of the verify run to JSON
This builds up reports on the repeated runs of `fdroid verify` on
a set of apps. It uses the timestamps on the compared files to
ensure that there is only one report per file, even when run
repeatedly.
"""
jsonfile = unsigned_apk + '.json'
if os.path.exists(jsonfile):
with open(jsonfile) as fp:
data = json.load(fp, object_pairs_hook=OrderedDict)
else:
data = OrderedDict()
output = hashabledict()
output['url'] = url
for key, filename in (('local', unsigned_apk), ('remote', remote_apk)):
d = hashabledict()
output[key] = d
d['file'] = filename
d['sha256'] = update.sha256sum(filename)
d['timestamp'] = os.stat(filename).st_ctime
d['packageName'], d['versionCode'], d['versionName'] = common.get_apk_id(filename)
if compare_result:
output['verified'] = False
output['result'] = compare_result
else:
output['verified'] = True
data[str(output['local']['timestamp'])] = output # str makes better dict keys than float
with open(jsonfile, 'w') as fp:
json.dump(data, fp, sort_keys=True)
if output['verified']:
jsonfile = 'unsigned/verified.json'
if os.path.exists(jsonfile):
with open(jsonfile) as fp:
data = json.load(fp, cls=Decoder, object_pairs_hook=hashabledict)
else:
data = OrderedDict()
data['packages'] = OrderedDict()
packageName = output['local']['packageName']
if packageName not in data['packages']:
data['packages'][packageName] = set()
data['packages'][packageName].add(output)
with open(jsonfile, 'w') as fp:
json.dump(data, fp, cls=Encoder, sort_keys=True)
def main(): def main():
global options, config global options, config
@ -42,6 +131,8 @@ def main():
parser.add_argument("appid", nargs='*', help=_("applicationId with optional versionCode in the form APPID[:VERCODE]")) parser.add_argument("appid", nargs='*', help=_("applicationId with optional versionCode in the form APPID[:VERCODE]"))
parser.add_argument("--reuse-remote-apk", action="store_true", default=False, parser.add_argument("--reuse-remote-apk", action="store_true", default=False,
help=_("Verify against locally cached copy rather than redownloading.")) help=_("Verify against locally cached copy rather than redownloading."))
parser.add_argument("--output-json", action="store_true", default=False,
help=_("Output JSON report to file named after APK."))
options = parser.parse_args() options = parser.parse_args()
config = common.read_config(options) config = common.read_config(options)
@ -64,6 +155,7 @@ def main():
for apkfile in sorted(glob.glob(os.path.join(unsigned_dir, '*.apk'))): for apkfile in sorted(glob.glob(os.path.join(unsigned_dir, '*.apk'))):
apkfilename = os.path.basename(apkfile) apkfilename = os.path.basename(apkfile)
url = 'https://f-droid.org/repo/' + apkfilename
appid, vercode = common.publishednameinfo(apkfile) appid, vercode = common.publishednameinfo(apkfile)
if vercodes and appid not in vercodes: if vercodes and appid not in vercodes:
@ -79,7 +171,6 @@ def main():
if not options.reuse_remote_apk or not os.path.exists(remote_apk): if not options.reuse_remote_apk or not os.path.exists(remote_apk):
if os.path.exists(remote_apk): if os.path.exists(remote_apk):
os.remove(remote_apk) os.remove(remote_apk)
url = 'https://f-droid.org/repo/' + apkfilename
logging.info("...retrieving " + url) logging.info("...retrieving " + url)
try: try:
net.download_file(url, dldir=tmp_dir) net.download_file(url, dldir=tmp_dir)
@ -90,10 +181,10 @@ def main():
raise FDroidException(_('Downloading {url} failed. {error}') raise FDroidException(_('Downloading {url} failed. {error}')
.format(url=url, error=e)) .format(url=url, error=e))
compare_result = common.verify_apks( unsigned_apk = os.path.join(unsigned_dir, apkfilename)
remote_apk, compare_result = common.verify_apks(remote_apk, unsigned_apk, tmp_dir)
os.path.join(unsigned_dir, apkfilename), if options.output_json:
tmp_dir) write_json_report(url, remote_apk, unsigned_apk, compare_result)
if compare_result: if compare_result:
raise FDroidException(compare_result) raise FDroidException(compare_result)