• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import os
6import re
7import tempfile
8
9from devil.utils import cmd_helper
10from pylib import constants
11
12
13_PROGUARD_CLASS_RE = re.compile(r'\s*?- Program class:\s*([\S]+)$')
14_PROGUARD_SUPERCLASS_RE = re.compile(r'\s*?  Superclass:\s*([\S]+)$')
15_PROGUARD_SECTION_RE = re.compile(
16    r'^(Interfaces|Constant Pool|Fields|Methods|Class file attributes) '
17    r'\(count = \d+\):$')
18_PROGUARD_METHOD_RE = re.compile(r'\s*?- Method:\s*(\S*)[(].*$')
19_PROGUARD_ANNOTATION_RE = re.compile(r'^(\s*?)- Annotation \[L(\S*);\]:$')
20_ELEMENT_PRIMITIVE = 0
21_ELEMENT_ARRAY = 1
22_ELEMENT_ANNOTATION = 2
23_PROGUARD_ELEMENT_RES = [
24  (_ELEMENT_PRIMITIVE,
25   re.compile(r'^(\s*?)- Constant element value \[(\S*) .*\]$')),
26  (_ELEMENT_ARRAY,
27   re.compile(r'^(\s*?)- Array element value \[(\S*)\]:$')),
28  (_ELEMENT_ANNOTATION,
29   re.compile(r'^(\s*?)- Annotation element value \[(\S*)\]:$'))
30]
31_PROGUARD_INDENT_WIDTH = 2
32_PROGUARD_ANNOTATION_VALUE_RE = re.compile(r'^(\s*?)- \S+? \[(.*)\]$')
33
34_PROGUARD_PATH_SDK = os.path.join(
35    constants.PROGUARD_ROOT, 'lib', 'proguard.jar')
36_PROGUARD_PATH_BUILT = (
37    os.path.join(os.environ['ANDROID_BUILD_TOP'], 'external', 'proguard',
38                 'lib', 'proguard.jar')
39    if 'ANDROID_BUILD_TOP' in os.environ else None)
40_PROGUARD_PATH = (
41    _PROGUARD_PATH_SDK if os.path.exists(_PROGUARD_PATH_SDK)
42    else _PROGUARD_PATH_BUILT)
43
44
45def Dump(jar_path):
46  """Dumps class and method information from a JAR into a dict via proguard.
47
48  Args:
49    jar_path: An absolute path to the JAR file to dump.
50  Returns:
51    A dict in the following format:
52      {
53        'classes': [
54          {
55            'class': '',
56            'superclass': '',
57            'annotations': {/* dict -- see below */},
58            'methods': [
59              {
60                'method': '',
61                'annotations': {/* dict -- see below */},
62              },
63              ...
64            ],
65          },
66          ...
67        ],
68      }
69
70    Annotations dict format:
71      {
72        'empty-annotation-class-name': None,
73        'annotation-class-name': {
74          'field': 'primitive-value',
75          'field': [ 'array-item-1', 'array-item-2', ... ],
76          'field': {
77            /* Object value */
78            'field': 'primitive-value',
79            'field': [ 'array-item-1', 'array-item-2', ... ],
80            'field': { /* Object value */ }
81          }
82        }
83      }
84
85    Note that for top-level annotations their class names are used for
86    identification, whereas for any nested annotations the corresponding
87    field names are used.
88
89    One drawback of this approach is that an array containing empty
90    annotation classes will be represented as an array of 'None' values,
91    thus it will not be possible to find out annotation class names.
92    On the other hand, storing both annotation class name and the field name
93    would produce a very complex JSON.
94  """
95
96  with tempfile.NamedTemporaryFile() as proguard_output:
97    cmd_helper.GetCmdStatusAndOutput([
98        'java',
99        '-jar', _PROGUARD_PATH,
100        '-injars', jar_path,
101        '-dontshrink', '-dontoptimize', '-dontobfuscate', '-dontpreverify',
102        '-dump', proguard_output.name])
103    return Parse(proguard_output)
104
105class _AnnotationElement(object):
106  def __init__(self, name, ftype, depth):
107    self.ref = None
108    self.name = name
109    self.ftype = ftype
110    self.depth = depth
111
112class _ParseState(object):
113  _INITIAL_VALUES = (lambda: None, list, dict)
114  # Empty annotations are represented as 'None', not as an empty dictionary.
115  _LAZY_INITIAL_VALUES = (lambda: None, list, lambda: None)
116
117  def __init__(self):
118    self._class_result = None
119    self._method_result = None
120    self._parse_annotations = False
121    self._annotation_stack = []
122
123  def ResetPerSection(self, section_name):
124    self.InitMethod(None)
125    self._parse_annotations = (
126      section_name in ['Class file attributes', 'Methods'])
127
128  def ParseAnnotations(self):
129    return self._parse_annotations
130
131  def CreateAndInitClass(self, class_name):
132    self.InitMethod(None)
133    self._class_result = {
134      'class': class_name,
135      'superclass': '',
136      'annotations': {},
137      'methods': [],
138    }
139    return self._class_result
140
141  def HasCurrentClass(self):
142    return bool(self._class_result)
143
144  def SetSuperClass(self, superclass):
145    assert self.HasCurrentClass()
146    self._class_result['superclass'] = superclass
147
148  def InitMethod(self, method_name):
149    self._annotation_stack = []
150    if method_name:
151      self._method_result = {
152        'method': method_name,
153        'annotations': {},
154      }
155      self._class_result['methods'].append(self._method_result)
156    else:
157      self._method_result = None
158
159  def InitAnnotation(self, annotation, depth):
160    if not self._annotation_stack:
161      # Add a fake parent element comprising 'annotations' dictionary,
162      # so we can work uniformly with both top-level and nested annotations.
163      annotations = _AnnotationElement(
164        '<<<top level>>>', _ELEMENT_ANNOTATION, depth - 1)
165      if self._method_result:
166        annotations.ref = self._method_result['annotations']
167      else:
168        annotations.ref = self._class_result['annotations']
169      self._annotation_stack = [annotations]
170    self._BacktrackAnnotationStack(depth)
171    if not self.HasCurrentAnnotation():
172      self._annotation_stack.append(
173        _AnnotationElement(annotation, _ELEMENT_ANNOTATION, depth))
174    self._CreateAnnotationPlaceHolder(self._LAZY_INITIAL_VALUES)
175
176  def HasCurrentAnnotation(self):
177    return len(self._annotation_stack) > 1
178
179  def InitAnnotationField(self, field, field_type, depth):
180    self._BacktrackAnnotationStack(depth)
181    # Create the parent representation, if needed. E.g. annotations
182    # are represented with `None`, not with `{}` until they receive the first
183    # field.
184    self._CreateAnnotationPlaceHolder(self._INITIAL_VALUES)
185    if self._annotation_stack[-1].ftype == _ELEMENT_ARRAY:
186      # Nested arrays are not allowed in annotations.
187      assert not field_type == _ELEMENT_ARRAY
188      # Use array index instead of bogus field name.
189      field = len(self._annotation_stack[-1].ref)
190    self._annotation_stack.append(_AnnotationElement(field, field_type, depth))
191    self._CreateAnnotationPlaceHolder(self._LAZY_INITIAL_VALUES)
192
193  def UpdateCurrentAnnotationFieldValue(self, value, depth):
194    self._BacktrackAnnotationStack(depth)
195    self._InitOrUpdateCurrentField(value)
196
197  def _CreateAnnotationPlaceHolder(self, constructors):
198    assert self.HasCurrentAnnotation()
199    field = self._annotation_stack[-1]
200    if field.ref is None:
201      field.ref = constructors[field.ftype]()
202      self._InitOrUpdateCurrentField(field.ref)
203
204  def _BacktrackAnnotationStack(self, depth):
205    stack = self._annotation_stack
206    while len(stack) > 0 and stack[-1].depth >= depth:
207      stack.pop()
208
209  def _InitOrUpdateCurrentField(self, value):
210    assert self.HasCurrentAnnotation()
211    parent = self._annotation_stack[-2]
212    assert not parent.ref is None
213    # There can be no nested constant element values.
214    assert parent.ftype in [_ELEMENT_ARRAY, _ELEMENT_ANNOTATION]
215    field = self._annotation_stack[-1]
216    if type(value) is str and not field.ftype == _ELEMENT_PRIMITIVE:
217      # The value comes from the output parser via
218      # UpdateCurrentAnnotationFieldValue, and should be a value of a constant
219      # element. If it isn't, just skip it.
220      return
221    if parent.ftype == _ELEMENT_ARRAY and field.name >= len(parent.ref):
222      parent.ref.append(value)
223    else:
224      parent.ref[field.name] = value
225
226
227def _GetDepth(prefix):
228  return len(prefix) // _PROGUARD_INDENT_WIDTH
229
230def Parse(proguard_output):
231  results = {
232    'classes': [],
233  }
234
235  state = _ParseState()
236
237  for line in proguard_output:
238    line = line.strip('\r\n')
239
240    m = _PROGUARD_CLASS_RE.match(line)
241    if m:
242      results['classes'].append(
243        state.CreateAndInitClass(m.group(1).replace('/', '.')))
244      continue
245
246    if not state.HasCurrentClass():
247      continue
248
249    m = _PROGUARD_SUPERCLASS_RE.match(line)
250    if m:
251      state.SetSuperClass(m.group(1).replace('/', '.'))
252      continue
253
254    m = _PROGUARD_SECTION_RE.match(line)
255    if m:
256      state.ResetPerSection(m.group(1))
257      continue
258
259    m = _PROGUARD_METHOD_RE.match(line)
260    if m:
261      state.InitMethod(m.group(1))
262      continue
263
264    if not state.ParseAnnotations():
265      continue
266
267    m = _PROGUARD_ANNOTATION_RE.match(line)
268    if m:
269      # Ignore the annotation package.
270      state.InitAnnotation(m.group(2).split('/')[-1], _GetDepth(m.group(1)))
271      continue
272
273    if state.HasCurrentAnnotation():
274      m = None
275      for (element_type, element_re) in _PROGUARD_ELEMENT_RES:
276        m = element_re.match(line)
277        if m:
278          state.InitAnnotationField(
279            m.group(2), element_type, _GetDepth(m.group(1)))
280          break
281      if m:
282        continue
283      m = _PROGUARD_ANNOTATION_VALUE_RE.match(line)
284      if m:
285        state.UpdateCurrentAnnotationFieldValue(
286          m.group(2), _GetDepth(m.group(1)))
287      else:
288        state.InitMethod(None)
289
290
291  return results
292