1 /* 2 * Copyright (C) 2016 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ahat.proguard; 18 19 import java.io.BufferedReader; 20 import java.io.File; 21 import java.io.FileNotFoundException; 22 import java.io.FileReader; 23 import java.io.IOException; 24 import java.io.Reader; 25 import java.text.ParseException; 26 import java.util.HashMap; 27 import java.util.Map; 28 29 /** 30 * A representation of a proguard mapping for deobfuscating class names, 31 * field names, and stack frames. 32 */ 33 public class ProguardMap { 34 35 private static final String ARRAY_SYMBOL = "[]"; 36 37 private static class FrameData { FrameData(String clearMethodName, int lineDelta)38 public FrameData(String clearMethodName, int lineDelta) { 39 this.clearMethodName = clearMethodName; 40 this.lineDelta = lineDelta; 41 } 42 43 public final String clearMethodName; 44 public final int lineDelta; // lineDelta = obfuscatedLine - clearLine 45 } 46 47 private static class ClassData { 48 private final String mClearName; 49 50 // Mapping from obfuscated field name to clear field name. 51 private final Map<String, String> mFields = new HashMap<String, String>(); 52 53 // obfuscatedMethodName + clearSignature -> FrameData 54 private final Map<String, FrameData> mFrames = new HashMap<String, FrameData>(); 55 56 // Constructs a ClassData object for a class with the given clear name. ClassData(String clearName)57 public ClassData(String clearName) { 58 mClearName = clearName; 59 } 60 61 // Returns the clear name of the class. getClearName()62 public String getClearName() { 63 return mClearName; 64 } 65 addField(String obfuscatedName, String clearName)66 public void addField(String obfuscatedName, String clearName) { 67 mFields.put(obfuscatedName, clearName); 68 } 69 70 // Get the clear name for the field in this class with the given 71 // obfuscated name. Returns the original obfuscated name if a clear 72 // name for the field could not be determined. 73 // TODO: Do we need to take into account the type of the field to 74 // propery determine the clear name? getField(String obfuscatedName)75 public String getField(String obfuscatedName) { 76 String clearField = mFields.get(obfuscatedName); 77 return clearField == null ? obfuscatedName : clearField; 78 } 79 80 // TODO: Does this properly interpret the meaning of line numbers? Is 81 // it possible to have multiple frame entries for the same method 82 // name and signature that differ only by line ranges? addFrame(String obfuscatedMethodName, String clearMethodName, String clearSignature, int obfuscatedLine, int clearLine)83 public void addFrame(String obfuscatedMethodName, String clearMethodName, 84 String clearSignature, int obfuscatedLine, int clearLine) { 85 String key = obfuscatedMethodName + clearSignature; 86 mFrames.put(key, new FrameData(clearMethodName, obfuscatedLine - clearLine)); 87 } 88 getFrame(String clearClassName, String obfuscatedMethodName, String clearSignature, String obfuscatedFilename, int obfuscatedLine)89 public Frame getFrame(String clearClassName, String obfuscatedMethodName, 90 String clearSignature, String obfuscatedFilename, int obfuscatedLine) { 91 String key = obfuscatedMethodName + clearSignature; 92 FrameData frame = mFrames.get(key); 93 if (frame == null) { 94 frame = new FrameData(obfuscatedMethodName, 0); 95 } 96 return new Frame(frame.clearMethodName, clearSignature, 97 getFileName(clearClassName), obfuscatedLine - frame.lineDelta); 98 } 99 } 100 101 private Map<String, ClassData> mClassesFromClearName = new HashMap<String, ClassData>(); 102 private Map<String, ClassData> mClassesFromObfuscatedName = new HashMap<String, ClassData>(); 103 104 /** 105 * Information associated with a stack frame that identifies a particular 106 * line of source code. 107 */ 108 public static class Frame { Frame(String method, String signature, String filename, int line)109 Frame(String method, String signature, String filename, int line) { 110 this.method = method; 111 this.signature = signature; 112 this.filename = filename; 113 this.line = line; 114 } 115 116 /** 117 * The name of the method the stack frame belongs to. 118 * For example, "equals". 119 */ 120 public final String method; 121 122 /** 123 * The signature of the method the stack frame belongs to. 124 * For example, "(Ljava/lang/Object;)Z". 125 */ 126 public final String signature; 127 128 /** 129 * The name of the file with containing the line of source that the stack 130 * frame refers to. 131 */ 132 public final String filename; 133 134 /** 135 * The line number of the code in the source file that the stack frame 136 * refers to. 137 */ 138 public final int line; 139 } 140 parseException(String msg)141 private static void parseException(String msg) throws ParseException { 142 throw new ParseException(msg, 0); 143 } 144 145 /** 146 * Creates a new empty proguard mapping. 147 * The {@link #readFromFile readFromFile} and 148 * {@link #readFromReader readFromReader} methods can be used to populate 149 * the proguard mapping with proguard mapping information. 150 */ ProguardMap()151 public ProguardMap() { 152 } 153 154 /** 155 * Adds the proguard mapping information in <code>mapFile</code> to this 156 * proguard mapping. 157 * The <code>mapFile</code> should be a proguard mapping file generated with 158 * the <code>-printmapping</code> option when proguard was run. 159 * 160 * @param mapFile the name of a file with proguard mapping information 161 * @throws FileNotFoundException If the <code>mapFile</code> could not be 162 * found 163 * @throws IOException If an input exception occurred. 164 * @throws ParseException If the <code>mapFile</code> is not a properly 165 * formatted proguard mapping file. 166 */ readFromFile(File mapFile)167 public void readFromFile(File mapFile) 168 throws FileNotFoundException, IOException, ParseException { 169 readFromReader(new FileReader(mapFile)); 170 } 171 172 /** 173 * Adds the proguard mapping information read from <code>mapReader</code> to 174 * this proguard mapping. 175 * <code>mapReader</code> should be a Reader of a proguard mapping file 176 * generated with the <code>-printmapping</code> option when proguard was run. 177 * 178 * @param mapReader a Reader for reading the proguard mapping information 179 * @throws IOException If an input exception occurred. 180 * @throws ParseException If the <code>mapFile</code> is not a properly 181 * formatted proguard mapping file. 182 */ readFromReader(Reader mapReader)183 public void readFromReader(Reader mapReader) throws IOException, ParseException { 184 BufferedReader reader = new BufferedReader(mapReader); 185 String line = reader.readLine(); 186 while (line != null) { 187 // Class lines are of the form: 188 // 'clear.class.name -> obfuscated_class_name:' 189 int sep = line.indexOf(" -> "); 190 if (sep == -1 || sep + 5 >= line.length()) { 191 parseException("Error parsing class line: '" + line + "'"); 192 } 193 String clearClassName = line.substring(0, sep); 194 String obfuscatedClassName = line.substring(sep + 4, line.length() - 1); 195 196 ClassData classData = new ClassData(clearClassName); 197 mClassesFromClearName.put(clearClassName, classData); 198 mClassesFromObfuscatedName.put(obfuscatedClassName, classData); 199 200 // After the class line comes zero or more field/method lines of the form: 201 // ' type clearName -> obfuscatedName' 202 line = reader.readLine(); 203 while (line != null && line.startsWith(" ")) { 204 String trimmed = line.trim(); 205 int ws = trimmed.indexOf(' '); 206 sep = trimmed.indexOf(" -> "); 207 if (ws == -1 || sep == -1) { 208 parseException("Error parse field/method line: '" + line + "'"); 209 } 210 211 String type = trimmed.substring(0, ws); 212 String clearName = trimmed.substring(ws + 1, sep); 213 String obfuscatedName = trimmed.substring(sep + 4, trimmed.length()); 214 215 // If the clearName contains '(', then this is for a method instead of a 216 // field. 217 if (clearName.indexOf('(') == -1) { 218 classData.addField(obfuscatedName, clearName); 219 } else { 220 // For methods, the type is of the form: [#:[#:]]<returnType> 221 int obfuscatedLine = 0; 222 int colon = type.indexOf(':'); 223 if (colon != -1) { 224 obfuscatedLine = Integer.parseInt(type.substring(0, colon)); 225 type = type.substring(colon + 1); 226 } 227 colon = type.indexOf(':'); 228 if (colon != -1) { 229 type = type.substring(colon + 1); 230 } 231 232 // For methods, the clearName is of the form: <clearName><sig>[:#[:#]] 233 int op = clearName.indexOf('('); 234 int cp = clearName.indexOf(')'); 235 if (op == -1 || cp == -1) { 236 parseException("Error parse method line: '" + line + "'"); 237 } 238 239 String sig = clearName.substring(op, cp + 1); 240 241 int clearLine = obfuscatedLine; 242 colon = clearName.lastIndexOf(':'); 243 if (colon != -1) { 244 clearLine = Integer.parseInt(clearName.substring(colon + 1)); 245 clearName = clearName.substring(0, colon); 246 } 247 248 colon = clearName.lastIndexOf(':'); 249 if (colon != -1) { 250 clearLine = Integer.parseInt(clearName.substring(colon + 1)); 251 clearName = clearName.substring(0, colon); 252 } 253 254 clearName = clearName.substring(0, op); 255 256 String clearSig = fromProguardSignature(sig + type); 257 classData.addFrame(obfuscatedName, clearName, clearSig, 258 obfuscatedLine, clearLine); 259 } 260 261 line = reader.readLine(); 262 } 263 } 264 reader.close(); 265 } 266 267 /** 268 * Returns the deobfuscated version of the given obfuscated class name. 269 * If this proguard mapping does not include information about how to 270 * deobfuscate the obfuscated class name, the obfuscated class name 271 * is returned. 272 * 273 * @param obfuscatedClassName the obfuscated class name to deobfuscate 274 * @return the deobfuscated class name. 275 */ getClassName(String obfuscatedClassName)276 public String getClassName(String obfuscatedClassName) { 277 // Class names for arrays may have trailing [] that need to be 278 // stripped before doing the lookup. 279 String baseName = obfuscatedClassName; 280 String arraySuffix = ""; 281 while (baseName.endsWith(ARRAY_SYMBOL)) { 282 arraySuffix += ARRAY_SYMBOL; 283 baseName = baseName.substring(0, baseName.length() - ARRAY_SYMBOL.length()); 284 } 285 286 ClassData classData = mClassesFromObfuscatedName.get(baseName); 287 String clearBaseName = classData == null ? baseName : classData.getClearName(); 288 return clearBaseName + arraySuffix; 289 } 290 291 /** 292 * Returns the deobfuscated version of the obfuscated field name for the 293 * given deobfuscated class name. 294 * If this proguard mapping does not include information about how to 295 * deobfuscate the obfuscated field name, the obfuscated field name is 296 * returned. 297 * 298 * @param clearClass the deobfuscated name of the class the field belongs to 299 * @param obfuscatedField the obfuscated field name to deobfuscate 300 * @return the deobfuscated field name. 301 */ getFieldName(String clearClass, String obfuscatedField)302 public String getFieldName(String clearClass, String obfuscatedField) { 303 ClassData classData = mClassesFromClearName.get(clearClass); 304 if (classData == null) { 305 return obfuscatedField; 306 } 307 return classData.getField(obfuscatedField); 308 } 309 310 /** 311 * Returns the deobfuscated version of the obfuscated stack frame 312 * information for the given deobfuscated class name. 313 * If this proguard mapping does not include information about how to 314 * deobfuscate the obfuscated stack frame information, the obfuscated stack 315 * frame information is returned. 316 * 317 * @param clearClassName the deobfuscated name of the class the stack frame's 318 * method belongs to 319 * @param obfuscatedMethodName the obfuscated method name to deobfuscate 320 * @param obfuscatedSignature the obfuscated method signature to deobfuscate 321 * @param obfuscatedFilename the obfuscated file name to deobfuscate. 322 * @param obfuscatedLine the obfuscated line number to deobfuscate. 323 * @return the deobfuscated stack frame information. 324 */ getFrame(String clearClassName, String obfuscatedMethodName, String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine)325 public Frame getFrame(String clearClassName, String obfuscatedMethodName, 326 String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine) { 327 String clearSignature = getSignature(obfuscatedSignature); 328 ClassData classData = mClassesFromClearName.get(clearClassName); 329 if (classData == null) { 330 return new Frame(obfuscatedMethodName, clearSignature, 331 obfuscatedFilename, obfuscatedLine); 332 } 333 return classData.getFrame(clearClassName, obfuscatedMethodName, clearSignature, 334 obfuscatedFilename, obfuscatedLine); 335 } 336 337 // Converts a proguard-formatted method signature into a Java formatted 338 // method signature. fromProguardSignature(String sig)339 private static String fromProguardSignature(String sig) throws ParseException { 340 if (sig.startsWith("(")) { 341 int end = sig.indexOf(')'); 342 if (end == -1) { 343 parseException("Error parsing signature: " + sig); 344 } 345 346 StringBuilder converted = new StringBuilder(); 347 converted.append('('); 348 if (end > 1) { 349 for (String arg : sig.substring(1, end).split(",")) { 350 converted.append(fromProguardSignature(arg)); 351 } 352 } 353 converted.append(')'); 354 converted.append(fromProguardSignature(sig.substring(end + 1))); 355 return converted.toString(); 356 } else if (sig.endsWith(ARRAY_SYMBOL)) { 357 return "[" + fromProguardSignature(sig.substring(0, sig.length() - 2)); 358 } else if (sig.equals("boolean")) { 359 return "Z"; 360 } else if (sig.equals("byte")) { 361 return "B"; 362 } else if (sig.equals("char")) { 363 return "C"; 364 } else if (sig.equals("short")) { 365 return "S"; 366 } else if (sig.equals("int")) { 367 return "I"; 368 } else if (sig.equals("long")) { 369 return "J"; 370 } else if (sig.equals("float")) { 371 return "F"; 372 } else if (sig.equals("double")) { 373 return "D"; 374 } else if (sig.equals("void")) { 375 return "V"; 376 } else { 377 return "L" + sig.replace('.', '/') + ";"; 378 } 379 } 380 381 // Return a clear signature for the given obfuscated signature. getSignature(String obfuscatedSig)382 private String getSignature(String obfuscatedSig) { 383 StringBuilder builder = new StringBuilder(); 384 for (int i = 0; i < obfuscatedSig.length(); i++) { 385 if (obfuscatedSig.charAt(i) == 'L') { 386 int e = obfuscatedSig.indexOf(';', i); 387 builder.append('L'); 388 String cls = obfuscatedSig.substring(i + 1, e).replace('/', '.'); 389 builder.append(getClassName(cls).replace('.', '/')); 390 builder.append(';'); 391 i = e; 392 } else { 393 builder.append(obfuscatedSig.charAt(i)); 394 } 395 } 396 return builder.toString(); 397 } 398 399 // Return a file name for the given clear class name. getFileName(String clearClass)400 private static String getFileName(String clearClass) { 401 String filename = clearClass; 402 int dot = filename.lastIndexOf('.'); 403 if (dot != -1) { 404 filename = filename.substring(dot + 1); 405 } 406 407 int dollar = filename.indexOf('$'); 408 if (dollar != -1) { 409 filename = filename.substring(0, dollar); 410 } 411 return filename + ".java"; 412 } 413 } 414