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