summaryrefslogtreecommitdiffstats
path: root/chromium/build/android/constant_pool_refs_to_keep_rules.py
blob: b2452d7261ef0325aafe9418eaceb7e65958a15f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# 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']

# Classes in _WHITELIST_PACKAGES are support libraries compiled into chrome
# that must bypass the _IGNORED_PACKAGES.
_WHITELIST_PACKAGES = ['android.support']

# TODO(https://crbug.com/968769): Filter may be too broad.
# Classes in _DFM_FEATURES will be excluded from "keep all members" rule.
_DFM_FEATURES = [
    'org.chromium.chrome.autofill_assistant', 'org.chromium.chrome.tab_ui',
    'org.chromium.chrome.browser.tasks.tab_management', 'org.chromium.chrome.vr'
]

# 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 should_include_class_path(class_path):
  """ Check whether a class_path should be added as keep rule.
      Conditions:
        - Class is auto-generated (Lambdas/Nested, for example $)
        - Class is not in a DFM Module
        - Class is not in a black/white listed package
    """
  nested_class = '$' in class_path
  not_in_dfm = all(not class_path.startswith(f) for f in _DFM_FEATURES)
  allowed_packages = not (any(
      class_path.startswith(p)
      for p in _IGNORED_PACKAGES) and all(not class_path.startswith(p)
                                          for p in _WHITELIST_PACKAGES))
  return nested_class or (not_in_dfm and allowed_packages)


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 and not in _WHITELIST_PACKAGES.
      if (any(line.startswith(p) for p in _IGNORED_PACKAGES)
          and all(not line.startswith(p) for p in _WHITELIST_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

      # Ignore R(esources) files that are from the same module.
      if ('$' in class_name
          and any(class_name.startswith(f) for f in _DFM_FEATURES)):
        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]

        # Include types of function arguments.
        for arg_type in args_list:
          if should_include_class_path(arg_type):
            extended_and_implemented_classes.add(arg_type)

        # Include the actual class when it's a constructor.
        if member_name == '<init>':
          if should_include_class_path(class_name):
            extended_and_implemented_classes.add(class_name)
          continue

        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 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()):
      if c in extended_and_implemented_classes:
        continue
      class_keeps = '\n  '.join(dep_refs[c])
      keep_rules.write('-keep class %s {\n  %s\n}\n' % (c, class_keeps))
      keep_rules.write('\n')


if __name__ == '__main__':
  main(sys.argv)