blob: 7a06efc5c22055b45414674180ca8b056a674179 [file] [log] [blame]
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
This script is used to convert a list of references to corresponding ProGuard
keep rules, for the purposes of maintaining compatibility between async DFMs
and synchronously proguarded modules.
This script take an input file generated from
//build/android/bytecode/java/org/chromium/bytecode/ByteCodeProcessor.java
during the build phase of an async module.
"""
from collections import defaultdict
import argparse
import sys
# Classes in _IGNORED_PACKAGES do not need explicit keep rules because they are
# system APIs and are already included in ProGuard configs.
_IGNORED_PACKAGES = ['java', 'android', 'org.w3c', 'org.xml', 'dalvik']
# Mapping for translating Java bytecode type identifiers to source code type
# identifiers.
_TYPE_IDENTIFIER_MAP = {
'V': 'void',
'Z': 'boolean',
'B': 'byte',
'S': 'short',
'C': 'char',
'I': 'int',
'J': 'long',
'F': 'float',
'D': 'double',
}
# Translates DEX TypeDescriptor of the first type found in a given string to
# its source code type identifier, as described in
# https://source.android.com/devices/tech/dalvik/dex-format#typedescriptor,
# and returns the translated type and the starting index of the next type
# (if present).
def translate_single_type(typedesc):
array_count = 0
translated = ''
next_index = 0
# In the constant pool, fully qualified names (prefixed by 'L') have a
# trailing ';' if they are describing the type/return type of a symbol,
# or the type of arguments passed to a symbol. TypeDescriptor representing
# primitive types do not have trailing ';'s in any circumstances.
for i, c in enumerate(typedesc):
if c == '[':
array_count += 1
continue
if c == 'L':
# Fully qualified names have no trailing ';' if they are describing the
# containing class of a reference.
next_index = typedesc.find(';')
if next_index == -1:
next_index = len(typedesc)
translated = typedesc[i + 1:next_index]
break
else:
translated = _TYPE_IDENTIFIER_MAP[c]
next_index = i
break
translated += '[]' * array_count
return translated, next_index + 1
# Convert string of method argument types read from constant pool to
# corresponding list of srouce code type identifiers.
def parse_args_list(args_list):
parsed_args = []
start_index = 0
while start_index < len(args_list):
args_list = args_list[start_index:]
translated_arg, start_index = translate_single_type(args_list)
parsed_args.append(translated_arg)
return parsed_args
def add_to_refs(class_name, keep_entry, dep_refs):
# Add entry to class's keep rule if entry is not the empty string
if class_name in dep_refs and keep_entry:
dep_refs[class_name].append(keep_entry)
else:
dep_refs[class_name] = [keep_entry]
def main(argv):
dep_refs = defaultdict(list)
extended_and_implemented_classes = set()
parser = argparse.ArgumentParser()
parser.add_argument(
'--input-file',
required=True,
help='Path to constant pool reference output.')
parser.add_argument(
'--output-file',
required=True,
help='Path to write corresponding keep rules to')
args = parser.parse_args(argv[1:])
with open(args.input_file, 'r') as constant_pool_refs:
for line in constant_pool_refs:
line = line.rstrip().replace('/', '.')
# Ignore any references specified by the list of _IGNORED_PACKAGES.
if any(line.startswith(package) for package in _IGNORED_PACKAGES):
continue
reflist = line.split(',')
# Lines denoting super classes and implemented interface references do
# not contain additional information and thus have reflist size 1.
# Store these as a separate set as they require full keep rules.
if len(reflist) == 1:
extended_and_implemented_classes.add(reflist[0])
continue
class_name = reflist[0]
member_name = reflist[1]
member_info = reflist[2]
keep_entry = ''
# When testing with the VR module, all class names read from constant
# pool output that were prefixed with '[' matched references to the
# overridden clone() method of the Object class. These seem to correspond
# to Java enum types defined within classes.
# It is not entirely clear whether or not this always represents
# an enum, why enums would be represented as such in the constant pool,
# or how we should go about keeping these references. For the moment,
# ignoring these references does not impact compatibility between
# modules.
if class_name.startswith('['):
continue
# If member_info starts with '(', member is a method, otherwise member
# is a field.
# Format keep entries as per ProGuard documentation
# guardsquare.com/en/products/proguard/manual/usage#classspecification.
if member_info.startswith('('):
args_list, return_type = member_info.split(')')
args_list = parse_args_list(args_list[1:])
if member_name == '<init>':
# No return type specified for constructors.
return_type = ''
else:
return_type = translate_single_type(return_type)[0]
keep_entry = '%s %s(%s);' % (return_type, member_name,
', '.join(args_list))
else:
keep_entry = '%s %s;' % (translate_single_type(member_info)[0],
member_name)
dep_refs[class_name].append(keep_entry)
with open(args.output_file, 'w') as keep_rules:
# Write super classes and implemented interfaces to keep rules.
for super_class in sorted(extended_and_implemented_classes):
keep_rules.write(
'-keep,allowobfuscation class %s { *; }\n' % (super_class.rstrip()))
keep_rules.write('\n')
# Write all other class references to keep rules.
for c in sorted(dep_refs.iterkeys()):
class_keeps = '\n '.join(dep_refs[c])
keep_rules.write(
'-keep,allowobfuscation class %s {\n %s\n}\n' % (c, class_keeps))
keep_rules.write('\n')
if __name__ == '__main__':
main(sys.argv)