diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f26a093a..79fd1143 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,7 +41,7 @@ metadata_v0: image: registry.gitlab.com/fdroid/fdroidserver:buildserver variables: GIT_DEPTH: 1000 - RELEASE_COMMIT_ID: 0124b9dde99f9cab19c034cbc7d8cc6005a99b48 # 2.3a0 + RELEASE_COMMIT_ID: 1c2187a53bfb7a92cb9837435ae3717aacf5920d # 2.3a0 script: - git fetch https://gitlab.com/fdroid/fdroidserver.git $RELEASE_COMMIT_ID - cd tests diff --git a/MANIFEST.in b/MANIFEST.in index e534b0da..e301d8a8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -597,7 +597,14 @@ include tests/main.TestCase include tests/metadata/apk/info.guardianproject.urzip.yaml include tests/metadata/apk/org.dyndns.fules.ck.yaml include tests/metadata/app.with.special.build.params.yml +include tests/metadata/app.with.special.build.params/en-US/antifeatures/50_Ads.txt +include tests/metadata/app.with.special.build.params/en-US/antifeatures/50_Tracking.txt +include tests/metadata/app.with.special.build.params/en-US/antifeatures/Ads.txt +include tests/metadata/app.with.special.build.params/en-US/antifeatures/NoSourceSince.txt +include tests/metadata/app.with.special.build.params/zh-CN/antifeatures/49_Tracking.txt +include tests/metadata/app.with.special.build.params/zh-CN/antifeatures/50_Ads.txt include tests/metadata/com.politedroid.yml +include tests/metadata/dump/app.with.special.build.params.yaml include tests/metadata/dump/com.politedroid.yaml include tests/metadata/dump/org.adaway.yaml include tests/metadata/dump/org.smssecure.smssecure.yaml diff --git a/fdroidserver/index.py b/fdroidserver/index.py index 48b51ffe..eb05fde6 100644 --- a/fdroidserver/index.py +++ b/fdroidserver/index.py @@ -546,6 +546,13 @@ def package_metadata(app, repodir): def convert_version(version, app, repodir): + """Convert the internal representation of Builds: into index-v2 versions. + + The diff algorithm of index-v2 uses null/None to mean a field to + be removed, so this function handles any Nones that are in the + metadata file. + + """ ver = {} if "added" in version: ver["added"] = convert_datetime(version["added"]) @@ -555,7 +562,7 @@ def convert_version(version, app, repodir): ver["file"] = { "name": "/{}".format(version["apkName"]), version["hashType"]: version["hash"], - "size": version["size"] + "size": version["size"], } ipfsCIDv1 = version.get("ipfsCIDv1") @@ -619,24 +626,14 @@ def convert_version(version, app, repodir): else: manifest[en].append({"name": perm[0]}) - antiFeatures = dict() - if "AntiFeatures" in app and app["AntiFeatures"]: - for antif in app["AntiFeatures"]: - # TODO: get reasons from fdroiddata - # ver["antiFeatures"][antif] = {"en-US": "reason"} - antiFeatures[antif] = dict() - - if "antiFeatures" in version and version["antiFeatures"]: - for antif in version["antiFeatures"]: - # TODO: get reasons from fdroiddata - # ver["antiFeatures"][antif] = {"en-US": "reason"} - antiFeatures[antif] = dict() - - if app.get("NoSourceSince"): - antiFeatures["NoSourceSince"] = dict() - + # index-v2 has only per-version antifeatures, not per package. + antiFeatures = app.get('AntiFeatures', {}) + for name, descdict in version.get('antiFeatures', dict()).items(): + antiFeatures[name] = descdict if antiFeatures: - ver["antiFeatures"] = dict(sorted(antiFeatures.items())) + ver['antiFeatures'] = { + k: dict(sorted(antiFeatures[k].items())) for k in sorted(antiFeatures) + } if "versionCode" in version: if version["versionCode"] > app["CurrentVersionCode"]: @@ -881,9 +878,8 @@ def make_v1(apps, packages, repodir, repodict, requestsdict, fdroid_signing_key_ for ikey, iname in sorted(lvalue.items()): lordered[lkey][ikey] = iname app_dict['localized'] = lordered - antiFeatures = app_dict.get('antiFeatures', []) - if apps[app_dict["packageName"]].get("NoSourceSince"): - antiFeatures.append("NoSourceSince") + # v1 uses a list of keys for Anti-Features + antiFeatures = app_dict.get('antiFeatures', dict()).keys() if antiFeatures: app_dict['antiFeatures'] = sorted(set(antiFeatures)) @@ -915,6 +911,9 @@ def make_v1(apps, packages, repodir, repodict, requestsdict, fdroid_signing_key_ continue if k in ('icon', 'icons', 'icons_src', 'ipfsCIDv1', 'name'): continue + if k == 'antiFeatures': + d[k] = sorted(v.keys()) + continue d[k] = v json_name = 'index-v1.json' @@ -1160,8 +1159,6 @@ def make_v0(apps, apks, repodir, repodict, requestsdict, fdroid_signing_key_fing antiFeatures = list(app.AntiFeatures) if 'antiFeatures' in apklist[0]: antiFeatures.extend(apklist[0]['antiFeatures']) - if app.get("NoSourceSince"): - antiFeatures.append("NoSourceSince") if antiFeatures: afout = sorted(set(antiFeatures)) addElementNonEmpty('antifeatures', ','.join(afout), doc, apel) diff --git a/fdroidserver/lint.py b/fdroidserver/lint.py index 1283409b..ba21295c 100644 --- a/fdroidserver/lint.py +++ b/fdroidserver/lint.py @@ -624,6 +624,17 @@ def check_app_field_types(app): fieldtype=v.__class__.__name__, ) ) + elif t == metadata.TYPE_STRINGMAP and not isinstance(v, dict): + yield ( + _( + "{appid}: {field} must be a '{type}', but it is a '{fieldtype}'!" + ).format( + appid=app.id, + field=field, + type='dict', + fieldtype=v.__class__.__name__, + ) + ) def check_antiFeatures(app): diff --git a/fdroidserver/metadata.py b/fdroidserver/metadata.py index 787de9a0..abef3e7a 100644 --- a/fdroidserver/metadata.py +++ b/fdroidserver/metadata.py @@ -114,7 +114,7 @@ class App(dict): super().__init__() self.Disabled = None - self.AntiFeatures = [] + self.AntiFeatures = dict() self.Provides = None self.Categories = [] self.License = 'Unknown' @@ -182,12 +182,14 @@ TYPE_SCRIPT = 5 TYPE_MULTILINE = 6 TYPE_BUILD = 7 TYPE_INT = 8 +TYPE_STRINGMAP = 9 fieldtypes = { 'Description': TYPE_MULTILINE, 'MaintainerNotes': TYPE_MULTILINE, 'Categories': TYPE_LIST, - 'AntiFeatures': TYPE_LIST, + 'AntiFeatures': TYPE_STRINGMAP, + 'RequiresRoot': TYPE_BOOL, 'AllowedAPKSigningKeys': TYPE_LIST, 'Builds': TYPE_BUILD, 'VercodeOperation': TYPE_LIST, @@ -277,7 +279,7 @@ class Build(dict): self.antcommands = [] self.postbuild = '' self.novcheck = False - self.antifeatures = [] + self.antifeatures = dict() if copydict: super().__init__(copydict) return @@ -358,7 +360,7 @@ flagtypes = { 'forceversion': TYPE_BOOL, 'forcevercode': TYPE_BOOL, 'novcheck': TYPE_BOOL, - 'antifeatures': TYPE_LIST, + 'antifeatures': TYPE_STRINGMAP, 'timeout': TYPE_INT, } @@ -649,8 +651,6 @@ def parse_metadata(metadatapath): metadatapath = Path(metadatapath) app = App() app.metadatapath = metadatapath.as_posix() - if metadatapath.stem != '.fdroid': - app.id = metadatapath.stem if metadatapath.suffix == '.yml': with metadatapath.open('r', encoding='utf-8') as mf: app.update(parse_yaml_metadata(mf)) @@ -659,6 +659,10 @@ def parse_metadata(metadatapath): _('Unknown metadata format: {path} (use: *.yml)').format(path=metadatapath) ) + if metadatapath.stem != '.fdroid': + app.id = metadatapath.stem + parse_localized_antifeatures(app) + if metadatapath.name != '.fdroid.yml' and app.Repo: build_dir = common.get_build_dir(app) metadata_in_repo = build_dir / '.fdroid.yml' @@ -770,6 +774,115 @@ def parse_yaml_metadata(mf): return yamldata +def parse_localized_antifeatures(app): + """Read in localized Anti-Features files from the filesystem. + + To support easy integration with Weblate and other translation + systems, there is a special type of metadata that can be + maintained in a Fastlane-style directory layout, where each field + is represented by a text file on directories that specified which + app it belongs to, which locale, etc. This function reads those + in and puts them into the internal dict, to be merged with any + related data that came from the metadata.yml file. + + This needs to be run after parse_yaml_metadata() since that + normalizes the data structure. Also, these values are lower + priority than what comes from the metadata file. So this should + not overwrite anything parse_yaml_metadata() puts into the App + instance. + + metadata///antifeatures/_.txt + metadata///antifeatures/.txt + + └── metadata/ + └── / + ├── en-US/ + │ └── antifeatures/ + │ ├── 123_Ads.txt -> "includes ad lib" + │ ├── 123_Tracking.txt -> "standard suspects" + │ └── NoSourceSince.txt -> "it vanished" + │ + └── zh-CN/ + └── antifeatures/ + └── 123_Ads.txt -> "包括广告图书馆" + + Gets parsed into the metadata data structure: + + AntiFeatures: + NoSourceSince: + en-US: it vanished + Builds: + - versionCode: 123 + antifeatures: + Ads: + en-US: includes ad lib + zh-CN: 包括广告图书馆 + Tracking: + en-US: standard suspects + + """ + app_dir = Path('metadata', app['id']) + if not app_dir.is_dir(): + return + af_dup_msg = _('Duplicate Anti-Feature declaration at {path} was ignored!') + + if app.get('AntiFeatures'): + app_has_AntiFeatures = True + else: + app_has_AntiFeatures = False + + has_versionCode = re.compile(r'^-?[0-9]+_.*') + has_antifeatures_from_app = set() + for build in app.get('Builds', []): + antifeatures = build.get('antifeatures') + if antifeatures: + has_antifeatures_from_app.add(build['versionCode']) + + for f in sorted(app_dir.glob('*/antifeatures/*.txt')): + path = f.as_posix() + left = path.index('/', 9) # 9 is length of "metadata/" + right = path.index('/', left + 1) + locale = path[left + 1 : right] + description = f.read_text() + if has_versionCode.match(f.stem): + i = f.stem.index('_') + versionCode = int(f.stem[:i]) + antifeature = f.stem[i + 1 :] + if versionCode in has_antifeatures_from_app: + logging.error(af_dup_msg.format(path=f)) + continue + if 'Builds' not in app: + app['Builds'] = [] + found = False + for build in app['Builds']: + # loop though builds again, there might be duplicate versionCodes + if versionCode == build['versionCode']: + found = True + if 'antifeatures' not in build: + build['antifeatures'] = dict() + if antifeature not in build['antifeatures']: + build['antifeatures'][antifeature] = dict() + build['antifeatures'][antifeature][locale] = description + if not found: + app['Builds'].append( + { + 'versionCode': versionCode, + 'antifeatures': { + antifeature: {locale: description}, + }, + } + ) + elif app_has_AntiFeatures: + logging.error(af_dup_msg.format(path=f)) + continue + else: + if 'AntiFeatures' not in app: + app['AntiFeatures'] = dict() + if f.stem not in app['AntiFeatures']: + app['AntiFeatures'][f.stem] = dict() + app['AntiFeatures'][f.stem][locale] = f.read_text() + + def _normalize_type_string(v): """Normalize any data to TYPE_STRING. @@ -787,6 +900,61 @@ def _normalize_type_string(v): return str(v) +def _normalize_type_stringmap(k, v): + """Normalize any data to TYPE_STRINGMAP. + + The internal representation of this format is a dict of dicts, + where the outer dict's keys are things like tag names of + Anti-Features, the inner dict's keys are locales, and the ultimate + values are human readable text. + + Metadata entries like AntiFeatures: can be written in many + forms, including a simple one-entry string, a list of strings, + a dict with keys and descriptions as values, or a dict with + localization. + + Returns + ------- + A dictionary with string keys, where each value is either a string + message or a dict with locale keys and string message values. + + """ + if v is None: + return dict() + if isinstance(v, str) or isinstance(v, int) or isinstance(v, float): + return {_normalize_type_string(v): dict()} + if isinstance(v, list) or isinstance(v, tuple) or isinstance(v, set): + retdict = dict() + for i in v: + if isinstance(i, dict): + # transitional format + if len(i) != 1: + _warn_or_exception( + _( + "'{value}' is not a valid {field}, should be {pattern}" + ).format(field=k, value=v, pattern='key: value') + ) + afname = _normalize_type_string(next(iter(i))) + desc = _normalize_type_string(next(iter(i.values()))) + retdict[afname] = {common.DEFAULT_LOCALE: desc} + else: + retdict[_normalize_type_string(i)] = {} + return retdict + + retdict = dict() + for af, afdict in v.items(): + key = _normalize_type_string(af) + if afdict: + if isinstance(afdict, dict): + retdict[key] = afdict + else: + retdict[key] = {common.DEFAULT_LOCALE: _normalize_type_string(afdict)} + else: + retdict[key] = dict() + + return retdict + + def post_parse_yaml_metadata(yamldata): """Convert human-readable metadata data structures into consistent data structures. @@ -808,6 +976,9 @@ def post_parse_yaml_metadata(yamldata): elif _fieldtype == TYPE_STRING: if v or v == 0: yamldata[k] = _normalize_type_string(v) + elif _fieldtype == TYPE_STRINGMAP: + if v or v == 0: # TODO probably want just `if v:` + yamldata[k] = _normalize_type_stringmap(k, v) else: if type(v) in (float, int): yamldata[k] = str(v) @@ -843,12 +1014,187 @@ def post_parse_yaml_metadata(yamldata): build_flag=k, value=v ) ) + elif _flagtype == TYPE_STRINGMAP: + if v or v == 0: + build[k] = _normalize_type_stringmap(k, v) builds.append(build) if builds: yamldata['Builds'] = sorted(builds, key=lambda build: build['versionCode']) + no_source_since = yamldata.get("NoSourceSince") + # do not overwrite the description if it is there + if no_source_since and not yamldata.get('AntiFeatures', {}).get('NoSourceSince'): + if 'AntiFeatures' not in yamldata: + yamldata['AntiFeatures'] = dict() + yamldata['AntiFeatures']['NoSourceSince'] = { + common.DEFAULT_LOCALE: no_source_since + } + + +def _format_stringmap(appid, field, stringmap, versionCode=None): + """Format TYPE_STRINGMAP taking into account localized files in the metadata dir. + + If there are any localized versions on the filesystem already, + then move them all there. Otherwise, keep them in the .yml file. + + The directory for the localized files that is named after the + field is all lower case, following the convention set by Fastlane + metadata, and used by fdroidserver. + + """ + app_dir = Path('metadata', appid) + try: + next(app_dir.glob('*/%s/*.txt' % field.lower())) + files = [] + overwrites = [] + for name, descdict in stringmap.items(): + for locale, desc in descdict.items(): + outdir = app_dir / locale / field.lower() + if versionCode: + filename = '%d_%s.txt' % (versionCode, name) + else: + filename = '%s.txt' % name + outfile = outdir / filename + files.append(str(outfile)) + if outfile.exists(): + if desc != outfile.read_text(): + overwrites.append(str(outfile)) + else: + if not outfile.parent.exists(): + outfile.parent.mkdir(parents=True) + outfile.write_text(desc) + if overwrites: + _warn_or_exception( + _( + 'Conflicting "{field}" definitions between .yml and localized files:' + ).format(field=field) + + '\n' + + '\n'.join(sorted(overwrites)) + ) + logging.warning( + _('Moving Anti-Features declarations to localized files:') + + '\n' + + '\n'.join(sorted(files)) + ) + return + except StopIteration: + pass + make_list = True + outlist = [] + for name in sorted(stringmap): + outlist.append(name) + descdict = stringmap.get(name) + if descdict and any(descdict.values()): + make_list = False + break + if make_list: + return outlist + return stringmap + + +def _del_duplicated_NoSourceSince(app): + # noqa: D403 NoSourceSince is the word. + """NoSourceSince gets auto-added to AntiFeatures, but can also be manually added.""" + key = 'NoSourceSince' + if key in app: + no_source_since = app.get(key) + af_no_source_since = app.get('AntiFeatures', dict()).get(key) + if af_no_source_since == {common.DEFAULT_LOCALE: no_source_since}: + del app['AntiFeatures'][key] + + +def _field_to_yaml(typ, value): + """Convert data to YAML 1.2 format that keeps the right TYPE_*.""" + if typ == TYPE_STRING: + return str(value) + elif typ == TYPE_INT: + return int(value) + elif typ == TYPE_MULTILINE: + if '\n' in value: + return ruamel.yaml.scalarstring.preserve_literal(str(value)) + else: + return str(value) + elif typ == TYPE_SCRIPT: + if type(value) == list: + if len(value) == 1: + return value[0] + else: + return value + else: + return value + + +def _builds_to_yaml(app): + builds = ruamel.yaml.comments.CommentedSeq() + for build in app.get('Builds', []): + if not isinstance(build, Build): + build = Build(build) + b = ruamel.yaml.comments.CommentedMap() + for field in build_flags: + if hasattr(build, field): + value = getattr(build, field) + if field == 'gradle' and value == ['off']: + value = [ + ruamel.yaml.scalarstring.SingleQuotedScalarString('off') + ] + typ = flagtype(field) + # don't check value == True for TYPE_INT as it could be 0 + if value and typ == TYPE_STRINGMAP: + v = _format_stringmap(app['id'], field, value, build['versionCode']) + if v: + b[field] = v + elif value is not None and (typ == TYPE_INT or value): + b.update({field: _field_to_yaml(typ, value)}) + + builds.append(b) + + # insert extra empty lines between build entries + for i in range(1, len(builds)): + builds.yaml_set_comment_before_after_key(i, 'bogus') + builds.ca.items[i][1][-1].value = '\n' + + return builds + + +def _app_to_yaml(app): + cm = ruamel.yaml.comments.CommentedMap() + insert_newline = False + for field in yaml_app_field_order: + if field == '\n': + # next iteration will need to insert a newline + insert_newline = True + else: + value = app.get(field) + if value or field == 'Builds': + if field == 'Builds': + if app.get('Builds'): + cm.update({field: _builds_to_yaml(app)}) + elif field == 'CurrentVersionCode': + cm[field] = _field_to_yaml(TYPE_INT, value) + elif field == 'AntiFeatures': + v = _format_stringmap(app['id'], field, value) + if v: + cm[field] = v + elif field == 'AllowedAPKSigningKeys': + value = [str(i).lower() for i in value] + if len(value) == 1: + cm[field] = _field_to_yaml(TYPE_STRING, value[0]) + else: + cm[field] = _field_to_yaml(TYPE_LIST, value) + else: + cm[field] = _field_to_yaml(fieldtype(field), value) + + if insert_newline: + # we need to prepend a newline in front of this field + insert_newline = False + # inserting empty lines is not supported so we add a + # bogus comment and over-write its value + cm.yaml_set_comment_before_after_key(field, 'bogus') + cm.ca.items[field][1][-1].value = '\n' + return cm + def write_yaml(mf, app): """Write metadata in yaml format. @@ -859,87 +1205,9 @@ def write_yaml(mf, app): active file discriptor for writing app app metadata to written to the yaml file + """ - def _field_to_yaml(typ, value): - """Convert data to YAML 1.2 format that keeps the right TYPE_*.""" - if typ == TYPE_STRING: - return str(value) - elif typ == TYPE_INT: - return int(value) - elif typ == TYPE_MULTILINE: - if '\n' in value: - return ruamel.yaml.scalarstring.preserve_literal(str(value)) - else: - return str(value) - elif typ == TYPE_SCRIPT: - if type(value) == list: - if len(value) == 1: - return value[0] - else: - return value - else: - return value - - def _app_to_yaml(app): - cm = ruamel.yaml.comments.CommentedMap() - insert_newline = False - for field in yaml_app_field_order: - if field == '\n': - # next iteration will need to insert a newline - insert_newline = True - else: - if app.get(field) or field == 'Builds': - if field == 'Builds': - if app.get('Builds'): - cm.update({field: _builds_to_yaml(app)}) - elif field == 'CurrentVersionCode': - cm.update({field: _field_to_yaml(TYPE_INT, getattr(app, field))}) - elif field == 'AllowedAPKSigningKeys': - value = getattr(app, field) - if value: - value = [str(i).lower() for i in value] - if len(value) == 1: - cm.update({field: _field_to_yaml(TYPE_STRING, value[0])}) - else: - cm.update({field: _field_to_yaml(TYPE_LIST, value)}) - else: - cm.update({field: _field_to_yaml(fieldtype(field), getattr(app, field))}) - - if insert_newline: - # we need to prepend a newline in front of this field - insert_newline = False - # inserting empty lines is not supported so we add a - # bogus comment and over-write its value - cm.yaml_set_comment_before_after_key(field, 'bogus') - cm.ca.items[field][1][-1].value = '\n' - return cm - - def _builds_to_yaml(app): - builds = ruamel.yaml.comments.CommentedSeq() - for build in app.get('Builds', []): - if not isinstance(build, Build): - build = Build(build) - b = ruamel.yaml.comments.CommentedMap() - for field in build_flags: - if hasattr(build, field): - value = getattr(build, field) - if field == 'gradle' and value == ['off']: - value = [ - ruamel.yaml.scalarstring.SingleQuotedScalarString('off') - ] - typ = flagtype(field) - # don't check value == True for TYPE_INT as it could be 0 - if value is not None and (typ == TYPE_INT or value): - b.update({field: _field_to_yaml(typ, value)}) - builds.append(b) - - # insert extra empty lines between build entries - for i in range(1, len(builds)): - builds.yaml_set_comment_before_after_key(i, 'bogus') - builds.ca.items[i][1][-1].value = '\n' - - return builds - + _del_duplicated_NoSourceSince(app) yaml_app = _app_to_yaml(app) yaml = ruamel.yaml.YAML() yaml.indent(mapping=4, sequence=4, offset=2) diff --git a/fdroidserver/rewritemeta.py b/fdroidserver/rewritemeta.py index 7413b029..5a1a6ea0 100644 --- a/fdroidserver/rewritemeta.py +++ b/fdroidserver/rewritemeta.py @@ -52,8 +52,9 @@ def remove_blank_flags_from_builds(builds): for build in builds: new = dict() for k in metadata.build_flags: - v = build[k] - if v is None or v is False or v == [] or v == '': + v = build.get(k) + # 0 is valid value, it should not be stripped + if v is None or v is False or v == '' or v == dict() or v == list(): continue new[k] = v newbuilds.append(new) @@ -98,6 +99,7 @@ def main(): print(path) continue + # TODO these should be moved to metadata.write_yaml() builds = remove_blank_flags_from_builds(app.get('Builds')) if builds: app['Builds'] = builds diff --git a/fdroidserver/scanner.py b/fdroidserver/scanner.py index daab6155..7db619d7 100644 --- a/fdroidserver/scanner.py +++ b/fdroidserver/scanner.py @@ -294,7 +294,7 @@ class ExodusSignatureDataController(SignatureDataController): "warn_code_signatures": [tracker["code_signature"]], # exodus also provides network signatures, unused atm. # "network_signatures": [tracker["network_signature"]], - "AntiFeatures": ["Tracking"], + "AntiFeatures": ["Tracking"], # TODO "license": "NonFree" # We assume all trackers in exodus # are non-free, although free # trackers like piwik, acra, diff --git a/fdroidserver/update.py b/fdroidserver/update.py index 45d66d33..398932b4 100644 --- a/fdroidserver/update.py +++ b/fdroidserver/update.py @@ -158,7 +158,7 @@ def status_update_json(apps, apks): for appid in apps: app = apps[appid] - for af in app.get('AntiFeatures', []): + for af in app.get('AntiFeatures', dict()): antiFeatures = output['antiFeatures'] # JSON camelCase if af not in antiFeatures: antiFeatures[af] = dict() @@ -351,7 +351,8 @@ def get_cache(): if not isinstance(v, dict): continue if 'antiFeatures' in v: - v['antiFeatures'] = set(v['antiFeatures']) + if not isinstance(v['antiFeatures'], dict): + v['antiFeatures'] = {k: {} for k in sorted(v['antiFeatures'])} if 'added' in v: v['added'] = datetime.fromtimestamp(v['added']) @@ -400,7 +401,7 @@ def has_known_vulnerability(filename): Janus is similar to Master Key but is perhaps easier to scan for. https://www.guardsquare.com/en/blog/new-android-vulnerability-allows-attackers-modify-apps-without-affecting-their-signatures """ - found_vuln = False + found_vuln = '' # statically load this pattern if not hasattr(has_known_vulnerability, "pattern"): @@ -431,15 +432,23 @@ def has_known_vulnerability(filename): logging.debug(_('"{path}" contains recent {name} ({version})') .format(path=filename, name=name, version=version)) else: - logging.warning(_('"{path}" contains outdated {name} ({version})') - .format(path=filename, name=name, version=version)) - found_vuln = True + msg = '"{path}" contains outdated {name} ({version})' + logging.warning( + _(msg).format(path=filename, name=name, version=version) + ) + found_vuln += msg.format( + path=filename, name=name, version=version + ) + found_vuln += '\n' break elif name == 'AndroidManifest.xml' or name == 'classes.dex' or name.endswith('.so'): if name in files_in_apk: - logging.warning(_('{apkfilename} has multiple {name} files, looks like Master Key exploit!') - .format(apkfilename=filename, name=name)) - found_vuln = True + msg = '{apkfilename} has multiple {name} files, looks like Master Key exploit!' + logging.warning( + _(msg).format(apkfilename=filename, name=name) + ) + found_vuln += msg.format(apkfilename=filename, name=name) + found_vuln += '\n' files_in_apk.add(name) return found_vuln @@ -545,7 +554,7 @@ def translate_per_build_anti_features(apps, apks): if d: afl = d.get(apk['versionCode']) if afl: - apk['antiFeatures'].update(afl) + apk['antiFeatures'].update(afl) # TODO def _get_localized_dict(app, locale): @@ -1228,7 +1237,7 @@ def scan_apk(apk_file, require_signature=True): 'features': [], 'icons_src': {}, 'icons': {}, - 'antiFeatures': set(), + 'antiFeatures': {}, } ipfsCIDv1 = common.calculate_IPFS_cid(apk_file) if ipfsCIDv1: @@ -1263,8 +1272,9 @@ def scan_apk(apk_file, require_signature=True): apk['minSdkVersion'] = 3 # aapt defaults to 3 as the min # Check for known vulnerabilities - if has_known_vulnerability(apk_file): - apk['antiFeatures'].add('KnownVuln') + hkv = has_known_vulnerability(apk_file) + if hkv: + apk['antiFeatures']['KnownVuln'] = {DEFAULT_LOCALE: hkv} return apk @@ -1545,7 +1555,7 @@ def process_apk(apkcache, apkfilename, repodir, knownapks, use_date_from_apk=Fal if repodir == 'archive' or allow_disabled_algorithms: try: common.verify_deprecated_jar_signature(apkfile) - apk['antiFeatures'].update(['KnownVuln', 'DisabledAlgorithm']) + apk['antiFeatures'].update(['KnownVuln', 'DisabledAlgorithm']) # TODO except VerificationException: skipapk = True else: @@ -1885,7 +1895,7 @@ def archive_old_apks(apps, apks, archapks, repodir, archivedir, defaultkeepversi for apk in all_app_apks: if len(keep) == keepversions: break - if 'antiFeatures' not in apk: + if 'antiFeatures' not in apk: # TODO keep.append(apk) elif 'DisabledAlgorithm' not in apk['antiFeatures'] or disabled_algorithms_allowed(): keep.append(apk) diff --git a/tests/metadata-rewrite-yml/app.with.special.build.params.yml b/tests/metadata-rewrite-yml/app.with.special.build.params.yml index d268d049..eeb174b7 100644 --- a/tests/metadata-rewrite-yml/app.with.special.build.params.yml +++ b/tests/metadata-rewrite-yml/app.with.special.build.params.yml @@ -77,6 +77,9 @@ Builds: maven: yes@.. srclibs: - FacebookSDK@sdk-version-3.0.2 + antifeatures: + Tracking: + en-US: Uses the Facebook SDK. - versionName: 2.1.1-c versionCode: 50 diff --git a/tests/metadata.TestCase b/tests/metadata.TestCase index b90abf9f..59c2bafe 100755 --- a/tests/metadata.TestCase +++ b/tests/metadata.TestCase @@ -26,6 +26,7 @@ if localmodule not in sys.path: import fdroidserver from fdroidserver import metadata from fdroidserver.exception import MetaDataException +from fdroidserver.common import DEFAULT_LOCALE def _get_mock_mf(s): @@ -203,7 +204,8 @@ class MetadataTest(unittest.TestCase): ) @mock.patch('git.Repo') - def test_read_metadata(self, git_repo): + @mock.patch('logging.error') + def test_read_metadata(self, logging_error, git_repo): """Read specified metadata files included in tests/, compare to stored output""" self.maxDiff = None @@ -216,6 +218,7 @@ class MetadataTest(unittest.TestCase): yaml = ruamel.yaml.YAML(typ='safe') apps = fdroidserver.metadata.read_metadata() for appid in ( + 'app.with.special.build.params', 'org.smssecure.smssecure', 'org.adaway', 'org.videolan.vlc', @@ -234,6 +237,10 @@ class MetadataTest(unittest.TestCase): # yaml.register_class(metadata.Build) # yaml.dump(frommeta, fp) + # errors are printed when .yml overrides localized + logging_error.assert_called() + self.assertEqual(3, len(logging_error.call_args_list)) + @mock.patch('git.Repo') def test_metadata_overrides_dot_fdroid_yml(self, git_Repo): """Fields in metadata files should override anything in .fdroid.yml.""" @@ -254,7 +261,8 @@ class MetadataTest(unittest.TestCase): metadata.parse_metadata(yml) # should not throw an exception @mock.patch('git.Repo') - def test_rewrite_yaml_fakeotaupdate(self, git_Repo): + @mock.patch('logging.error') + def test_rewrite_yaml_fakeotaupdate(self, logging_error, git_Repo): with tempfile.TemporaryDirectory() as testdir: testdir = Path(testdir) fdroidserver.common.config = {'accepted_formats': ['yml']} @@ -276,6 +284,10 @@ class MetadataTest(unittest.TestCase): (Path('metadata-rewrite-yml') / file_name).read_text(encoding='utf-8'), ) + # errors are printed when .yml overrides localized + logging_error.assert_called() + self.assertEqual(3, len(logging_error.call_args_list)) + @mock.patch('git.Repo') def test_rewrite_yaml_fdroidclient(self, git_Repo): with tempfile.TemporaryDirectory() as testdir: @@ -300,24 +312,24 @@ class MetadataTest(unittest.TestCase): @mock.patch('git.Repo') def test_rewrite_yaml_special_build_params(self, git_Repo): - with tempfile.TemporaryDirectory() as testdir: - testdir = Path(testdir) + """Test rewriting a plain YAML metadata file without localized files.""" + os.chdir(self.testdir) + os.mkdir('metadata') + appid = 'app.with.special.build.params' + file_name = Path('metadata/%s.yml' % appid) + shutil.copy(self.basedir / file_name, file_name) - # rewrite metadata - allapps = fdroidserver.metadata.read_metadata() - for appid, app in allapps.items(): - if appid == 'app.with.special.build.params': - fdroidserver.metadata.write_metadata( - testdir / (appid + '.yml'), app - ) + # rewrite metadata + allapps = fdroidserver.metadata.read_metadata({appid: -1}) + for appid, app in allapps.items(): + metadata.write_metadata(file_name, app) - # assert rewrite result - self.maxDiff = None - file_name = 'app.with.special.build.params.yml' - self.assertEqual( - (testdir / file_name).read_text(encoding='utf-8'), - (Path('metadata-rewrite-yml') / file_name).read_text(encoding='utf-8'), - ) + # assert rewrite result + self.maxDiff = None + self.assertEqual( + file_name.read_text(), + (self.basedir / 'metadata-rewrite-yml' / file_name.name).read_text(), + ) def test_normalize_type_string(self): """TYPE_STRING currently has some quirky behavior.""" @@ -331,6 +343,18 @@ class MetadataTest(unittest.TestCase): self.assertEqual('false', metadata._normalize_type_string(False)) self.assertEqual('true', metadata._normalize_type_string(True)) + def test_normalize_type_stringmap_none(self): + self.assertEqual(dict(), metadata._normalize_type_stringmap('key', None)) + + def test_normalize_type_stringmap_empty_list(self): + self.assertEqual(dict(), metadata._normalize_type_stringmap('AntiFeatures', [])) + + def test_normalize_type_stringmap_simple_list_format(self): + self.assertEqual( + {'Ads': {}, 'Tracking': {}}, + metadata._normalize_type_stringmap('AntiFeatures', ['Ads', 'Tracking']), + ) + def test_post_parse_yaml_metadata(self): yamldata = dict() metadata.post_parse_yaml_metadata(yamldata) @@ -507,6 +531,96 @@ class MetadataTest(unittest.TestCase): _warning.assert_called_once() _error.assert_called_once() + def test_parse_localized_antifeatures(self): + """Unit test based on reading files included in the test repo.""" + app = dict() + app['id'] = 'app.with.special.build.params' + metadata.parse_localized_antifeatures(app) + self.maxDiff = None + self.assertEqual( + app, + { + 'AntiFeatures': { + 'Ads': {'en-US': 'please no'}, + 'NoSourceSince': {'en-US': 'no activity\n'}, + }, + 'Builds': [ + { + 'versionCode': 50, + 'antifeatures': { + 'Ads': { + 'en-US': 'includes ad lib\n', + 'zh-CN': '包括广告图书馆\n', + }, + 'Tracking': {'en-US': 'standard suspects\n'}, + }, + }, + { + 'versionCode': 49, + 'antifeatures': { + 'Tracking': {'zh-CN': 'Text from zh-CN/49_Tracking.txt'}, + }, + }, + ], + 'id': app['id'], + }, + ) + + def test_parse_localized_antifeatures_passthrough(self): + """Test app values are cleanly passed through if no localized files.""" + before = { + 'id': 'placeholder', + 'AntiFeatures': {'NonFreeDep': {}}, + 'Builds': [{'versionCode': 999, 'antifeatures': {'zero': {}, 'one': {}}}], + } + after = copy.deepcopy(before) + with tempfile.TemporaryDirectory() as testdir: + os.chdir(testdir) + os.mkdir('metadata') + os.mkdir(os.path.join('metadata', after['id'])) + metadata.parse_localized_antifeatures(after) + self.assertEqual(before, after) + + def test_parse_metadata_antifeatures_NoSourceSince(self): + """Test that NoSourceSince gets added as an Anti-Feature.""" + os.chdir(self.testdir) + yml = Path('metadata/test.yml') + yml.parent.mkdir() + with yml.open('w') as fp: + fp.write('AntiFeatures: Ads\nNoSourceSince: gone\n') + app = metadata.parse_metadata(yml) + self.assertEqual( + app['AntiFeatures'], {'Ads': {}, 'NoSourceSince': {DEFAULT_LOCALE: 'gone'}} + ) + + @mock.patch('logging.error') + def test_yml_overrides_localized_antifeatures(self, logging_error): + """Definitions in .yml files should override the localized versions.""" + app = metadata.parse_metadata('metadata/app.with.special.build.params.yml') + + self.assertEqual(app['AntiFeatures'], {'UpstreamNonFree': {}}) + + self.assertEqual(49, app['Builds'][-3]['versionCode']) + self.assertEqual( + app['Builds'][-3]['antifeatures'], + {'Tracking': {DEFAULT_LOCALE: 'Uses the Facebook SDK.'}}, + ) + + self.assertEqual(50, app['Builds'][-2]['versionCode']) + self.assertEqual( + app['Builds'][-2]['antifeatures'], + { + 'Ads': { + 'en-US': 'includes ad lib\n', + 'zh-CN': '包括广告图书馆\n', + }, + 'Tracking': {'en-US': 'standard suspects\n'}, + }, + ) + # errors are printed when .yml overrides localized + logging_error.assert_called() + self.assertEqual(3, len(logging_error.call_args_list)) + def test_parse_yaml_srclib_corrupt_file(self): with tempfile.TemporaryDirectory() as testdir: testdir = Path(testdir) @@ -741,6 +855,257 @@ class MetadataTest(unittest.TestCase): self.assertNotIn('Provides', result) self.assertNotIn('provides', result) + def test_parse_yaml_app_antifeatures_dict(self): + nonfreenet = 'free it!' + tracking = 'so many' + mf = io.StringIO( + textwrap.dedent( + f""" + AntiFeatures: + Tracking: {tracking} + NonFreeNet: {nonfreenet} + """ + ) + ) + self.assertEqual( + metadata.parse_yaml_metadata(mf), + { + 'AntiFeatures': { + 'NonFreeNet': {DEFAULT_LOCALE: nonfreenet}, + 'Tracking': {DEFAULT_LOCALE: tracking}, + } + }, + ) + + def test_parse_yaml_metadata_build_antifeatures_old_style(self): + mf = _get_mock_mf( + textwrap.dedent( + """ + AntiFeatures: + - Ads + Builds: + - versionCode: 123 + antifeatures: + - KnownVuln + - UpstreamNonFree + - NonFreeAssets + """ + ) + ) + self.assertEqual( + metadata.parse_yaml_metadata(mf), + { + 'AntiFeatures': {'Ads': {}}, + 'Builds': [ + { + 'antifeatures': { + 'KnownVuln': {}, + 'NonFreeAssets': {}, + 'UpstreamNonFree': {}, + }, + 'versionCode': 123, + } + ], + }, + ) + + def test_parse_yaml_metadata_antifeatures_sort(self): + """All data should end up sorted, to minimize diffs in the index files.""" + self.assertEqual( + metadata.parse_yaml_metadata( + _get_mock_mf( + textwrap.dedent( + """ + Builds: + - versionCode: 123 + antifeatures: + KnownVuln: + es: 2nd + az: zero + en-US: first + UpstreamNonFree: + NonFreeAssets: + AntiFeatures: + NonFreeDep: + Ads: + sw: 2nd + zh-CN: 3rd + de: 1st + """ + ) + ) + ), + { + 'AntiFeatures': { + 'Ads': {'de': '1st', 'sw': '2nd', 'zh-CN': '3rd'}, + 'NonFreeDep': {}, + }, + 'Builds': [ + { + 'antifeatures': { + 'KnownVuln': {'az': 'zero', 'en-US': 'first', 'es': '2nd'}, + 'NonFreeAssets': {}, + 'UpstreamNonFree': {}, + }, + 'versionCode': 123, + } + ], + }, + ) + + def test_parse_yaml_app_antifeatures_str(self): + self.assertEqual( + metadata.parse_yaml_metadata(io.StringIO('AntiFeatures: Tracking')), + {'AntiFeatures': {'Tracking': {}}}, + ) + + def test_parse_yaml_app_antifeatures_bool(self): + self.assertEqual( + metadata.parse_yaml_metadata(io.StringIO('AntiFeatures: true')), + {'AntiFeatures': {'true': {}}}, + ) + + def test_parse_yaml_app_antifeatures_int(self): + self.assertEqual( + metadata.parse_yaml_metadata(io.StringIO('AntiFeatures: 1')), + {'AntiFeatures': {'1': {}}}, + ) + + def test_parse_yaml_app_antifeatures_float(self): + self.assertEqual( + metadata.parse_yaml_metadata(io.StringIO('AntiFeatures: 1.0')), + {'AntiFeatures': {'1.0': {}}}, + ) + + def test_parse_yaml_app_antifeatures_list_float(self): + self.assertEqual( + metadata.parse_yaml_metadata(io.StringIO('AntiFeatures:\n - 1.0\n')), + {'AntiFeatures': {'1.0': {}}}, + ) + + def test_parse_yaml_app_antifeatures_dict_float(self): + mf = io.StringIO('AntiFeatures:\n 0.0: too early\n') + self.assertEqual( + metadata.parse_yaml_metadata(mf), + {'AntiFeatures': {'0.0': {'en-US': 'too early'}}}, + ) + + def test_parse_yaml_app_antifeatures_dict_float_fail_value(self): + mf = io.StringIO('AntiFeatures:\n NoSourceSince: 1.0\n') + self.assertEqual( + metadata.parse_yaml_metadata(mf), + {'AntiFeatures': {'NoSourceSince': {'en-US': '1.0'}}}, + ) + + def test_parse_yaml_metadata_type_stringmap_old_list(self): + mf = _get_mock_mf( + textwrap.dedent( + """ + AntiFeatures: + - Ads + - Tracking + """ + ) + ) + self.assertEqual( + {'AntiFeatures': {'Ads': {}, 'Tracking': {}}}, + metadata.parse_yaml_metadata(mf), + ) + + def test_parse_yaml_app_antifeatures_dict_no_value(self): + mf = io.StringIO( + textwrap.dedent( + """\ + AntiFeatures: + Tracking: + NonFreeNet: + """ + ) + ) + self.assertEqual( + metadata.parse_yaml_metadata(mf), + {'AntiFeatures': {'NonFreeNet': {}, 'Tracking': {}}}, + ) + + def test_parse_yaml_metadata_type_stringmap_transitional(self): + """Support a transitional format, where users just append a text""" + ads = 'Has ad lib in it.' + tracking = 'opt-out reports with ACRA' + mf = _get_mock_mf( + textwrap.dedent( + f""" + AntiFeatures: + - Ads: {ads} + - Tracking: {tracking} + """ + ) + ) + self.assertEqual( + metadata.parse_yaml_metadata(mf), + { + 'AntiFeatures': { + 'Ads': {DEFAULT_LOCALE: ads}, + 'Tracking': {DEFAULT_LOCALE: tracking}, + } + }, + ) + + def test_parse_yaml_app_antifeatures_dict_mixed_values(self): + ads = 'true' + tracking = 'many' + nonfreenet = '1' + mf = io.StringIO( + textwrap.dedent( + f""" + AntiFeatures: + Ads: {ads} + Tracking: {tracking} + NonFreeNet: {nonfreenet} + """ + ) + ) + self.assertEqual( + metadata.parse_yaml_metadata(mf), + { + 'AntiFeatures': { + 'Ads': {DEFAULT_LOCALE: ads}, + 'NonFreeNet': {DEFAULT_LOCALE: nonfreenet}, + 'Tracking': {DEFAULT_LOCALE: tracking}, + } + }, + ) + + def test_parse_yaml_app_antifeatures_stringmap_full(self): + ads = 'watching' + tracking = 'many' + nonfreenet = 'pipes' + nonfreenet_zh = '非免费网络' + self.maxDiff = None + mf = io.StringIO( + textwrap.dedent( + f""" + AntiFeatures: + Ads: + {DEFAULT_LOCALE}: {ads} + Tracking: + {DEFAULT_LOCALE}: {tracking} + NonFreeNet: + {DEFAULT_LOCALE}: {nonfreenet} + zh-CN: {nonfreenet_zh} + """ + ) + ) + self.assertEqual( + metadata.parse_yaml_metadata(mf), + { + 'AntiFeatures': { + 'Ads': {DEFAULT_LOCALE: ads}, + 'NonFreeNet': {DEFAULT_LOCALE: nonfreenet, 'zh-CN': nonfreenet_zh}, + 'Tracking': {DEFAULT_LOCALE: tracking}, + } + }, + ) + def test_write_yaml_1_line_scripts_as_string(self): mf = io.StringIO() app = fdroidserver.metadata.App() @@ -931,6 +1296,87 @@ class MetadataTest(unittest.TestCase): ), ) + def test_write_yaml_build_antifeatures(self): + mf = io.StringIO() + app = metadata.App( + { + 'License': 'Apache-2.0', + 'Builds': [ + metadata.Build( + { + 'versionCode': 102030, + 'versionName': 'v1.2.3', + 'gradle': ['yes'], + 'antifeatures': { + 'a': {}, + 'b': {'de': 'Probe', 'en-US': 'test'}, + }, + } + ), + ], + 'id': 'placeholder', + } + ) + metadata.write_yaml(mf, app) + mf.seek(0) + self.assertEqual( + mf.read(), + textwrap.dedent( + """\ + License: Apache-2.0 + + Builds: + - versionName: v1.2.3 + versionCode: 102030 + gradle: + - yes + antifeatures: + a: {} + b: + de: Probe + en-US: test + """ + ), + ) + + def test_write_yaml_build_antifeatures_old_style(self): + mf = io.StringIO() + app = metadata.App( + { + 'License': 'Apache-2.0', + 'Builds': [ + metadata.Build( + { + 'versionCode': 102030, + 'versionName': 'v1.2.3', + 'gradle': ['yes'], + 'antifeatures': {'b': {}, 'a': {}}, + } + ), + ], + 'id': 'placeholder', + } + ) + metadata.write_yaml(mf, app) + mf.seek(0) + self.assertEqual( + mf.read(), + textwrap.dedent( + """\ + License: Apache-2.0 + + Builds: + - versionName: v1.2.3 + versionCode: 102030 + gradle: + - yes + antifeatures: + - a + - b + """ + ), + ) + def test_write_yaml_make_sure_provides_does_not_get_written(self): mf = io.StringIO() app = fdroidserver.metadata.App() @@ -1248,6 +1694,219 @@ class MetadataTest(unittest.TestCase): with self.assertRaises(TypeError): build.ndk_path() + def test_del_duplicated_NoSourceSince(self): + app = { + 'AntiFeatures': {'Ads': {}, 'NoSourceSince': {DEFAULT_LOCALE: '1.0'}}, + 'NoSourceSince': '1.0', + } + metadata._del_duplicated_NoSourceSince(app) + self.assertEqual(app, {'AntiFeatures': {'Ads': {}}, 'NoSourceSince': '1.0'}) + + def test_check_manually_extended_NoSourceSince(self): + app = { + 'AntiFeatures': {'NoSourceSince': {DEFAULT_LOCALE: '1.0', 'de': '1,0'}}, + 'NoSourceSince': '1.0', + } + metadata._del_duplicated_NoSourceSince(app) + self.assertEqual( + app, + { + 'AntiFeatures': {'NoSourceSince': {DEFAULT_LOCALE: '1.0', 'de': '1,0'}}, + 'NoSourceSince': '1.0', + }, + ) + + def test_make_sure_nosourcesince_does_not_get_written(self): + appid = 'com.politedroid' + app = metadata.read_metadata({appid: -1})[appid] + builds = app['Builds'] + app['Builds'] = [copy.deepcopy(builds[0])] + mf = io.StringIO() + metadata.write_yaml(mf, app) + mf.seek(0) + self.maxDiff = None + self.assertEqual( + mf.read(), + textwrap.dedent( + """\ + AntiFeatures: + - NonFreeNet + Categories: + - Time + License: GPL-3.0-only + SourceCode: https://github.com/miguelvps/PoliteDroid + IssueTracker: https://github.com/miguelvps/PoliteDroid/issues + + AutoName: Polite Droid + Summary: Calendar tool + Description: Activates silent mode during calendar events. + + RepoType: git + Repo: https://github.com/miguelvps/PoliteDroid.git + + Builds: + - versionName: '1.2' + versionCode: 3 + commit: 6a548e4b19 + target: android-10 + antifeatures: + - KnownVuln + - NonFreeAssets + - UpstreamNonFree + + ArchivePolicy: 4 versions + AutoUpdateMode: Version v%v + UpdateCheckMode: Tags + CurrentVersion: '1.5' + CurrentVersionCode: 6 + + NoSourceSince: '1.5' + """ + ), + ) + + def test_app_to_yaml_smokecheck(self): + self.assertTrue( + isinstance(metadata._app_to_yaml(dict()), ruamel.yaml.comments.CommentedMap) + ) + + def test_app_to_yaml_build_list_empty(self): + app = metadata.App({'Builds': [metadata.Build({'rm': []})]}) + self.assertEqual(dict(), metadata._app_to_yaml(app)['Builds'][0]) + + def test_app_to_yaml_build_list_string(self): + app = metadata.App({'Builds': [metadata.Build({'rm': 'one'})]}) + self.assertEqual({'rm': 'one'}, metadata._app_to_yaml(app)['Builds'][0]) + + def test_app_to_yaml_build_list_one(self): + app = metadata.App({'Builds': [metadata.Build({'rm': ['one']})]}) + self.assertEqual({'rm': ['one']}, metadata._app_to_yaml(app)['Builds'][0]) + + def test_app_to_yaml_build_list(self): + app = metadata.App({'Builds': [metadata.Build({'rm': ['b2', 'NO1']})]}) + self.assertEqual({'rm': ['b2', 'NO1']}, metadata._app_to_yaml(app)['Builds'][0]) + + def test_app_to_yaml_AllowedAPKSigningKeys_two(self): + cm = metadata._app_to_yaml(metadata.App({'AllowedAPKSigningKeys': ['b', 'A']})) + self.assertEqual(['b', 'a'], cm['AllowedAPKSigningKeys']) + + def test_app_to_yaml_AllowedAPKSigningKeys_one(self): + cm = metadata._app_to_yaml(metadata.App({'AllowedAPKSigningKeys': ['One']})) + self.assertEqual('one', cm['AllowedAPKSigningKeys']) + + def test_app_to_yaml_int_hex(self): + cm = metadata._app_to_yaml(metadata.App({'CurrentVersionCode': 0xFF})) + self.assertEqual(255, cm['CurrentVersionCode']) + + def test_app_to_yaml_int_underscore(self): + cm = metadata._app_to_yaml(metadata.App({'CurrentVersionCode': 1_2_3})) + self.assertEqual(123, cm['CurrentVersionCode']) + + def test_app_to_yaml_int_0(self): + """Document that 0 values fail to make it through.""" + # TODO it should be possible to use `CurrentVersionCode: 0` + cm = metadata._app_to_yaml(metadata.App({'CurrentVersionCode': 0})) + self.assertFalse('CurrentVersionCode' in cm) + + def test_format_stringmap_empty(self): + self.assertEqual( + metadata._format_stringmap('🔥', 'test', dict()), + list(), + ) + + def test_format_stringmap_one_list(self): + self.assertEqual( + metadata._format_stringmap('🔥', 'test', {'Tracking': {}, 'Ads': {}}), + ['Ads', 'Tracking'], + ) + + def test_format_stringmap_one_list_empty_desc(self): + self.assertEqual( + metadata._format_stringmap('🔥', 'test', {'NonFree': {}, 'Ads': {'en': ''}}), + ['Ads', 'NonFree'], + ) + + def test_format_stringmap_three_list(self): + self.assertEqual( + metadata._format_stringmap('🔥', 'test', {'B': {}, 'A': {}, 'C': {}}), + ['A', 'B', 'C'], + ) + + def test_format_stringmap_two_dict(self): + self.assertEqual( + metadata._format_stringmap('🔥', 'test', {'1': {'uz': 'a'}, '2': {}}), + {'1': {'uz': 'a'}, '2': {}}, + ) + + def test_format_stringmap_three_locales(self): + self.assertEqual( + metadata._format_stringmap( + '🔥', 'test', {'AF': {'uz': 'a', 'ko': 'b', 'zh': 'c'}} + ), + {'AF': {'ko': 'b', 'uz': 'a', 'zh': 'c'}}, + ) + + def test_format_stringmap_move_build_antifeatures_to_filesystem(self): + os.chdir(self.testdir) + appid = 'a' + yml = Path('metadata/a.yml') + yml.parent.mkdir() + self.assertEqual( + metadata._format_stringmap( + appid, 'antifeatures', {'AF': {'uz': 'a', 'ko': 'b', 'zh': 'c'}} + ), + {'AF': {'ko': 'b', 'uz': 'a', 'zh': 'c'}}, + ) + + def test_format_stringmap_app_antifeatures_conflict(self): + """Raise an error if a YAML Anti-Feature conflicts with a localized file.""" + os.chdir(self.testdir) + appid = 'a' + field = 'AntiFeatures' + locale = 'ko' + yml = Path('metadata/a.yml') + antifeatures_ko = yml.parent / appid / locale / field.lower() + antifeatures_ko.mkdir(parents=True) + afname = 'Anti-🔥' + (antifeatures_ko / (afname + '.txt')).write_text('SOMETHING ELSE') + with self.assertRaises(MetaDataException): + metadata._format_stringmap( + appid, field, {afname: {'uz': 'a', locale: 'b', 'zh': 'c'}} + ) + + def test_format_stringmap_app_antifeatures_conflict_same_contents(self): + """Raise an error if a YAML Anti-Feature conflicts with a localized file.""" + os.chdir(self.testdir) + appid = 'a' + field = 'AntiFeatures' + locale = 'ko' + yml = Path('metadata/a.yml') + antifeatures_ko = yml.parent / appid / locale / field.lower() + antifeatures_ko.mkdir(parents=True) + afname = 'Anti-🔥' + (antifeatures_ko / (afname + '.txt')).write_text('b') + metadata._format_stringmap( + appid, field, {afname: {'uz': 'a', locale: 'b', 'zh': 'c'}} + ) + + def test_format_stringmap_build_antifeatures_conflict(self): + """Raise an error if a YAML Anti-Feature conflicts with a localized file.""" + os.chdir(self.testdir) + appid = 'a' + field = 'antifeatures' + locale = 'ko' + versionCode = 123 + yml = Path('metadata/a.yml') + antifeatures_ko = yml.parent / appid / locale / field.lower() + antifeatures_ko.mkdir(parents=True) + afname = 'Anti-🔥' + with (antifeatures_ko / ('%d_%s.txt' % (versionCode, afname))).open('w') as fp: + fp.write('SOMETHING ELSE') + with self.assertRaises(MetaDataException): + metadata._format_stringmap( + appid, field, {afname: {'uz': 'a', locale: 'b', 'zh': 'c'}}, versionCode + ) + class PostMetadataParseTest(unittest.TestCase): """Test the functions that post process the YAML input. @@ -1263,9 +1922,9 @@ class PostMetadataParseTest(unittest.TestCase): fdroidserver.metadata.warnings_action = 'error' def _post_metadata_parse_app_list(self, from_yaml, expected): - app = {'AntiFeatures': from_yaml} + app = {'AllowedAPKSigningKeys': from_yaml} metadata.post_parse_yaml_metadata(app) - return {'AntiFeatures': expected}, app + return {'AllowedAPKSigningKeys': expected}, app def _post_metadata_parse_app_string(self, from_yaml, expected): app = {'Repo': from_yaml} diff --git a/tests/metadata/apk/info.guardianproject.urzip.yaml b/tests/metadata/apk/info.guardianproject.urzip.yaml index a3e75d87..ffe17471 100644 --- a/tests/metadata/apk/info.guardianproject.urzip.yaml +++ b/tests/metadata/apk/info.guardianproject.urzip.yaml @@ -1,4 +1,4 @@ -antiFeatures: !!set {} +antiFeatures: {} features: [] hash: abfb3adb7496611749e7abfb014c5c789e3a02489e48a5c3665110d1b1acd931 hashType: sha256 diff --git a/tests/metadata/apk/org.dyndns.fules.ck.yaml b/tests/metadata/apk/org.dyndns.fules.ck.yaml index b4e16511..876a9c6a 100644 --- a/tests/metadata/apk/org.dyndns.fules.ck.yaml +++ b/tests/metadata/apk/org.dyndns.fules.ck.yaml @@ -1,4 +1,4 @@ -antiFeatures: !!set {} +antiFeatures: {} features: [] hash: 897486e1f857c6c0ee32ccbad0e1b8cd82f6d0e65a44a23f13f852d2b63a18c8 hashType: sha256 diff --git a/tests/metadata/app.with.special.build.params.yml b/tests/metadata/app.with.special.build.params.yml index d268d049..b2daee52 100644 --- a/tests/metadata/app.with.special.build.params.yml +++ b/tests/metadata/app.with.special.build.params.yml @@ -77,6 +77,8 @@ Builds: maven: yes@.. srclibs: - FacebookSDK@sdk-version-3.0.2 + antifeatures: + Tracking: Uses the Facebook SDK. - versionName: 2.1.1-c versionCode: 50 diff --git a/tests/metadata/app.with.special.build.params/en-US/antifeatures/50_Ads.txt b/tests/metadata/app.with.special.build.params/en-US/antifeatures/50_Ads.txt new file mode 100644 index 00000000..982512f0 --- /dev/null +++ b/tests/metadata/app.with.special.build.params/en-US/antifeatures/50_Ads.txt @@ -0,0 +1 @@ +includes ad lib diff --git a/tests/metadata/app.with.special.build.params/en-US/antifeatures/50_Tracking.txt b/tests/metadata/app.with.special.build.params/en-US/antifeatures/50_Tracking.txt new file mode 100644 index 00000000..bc711b96 --- /dev/null +++ b/tests/metadata/app.with.special.build.params/en-US/antifeatures/50_Tracking.txt @@ -0,0 +1 @@ +standard suspects diff --git a/tests/metadata/app.with.special.build.params/en-US/antifeatures/Ads.txt b/tests/metadata/app.with.special.build.params/en-US/antifeatures/Ads.txt new file mode 100644 index 00000000..d7531e59 --- /dev/null +++ b/tests/metadata/app.with.special.build.params/en-US/antifeatures/Ads.txt @@ -0,0 +1 @@ +please no \ No newline at end of file diff --git a/tests/metadata/app.with.special.build.params/en-US/antifeatures/NoSourceSince.txt b/tests/metadata/app.with.special.build.params/en-US/antifeatures/NoSourceSince.txt new file mode 100644 index 00000000..067eb2c4 --- /dev/null +++ b/tests/metadata/app.with.special.build.params/en-US/antifeatures/NoSourceSince.txt @@ -0,0 +1 @@ +no activity diff --git a/tests/metadata/app.with.special.build.params/zh-CN/antifeatures/49_Tracking.txt b/tests/metadata/app.with.special.build.params/zh-CN/antifeatures/49_Tracking.txt new file mode 100644 index 00000000..fea9557e --- /dev/null +++ b/tests/metadata/app.with.special.build.params/zh-CN/antifeatures/49_Tracking.txt @@ -0,0 +1 @@ +Text from zh-CN/49_Tracking.txt \ No newline at end of file diff --git a/tests/metadata/app.with.special.build.params/zh-CN/antifeatures/50_Ads.txt b/tests/metadata/app.with.special.build.params/zh-CN/antifeatures/50_Ads.txt new file mode 100644 index 00000000..f0bc2f11 --- /dev/null +++ b/tests/metadata/app.with.special.build.params/zh-CN/antifeatures/50_Ads.txt @@ -0,0 +1 @@ +包括广告图书馆 diff --git a/tests/metadata/dump/app.with.special.build.params.yaml b/tests/metadata/dump/app.with.special.build.params.yaml new file mode 100644 index 00000000..f53ce361 --- /dev/null +++ b/tests/metadata/dump/app.with.special.build.params.yaml @@ -0,0 +1,373 @@ +AllowedAPKSigningKeys: [] +AntiFeatures: + UpstreamNonFree: {} +ArchivePolicy: 0 versions +AuthorEmail: null +AuthorName: null +AuthorWebSite: null +AutoName: UberSync for Facebook +AutoUpdateMode: None +Binaries: null +Bitcoin: null +Builds: +- androidupdate: [] + antcommands: [] + antifeatures: {} + binary: null + build: '' + buildjni: [] + commit: b3879c973e7cac3a3319 + disable: '' + encoding: null + extlibs: [] + forcevercode: false + forceversion: false + gradle: [] + gradleprops: [] + init: '' + maven: null + ndk: null + novcheck: false + oldsdkloc: false + output: null + patch: [] + postbuild: '' + preassemble: [] + prebuild: '' + rm: [] + scandelete: [] + scanignore: [] + srclibs: [] + subdir: null + submodules: false + sudo: '' + target: null + timeout: null + versionCode: 32 + versionName: 1.0.0 +- androidupdate: [] + antcommands: [] + antifeatures: {} + binary: null + build: '' + buildjni: [] + commit: 252c8dd4c9 + disable: '' + encoding: null + extlibs: [] + forcevercode: false + forceversion: false + gradle: [] + gradleprops: [] + init: '' + maven: null + ndk: null + novcheck: false + oldsdkloc: false + output: null + patch: [] + postbuild: '' + preassemble: [] + prebuild: '' + rm: [] + scandelete: [] + scanignore: [] + srclibs: [] + subdir: null + submodules: false + sudo: '' + target: null + timeout: null + versionCode: 33 + versionName: 1.0.1 +- androidupdate: [] + antcommands: [] + antifeatures: {} + binary: null + build: '' + buildjni: [] + commit: v1.2.0 + disable: '' + encoding: null + extlibs: [] + forcevercode: false + forceversion: false + gradle: [] + gradleprops: [] + init: '' + maven: null + ndk: null + novcheck: false + oldsdkloc: false + output: null + patch: + - appbrain.patch + postbuild: '' + preassemble: [] + prebuild: + - sed -i 's@\(reference.1=\).*@\1$$FacebookSDK$$@' project.properties + - sed -i 's/Class\[\]/Class\\[\]/g' $$FacebookSDK$$/src/com/facebook/model/GraphObject.java + rm: + - libs/appbrain-sdk-android.jar + scandelete: [] + scanignore: [] + srclibs: + - FacebookSDK@sdk-version-3.0.1 + subdir: null + submodules: false + sudo: '' + target: null + timeout: null + versionCode: 39 + versionName: 1.2.0 +- androidupdate: [] + antcommands: [] + antifeatures: {} + binary: null + build: '' + buildjni: [] + commit: v1.2.2 + disable: '' + encoding: null + extlibs: + - android/android-support-v4.jar + forcevercode: false + forceversion: false + gradle: [] + gradleprops: [] + init: '' + maven: null + ndk: null + novcheck: false + oldsdkloc: false + output: null + patch: + - appbrain.patch + postbuild: '' + preassemble: [] + prebuild: + - mv libs/android-support-v4.jar $$FacebookSDK$$/libs/ + - sed -i 's@\(reference.1=\).*@\1$$FacebookSDK$$@' project.properties + - sed -i 's/Class\[\]/Class\\[\]/g' $$FacebookSDK$$/src/com/facebook/model/GraphObject.java + rm: + - libs/appbrain-sdk-android.jar + scandelete: [] + scanignore: [] + srclibs: + - FacebookSDK@sdk-version-3.0.2 + subdir: null + submodules: false + sudo: '' + target: null + timeout: null + versionCode: 42 + versionName: 1.2.2 +- androidupdate: [] + antcommands: [] + antifeatures: {} + binary: null + build: '' + buildjni: [] + commit: 2.1.1 + disable: '' + encoding: null + extlibs: [] + forcevercode: false + forceversion: false + gradle: [] + gradleprops: [] + init: '' + maven: yes + ndk: null + novcheck: false + oldsdkloc: false + output: null + patch: + - manifest-ads.patch + - mobilecore.patch + postbuild: '' + preassemble: [] + prebuild: '' + rm: [] + scandelete: [] + scanignore: [] + srclibs: + - FacebookSDK@sdk-version-3.0.2 + subdir: null + submodules: false + sudo: '' + target: null + timeout: null + versionCode: 48 + versionName: 2.1.1 +- androidupdate: [] + antcommands: [] + antifeatures: + Tracking: + en-US: Uses the Facebook SDK. + binary: null + build: '' + buildjni: [] + commit: 2.1.1 + disable: '' + encoding: null + extlibs: [] + forcevercode: false + forceversion: false + gradle: [] + gradleprops: [] + init: '' + maven: yes@.. + ndk: null + novcheck: false + oldsdkloc: false + output: null + patch: + - manifest-ads.patch + - mobilecore.patch + postbuild: '' + preassemble: [] + prebuild: '' + rm: [] + scandelete: [] + scanignore: [] + srclibs: + - FacebookSDK@sdk-version-3.0.2 + subdir: null + submodules: false + sudo: '' + target: null + timeout: null + versionCode: 49 + versionName: 2.1.1-b +- androidupdate: [] + antcommands: [] + antifeatures: + Ads: + en-US: 'includes ad lib + + ' + zh-CN: '包括广告图书馆 + + ' + Tracking: + en-US: 'standard suspects + + ' + binary: null + build: '' + buildjni: [] + commit: 2.1.1 + disable: '' + encoding: null + extlibs: [] + forcevercode: false + forceversion: false + gradle: [] + gradleprops: [] + init: '' + maven: '2' + ndk: null + novcheck: false + oldsdkloc: false + output: null + patch: + - manifest-ads.patch + - mobilecore.patch + postbuild: '' + preassemble: [] + prebuild: '' + rm: [] + scandelete: [] + scanignore: [] + srclibs: + - FacebookSDK@sdk-version-3.0.2 + subdir: null + submodules: false + sudo: '' + target: null + timeout: null + versionCode: 50 + versionName: 2.1.1-c +- androidupdate: [] + antcommands: [] + antifeatures: {} + binary: null + build: '' + buildjni: [] + commit: null + disable: Labelled as pre-release, so skipped + encoding: null + extlibs: [] + forcevercode: false + forceversion: false + gradle: [] + gradleprops: [] + init: '' + maven: null + ndk: null + novcheck: false + oldsdkloc: false + output: null + patch: [] + postbuild: '' + preassemble: [] + prebuild: '' + rm: [] + scandelete: [] + scanignore: [] + srclibs: [] + subdir: null + submodules: false + sudo: '' + target: null + timeout: null + versionCode: 51 + versionName: 2.1.2 +Categories: +- System +Changelog: '' +CurrentVersion: 2.1.2 +CurrentVersionCode: 49 +Description: 'To configure, go to "Settings => Accounts & Sync => Add Account". Depending + on + + how many friends you have, the first import might take a while, so be patient. + + + * Facebook does not allow to export phone numbers or emails: only names, pictures + and statuses are synced. + + * Facebook users have the option to block one or all apps: if they opt for that, + they will be EXCLUDED from your friends list. + + + Appbrain SDK was removed before building.' +Disabled: null +Donate: null +FlattrID: null +IssueTracker: https://github.com/loadrunner/Facebook-Contact-Sync/issues +Liberapay: null +License: GPL-3.0-only +Litecoin: null +MaintainerNotes: '' +Name: null +NoSourceSince: '' +OpenCollective: null +Provides: null +Repo: https://github.com/loadrunner/Facebook-Contact-Sync.git +RepoType: git +RequiresRoot: false +SourceCode: https://github.com/loadrunner/Facebook-Contact-Sync +Summary: Sync your Facebook Contacts +Translation: '' +UpdateCheckData: null +UpdateCheckIgnore: null +UpdateCheckMode: None +UpdateCheckName: null +VercodeOperation: [] +WebSite: '' +added: null +id: app.with.special.build.params +lastUpdated: null +metadatapath: metadata/app.with.special.build.params.yml diff --git a/tests/metadata/dump/com.politedroid.yaml b/tests/metadata/dump/com.politedroid.yaml index 783308b7..e4f24356 100644 --- a/tests/metadata/dump/com.politedroid.yaml +++ b/tests/metadata/dump/com.politedroid.yaml @@ -1,6 +1,8 @@ AllowedAPKSigningKeys: [] AntiFeatures: -- NonFreeNet + NoSourceSince: + en-US: '1.5' + NonFreeNet: {} ArchivePolicy: 4 versions AuthorEmail: null AuthorName: null @@ -13,9 +15,9 @@ Builds: - androidupdate: [] antcommands: [] antifeatures: - - KnownVuln - - UpstreamNonFree - - NonFreeAssets + KnownVuln: {} + NonFreeAssets: {} + UpstreamNonFree: {} binary: null build: '' buildjni: [] @@ -50,7 +52,7 @@ Builds: versionName: '1.2' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -85,7 +87,7 @@ Builds: versionName: '1.3' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -120,7 +122,7 @@ Builds: versionName: '1.4' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] diff --git a/tests/metadata/dump/org.adaway.yaml b/tests/metadata/dump/org.adaway.yaml index d9b26cb9..cf448af3 100644 --- a/tests/metadata/dump/org.adaway.yaml +++ b/tests/metadata/dump/org.adaway.yaml @@ -1,5 +1,5 @@ AllowedAPKSigningKeys: [] -AntiFeatures: [] +AntiFeatures: {} ArchivePolicy: null AuthorEmail: null AuthorName: null @@ -11,7 +11,7 @@ Bitcoin: null Builds: - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -47,7 +47,7 @@ Builds: versionName: '1.12' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -84,7 +84,7 @@ Builds: versionName: '1.15' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -121,7 +121,7 @@ Builds: versionName: '1.18' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -158,7 +158,7 @@ Builds: versionName: '1.19' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -195,7 +195,7 @@ Builds: versionName: '1.20' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -232,7 +232,7 @@ Builds: versionName: '1.21' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -267,7 +267,7 @@ Builds: versionName: '1.23' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -304,7 +304,7 @@ Builds: versionName: '1.24' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -341,7 +341,7 @@ Builds: versionName: '1.25' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -378,7 +378,7 @@ Builds: versionName: '1.26' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -415,7 +415,7 @@ Builds: versionName: '1.27' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -453,7 +453,7 @@ Builds: versionName: '1.29' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -491,7 +491,7 @@ Builds: versionName: '1.32' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -528,7 +528,7 @@ Builds: versionName: '1.33' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -566,7 +566,7 @@ Builds: versionName: '1.34' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -604,7 +604,7 @@ Builds: versionName: '1.35' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -642,7 +642,7 @@ Builds: versionName: '1.36' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -684,7 +684,7 @@ Builds: - android-libs/ActionBarSherlock - android-libs/HtmlSpanner/htmlspanner antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -734,7 +734,7 @@ Builds: - android-libs/ActionBarSherlock - android-libs/HtmlSpanner/htmlspanner antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -780,7 +780,7 @@ Builds: versionName: '2.3' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -818,7 +818,7 @@ Builds: versionName: '2.6' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -856,7 +856,7 @@ Builds: versionName: '2.7' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -894,7 +894,7 @@ Builds: versionName: '2.8' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -932,7 +932,7 @@ Builds: versionName: 2.8.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -970,7 +970,7 @@ Builds: versionName: '2.9' - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -1008,7 +1008,7 @@ Builds: versionName: 2.9.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: @@ -1046,7 +1046,7 @@ Builds: versionName: 2.9.2 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: diff --git a/tests/metadata/dump/org.smssecure.smssecure.yaml b/tests/metadata/dump/org.smssecure.smssecure.yaml index af864923..b363b736 100644 --- a/tests/metadata/dump/org.smssecure.smssecure.yaml +++ b/tests/metadata/dump/org.smssecure.smssecure.yaml @@ -1,5 +1,5 @@ AllowedAPKSigningKeys: [] -AntiFeatures: [] +AntiFeatures: {} ArchivePolicy: null AuthorEmail: null AuthorName: null @@ -11,7 +11,7 @@ Bitcoin: null Builds: - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -72,7 +72,7 @@ Builds: versionName: 0.3.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -115,7 +115,7 @@ Builds: versionName: 0.3.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -156,7 +156,7 @@ Builds: versionName: 0.4.2 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -197,7 +197,7 @@ Builds: versionName: 0.5.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -237,7 +237,7 @@ Builds: versionName: 0.5.2 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -277,7 +277,7 @@ Builds: versionName: 0.5.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] @@ -317,7 +317,7 @@ Builds: versionName: 0.5.4 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: '' buildjni: [] diff --git a/tests/metadata/dump/org.videolan.vlc.yaml b/tests/metadata/dump/org.videolan.vlc.yaml index b1679ab9..f91dfb65 100644 --- a/tests/metadata/dump/org.videolan.vlc.yaml +++ b/tests/metadata/dump/org.videolan.vlc.yaml @@ -1,5 +1,5 @@ AllowedAPKSigningKeys: [] -AntiFeatures: [] +AntiFeatures: {} ArchivePolicy: 9 versions AuthorEmail: null AuthorName: null @@ -14,7 +14,7 @@ Builds: - ../java-libs/SlidingMenu - ../java-libs/ActionBarSherlock antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -54,7 +54,7 @@ Builds: - ../java-libs/SlidingMenu - ../java-libs/ActionBarSherlock antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi ./compile.sh release @@ -94,7 +94,7 @@ Builds: - ../java-libs/SlidingMenu - ../java-libs/ActionBarSherlock antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -134,7 +134,7 @@ Builds: - ../java-libs/SlidingMenu - ../java-libs/ActionBarSherlock antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=mips ./compile.sh release @@ -171,7 +171,7 @@ Builds: versionName: 0.0.11-mips - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=mips ./compile.sh release @@ -210,7 +210,7 @@ Builds: versionName: 0.1.3-MIPS - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -249,7 +249,7 @@ Builds: versionName: 0.1.3-x86 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi ./compile.sh release @@ -288,7 +288,7 @@ Builds: versionName: 0.1.3-ARM - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -327,7 +327,7 @@ Builds: versionName: 0.1.3-ARMv7 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -365,7 +365,7 @@ Builds: versionName: 0.9.0 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -403,7 +403,7 @@ Builds: versionName: 0.9.0 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -441,7 +441,7 @@ Builds: versionName: 0.9.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -479,7 +479,7 @@ Builds: versionName: 0.9.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -517,7 +517,7 @@ Builds: versionName: 0.9.5 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -555,7 +555,7 @@ Builds: versionName: 0.9.5 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -593,7 +593,7 @@ Builds: versionName: 0.9.6 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -631,7 +631,7 @@ Builds: versionName: 0.9.6 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -669,7 +669,7 @@ Builds: versionName: 0.9.7 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -707,7 +707,7 @@ Builds: versionName: 0.9.7 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=mips ./compile.sh release @@ -745,7 +745,7 @@ Builds: versionName: 0.9.7.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -783,7 +783,7 @@ Builds: versionName: 0.9.7.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -821,7 +821,7 @@ Builds: versionName: 0.9.7.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -859,7 +859,7 @@ Builds: versionName: 0.9.8 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi ./compile.sh release @@ -897,7 +897,7 @@ Builds: versionName: 0.9.8 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -935,7 +935,7 @@ Builds: versionName: 0.9.8 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -973,7 +973,7 @@ Builds: versionName: 0.9.9 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi ./compile.sh release @@ -1011,7 +1011,7 @@ Builds: versionName: 0.9.9 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -1049,7 +1049,7 @@ Builds: versionName: 0.9.9 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -1087,7 +1087,7 @@ Builds: versionName: 0.9.10 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi ./compile.sh release @@ -1125,7 +1125,7 @@ Builds: versionName: 0.9.10 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -1163,7 +1163,7 @@ Builds: versionName: 0.9.10 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -1201,7 +1201,7 @@ Builds: versionName: 1.0.0 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi ./compile.sh release @@ -1239,7 +1239,7 @@ Builds: versionName: 1.0.0 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -1277,7 +1277,7 @@ Builds: versionName: 1.0.0 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=x86 ./compile.sh release @@ -1315,7 +1315,7 @@ Builds: versionName: 1.0.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi ./compile.sh release @@ -1353,7 +1353,7 @@ Builds: versionName: 1.0.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ANDROID_ABI=armeabi-v7a ./compile.sh release @@ -1391,7 +1391,7 @@ Builds: versionName: 1.0.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -1431,7 +1431,7 @@ Builds: versionName: 1.1.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -1471,7 +1471,7 @@ Builds: versionName: 1.1.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -1511,7 +1511,7 @@ Builds: versionName: 1.1.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -1551,7 +1551,7 @@ Builds: versionName: 1.1.5 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -1591,7 +1591,7 @@ Builds: versionName: 1.1.5 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -1631,7 +1631,7 @@ Builds: versionName: 1.1.5 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -1671,7 +1671,7 @@ Builds: versionName: 1.1.6 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -1711,7 +1711,7 @@ Builds: versionName: 1.1.6 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -1751,7 +1751,7 @@ Builds: versionName: 1.1.6 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -1791,7 +1791,7 @@ Builds: versionName: 1.2.0 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -1831,7 +1831,7 @@ Builds: versionName: 1.2.0 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -1871,7 +1871,7 @@ Builds: versionName: 1.2.0 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -1911,7 +1911,7 @@ Builds: versionName: 1.2.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -1951,7 +1951,7 @@ Builds: versionName: 1.2.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -1991,7 +1991,7 @@ Builds: versionName: 1.2.1 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -2031,7 +2031,7 @@ Builds: versionName: 1.2.2 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -2071,7 +2071,7 @@ Builds: versionName: 1.2.2 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -2111,7 +2111,7 @@ Builds: versionName: 1.2.2 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -2151,7 +2151,7 @@ Builds: versionName: 1.2.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -2191,7 +2191,7 @@ Builds: versionName: 1.2.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -2231,7 +2231,7 @@ Builds: versionName: 1.2.3 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -2271,7 +2271,7 @@ Builds: versionName: 1.2.4 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -2311,7 +2311,7 @@ Builds: versionName: 1.2.4 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -2351,7 +2351,7 @@ Builds: versionName: 1.2.4 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -2391,7 +2391,7 @@ Builds: versionName: 1.2.5 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -2431,7 +2431,7 @@ Builds: versionName: 1.2.5 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release @@ -2471,7 +2471,7 @@ Builds: versionName: 1.2.5 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi" --release @@ -2511,7 +2511,7 @@ Builds: versionName: 1.2.6 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "armeabi-v7a" --release @@ -2551,7 +2551,7 @@ Builds: versionName: 1.2.6 - androidupdate: [] antcommands: [] - antifeatures: [] + antifeatures: {} binary: null build: - cd ../ && ./compile.sh -a "x86" --release diff --git a/tests/repo/entry.json b/tests/repo/entry.json index f13e622b..13514cc5 100644 --- a/tests/repo/entry.json +++ b/tests/repo/entry.json @@ -3,8 +3,8 @@ "version": 20002, "index": { "name": "/index-v2.json", - "sha256": "a3c7e88a522a7228937e5c3d760fc239e3578e292035d88478d32fec9ff5eb54", - "size": 52314, + "sha256": "f4979b9db840cb51a99e80c20da676ba42b13133dbaa4819673bc43ed2ffc3f3", + "size": 52481, "numPackages": 10 }, "diffs": {} diff --git a/tests/repo/index-v2.json b/tests/repo/index-v2.json index b59f17bf..fed40fb9 100644 --- a/tests/repo/index-v2.json +++ b/tests/repo/index-v2.json @@ -568,7 +568,9 @@ ] }, "antiFeatures": { - "NoSourceSince": {}, + "NoSourceSince": { + "en-US": "1.5" + }, "NonFreeNet": {} } }, @@ -602,7 +604,9 @@ ] }, "antiFeatures": { - "NoSourceSince": {}, + "NoSourceSince": { + "en-US": "1.5" + }, "NonFreeNet": {} } }, @@ -645,7 +649,9 @@ ] }, "antiFeatures": { - "NoSourceSince": {}, + "NoSourceSince": { + "en-US": "1.5" + }, "NonFreeNet": {} } }, @@ -689,7 +695,9 @@ }, "antiFeatures": { "KnownVuln": {}, - "NoSourceSince": {}, + "NoSourceSince": { + "en-US": "1.5" + }, "NonFreeAssets": {}, "NonFreeNet": {}, "UpstreamNonFree": {} @@ -1395,4 +1403,4 @@ } } } -} +} \ No newline at end of file diff --git a/tests/rewritemeta.TestCase b/tests/rewritemeta.TestCase index 0bcc704e..9d20550f 100755 --- a/tests/rewritemeta.TestCase +++ b/tests/rewritemeta.TestCase @@ -45,11 +45,11 @@ class RewriteMetaTest(unittest.TestCase): 'versionCode': 3, 'commit': '6a548e4b19', 'target': 'android-10', - 'antifeatures': [ - 'KnownVuln', - 'UpstreamNonFree', - 'NonFreeAssets', - ], + 'antifeatures': { + 'KnownVuln': {}, + 'UpstreamNonFree': {}, + 'NonFreeAssets': {}, + }, }, ) @@ -68,6 +68,24 @@ class RewriteMetaTest(unittest.TestCase): }, ) + def test_remove_blank_flags_from_builds_org_adaway_52(self): + """Unset fields in Builds: entries should be removed.""" + appid = 'org.adaway' + app = metadata.read_metadata({appid: -1})[appid] + builds = rewritemeta.remove_blank_flags_from_builds(app.get('Builds')) + self.assertEqual( + builds[-1], + { + 'buildjni': ['yes'], + 'commit': 'v3.0', + 'gradle': ['yes'], + 'preassemble': ['renameExecutables'], + 'subdir': 'AdAway', + 'versionCode': 52, + 'versionName': '3.0', + }, + ) + def test_remove_blank_flags_from_builds_no_builds(self): """Unset fields in Builds: entries should be removed.""" self.assertEqual( @@ -78,6 +96,43 @@ class RewriteMetaTest(unittest.TestCase): rewritemeta.remove_blank_flags_from_builds(dict()), list(), ) + self.assertEqual( + rewritemeta.remove_blank_flags_from_builds(list()), + list(), + ) + self.assertEqual( + rewritemeta.remove_blank_flags_from_builds(set()), + list(), + ) + self.assertEqual( + rewritemeta.remove_blank_flags_from_builds(tuple()), + list(), + ) + + def test_remove_blank_flags_from_builds_0_is_a_value(self): + self.assertEqual( + rewritemeta.remove_blank_flags_from_builds([{'versionCode': 0}]), + [{'versionCode': 0}], + ) + + def test_remove_blank_flags_from_builds_values_to_purge(self): + self.assertEqual( + rewritemeta.remove_blank_flags_from_builds( + [ + { + 'antifeatures': dict(), + 'forceversion': False, + 'init': None, + 'rm': '', + 'scandelete': list(), + 'versionCode': 0, + }, + {'antifeatures': list(), 'versionCode': 1}, + {'antifeatures': '', 'versionCode': 2}, + ] + ), + [{'versionCode': 0}, {'versionCode': 1}, {'versionCode': 2}], + ) def test_rewrite_no_builds(self): os.chdir(self.testdir) @@ -144,6 +199,30 @@ class RewriteMetaTest(unittest.TestCase): }, ) + def test_remove_blank_flags_from_builds_app_with_special_build_params_af(self): + """Unset fields in Builds: entries should be removed.""" + appid = 'app.with.special.build.params' + app = metadata.read_metadata({appid: -1})[appid] + builds = rewritemeta.remove_blank_flags_from_builds(app.get('Builds')) + self.assertEqual( + builds[-2], + { + 'antifeatures': { + 'Ads': {'en-US': 'includes ad lib\n', 'zh-CN': '包括广告图书馆\n'}, + 'Tracking': {'en-US': 'standard suspects\n'}, + }, + 'commit': '2.1.1', + 'maven': '2', + 'patch': [ + 'manifest-ads.patch', + 'mobilecore.patch', + ], + 'srclibs': ['FacebookSDK@sdk-version-3.0.2'], + 'versionCode': 50, + 'versionName': '2.1.1-c', + }, + ) + def test_rewrite_scenario_trivial(self): sys.argv = ['rewritemeta', 'a', 'b'] diff --git a/tests/update.TestCase b/tests/update.TestCase index b3234711..7c4e4518 100755 --- a/tests/update.TestCase +++ b/tests/update.TestCase @@ -768,7 +768,7 @@ class UpdateTest(unittest.TestCase): '-1': 'res/drawable-mdpi-v4/icon_launcher.png'}) self.assertEqual(apk_info['icons'], {}) self.assertEqual(apk_info['features'], []) - self.assertEqual(apk_info['antiFeatures'], set()) + self.assertEqual(apk_info['antiFeatures'], dict()) self.assertEqual(apk_info['versionName'], 'v1.6pre2') self.assertEqual(apk_info['hash'], '897486e1f857c6c0ee32ccbad0e1b8cd82f6d0e65a44a23f13f852d2b63a18c8') @@ -819,7 +819,7 @@ class UpdateTest(unittest.TestCase): 'hashType': 'sha256', 'packageName': 'no.min.target.sdk', 'features': [], - 'antiFeatures': set(), + 'antiFeatures': dict(), 'size': 14102, 'sig': 'b4964fd759edaa54e65bb476d0276880', 'versionName': '1.2-fake', @@ -1433,7 +1433,7 @@ class UpdateTest(unittest.TestCase): 'NoSourceSince': '', 'Repo': '', 'RepoType': '', - 'RequiresRoot': '', + 'RequiresRoot': None, 'SourceCode': '', 'Summary': 'rocks.janicerand', 'Translation': '',