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 import java.util.TreeMap; 29 30 /** 31 * A representation of a proguard mapping for deobfuscating class names, 32 * field names, and stack frames. 33 */ 34 public class ProguardMap { 35 36 private static final String ARRAY_SYMBOL = "[]"; 37 38 private static class FrameData { FrameData(String clearMethodName)39 public FrameData(String clearMethodName) { 40 this.clearMethodName = clearMethodName; 41 } 42 43 private final String clearMethodName; 44 private final TreeMap<Integer, LineNumber> lineNumbers = new TreeMap<>(); 45 getClearLine(int obfuscatedLine)46 public int getClearLine(int obfuscatedLine) { 47 Map.Entry<Integer, LineNumber> lineNumberEntry = lineNumbers.floorEntry(obfuscatedLine); 48 LineNumber lineNumber = lineNumberEntry == null ? null : lineNumberEntry.getValue(); 49 if (lineNumber != null 50 && obfuscatedLine >= lineNumber.obfuscatedLineStart 51 && obfuscatedLine <= lineNumber.obfuscatedLineEnd) { 52 return lineNumber.clearLineStart + obfuscatedLine - lineNumber.obfuscatedLineStart; 53 } else { 54 return obfuscatedLine; 55 } 56 } 57 } 58 59 private static class LineNumber { LineNumber(int obfuscatedLineStart, int obfuscatedLineEnd, int clearLineStart)60 public LineNumber(int obfuscatedLineStart, int obfuscatedLineEnd, int clearLineStart) { 61 this.obfuscatedLineStart = obfuscatedLineStart; 62 this.obfuscatedLineEnd = obfuscatedLineEnd; 63 this.clearLineStart = clearLineStart; 64 } 65 66 private final int obfuscatedLineStart; 67 private final int obfuscatedLineEnd; 68 private final int clearLineStart; 69 } 70 71 private static class ClassData { 72 private final String mClearName; 73 74 // Mapping from obfuscated field name to clear field name. 75 private final Map<String, String> mFields = new HashMap<String, String>(); 76 77 // obfuscatedMethodName + clearSignature -> FrameData 78 private final Map<String, FrameData> mFrames = new HashMap<String, FrameData>(); 79 80 // Constructs a ClassData object for a class with the given clear name. ClassData(String clearName)81 public ClassData(String clearName) { 82 mClearName = clearName; 83 } 84 85 // Returns the clear name of the class. getClearName()86 public String getClearName() { 87 return mClearName; 88 } 89 addField(String obfuscatedName, String clearName)90 public void addField(String obfuscatedName, String clearName) { 91 mFields.put(obfuscatedName, clearName); 92 } 93 94 // Get the clear name for the field in this class with the given 95 // obfuscated name. Returns the original obfuscated name if a clear 96 // name for the field could not be determined. 97 // TODO: Do we need to take into account the type of the field to 98 // propery determine the clear name? getField(String obfuscatedName)99 public String getField(String obfuscatedName) { 100 String clearField = mFields.get(obfuscatedName); 101 return clearField == null ? obfuscatedName : clearField; 102 } 103 addFrame(String obfuscatedMethodName, String clearMethodName, String clearSignature, int obfuscatedLine, int obfuscatedLineEnd, int clearLine)104 public void addFrame(String obfuscatedMethodName, String clearMethodName, 105 String clearSignature, int obfuscatedLine, int obfuscatedLineEnd, int clearLine) { 106 String key = obfuscatedMethodName + clearSignature; 107 FrameData data = mFrames.get(key); 108 if (data == null) { 109 data = new FrameData(clearMethodName); 110 } 111 data.lineNumbers.put( 112 obfuscatedLine, new LineNumber(obfuscatedLine, obfuscatedLineEnd, clearLine)); 113 mFrames.put(key, data); 114 } 115 getFrame(String clearClassName, String obfuscatedMethodName, String clearSignature, String obfuscatedFilename, int obfuscatedLine)116 public Frame getFrame(String clearClassName, String obfuscatedMethodName, 117 String clearSignature, String obfuscatedFilename, int obfuscatedLine) { 118 String key = obfuscatedMethodName + clearSignature; 119 FrameData frame = mFrames.get(key); 120 if (frame == null) { 121 frame = new FrameData(obfuscatedMethodName); 122 } 123 return new Frame(frame.clearMethodName, clearSignature, 124 getFileName(clearClassName), frame.getClearLine(obfuscatedLine)); 125 } 126 } 127 128 private Map<String, ClassData> mClassesFromClearName = new HashMap<String, ClassData>(); 129 private Map<String, ClassData> mClassesFromObfuscatedName = new HashMap<String, ClassData>(); 130 131 /** 132 * Information associated with a stack frame that identifies a particular 133 * line of source code. 134 */ 135 public static class Frame { Frame(String method, String signature, String filename, int line)136 Frame(String method, String signature, String filename, int line) { 137 this.method = method; 138 this.signature = signature; 139 this.filename = filename; 140 this.line = line; 141 } 142 143 /** 144 * The name of the method the stack frame belongs to. 145 * For example, "equals". 146 */ 147 public final String method; 148 149 /** 150 * The signature of the method the stack frame belongs to. 151 * For example, "(Ljava/lang/Object;)Z". 152 */ 153 public final String signature; 154 155 /** 156 * The name of the file with containing the line of source that the stack 157 * frame refers to. 158 */ 159 public final String filename; 160 161 /** 162 * The line number of the code in the source file that the stack frame 163 * refers to. 164 */ 165 public final int line; 166 } 167 parseException(String msg)168 private static void parseException(String msg) throws ParseException { 169 throw new ParseException(msg, 0); 170 } 171 172 /** 173 * Creates a new empty proguard mapping. 174 * The {@link #readFromFile readFromFile} and 175 * {@link #readFromReader readFromReader} methods can be used to populate 176 * the proguard mapping with proguard mapping information. 177 */ ProguardMap()178 public ProguardMap() { 179 } 180 181 /** 182 * Adds the proguard mapping information in <code>mapFile</code> to this 183 * proguard mapping. 184 * The <code>mapFile</code> should be a proguard mapping file generated with 185 * the <code>-printmapping</code> option when proguard was run. 186 * 187 * @param mapFile the name of a file with proguard mapping information 188 * @throws FileNotFoundException If the <code>mapFile</code> could not be 189 * found 190 * @throws IOException If an input exception occurred. 191 * @throws ParseException If the <code>mapFile</code> is not a properly 192 * formatted proguard mapping file. 193 */ readFromFile(File mapFile)194 public void readFromFile(File mapFile) 195 throws FileNotFoundException, IOException, ParseException { 196 readFromReader(new FileReader(mapFile)); 197 } 198 199 /** 200 * Adds the proguard mapping information read from <code>mapReader</code> to 201 * this proguard mapping. 202 * <code>mapReader</code> should be a Reader of a proguard mapping file 203 * generated with the <code>-printmapping</code> option when proguard was run. 204 * 205 * @param mapReader a Reader for reading the proguard mapping information 206 * @throws IOException If an input exception occurred. 207 * @throws ParseException If the <code>mapFile</code> is not a properly 208 * formatted proguard mapping file. 209 */ readFromReader(Reader mapReader)210 public void readFromReader(Reader mapReader) throws IOException, ParseException { 211 BufferedReader reader = new BufferedReader(mapReader); 212 String line = reader.readLine(); 213 while (line != null) { 214 // Comment lines start with '#'. Skip over them. 215 if (line.startsWith("#")) { 216 line = reader.readLine(); 217 continue; 218 } 219 220 // Class lines are of the form: 221 // 'clear.class.name -> obfuscated_class_name:' 222 int sep = line.indexOf(" -> "); 223 if (sep == -1 || sep + 5 >= line.length()) { 224 parseException("Error parsing class line: '" + line + "'"); 225 } 226 String clearClassName = line.substring(0, sep); 227 String obfuscatedClassName = line.substring(sep + 4, line.length() - 1); 228 229 ClassData classData = new ClassData(clearClassName); 230 mClassesFromClearName.put(clearClassName, classData); 231 mClassesFromObfuscatedName.put(obfuscatedClassName, classData); 232 233 // After the class line comes zero or more field/method lines of the form: 234 // ' type clearName -> obfuscatedName' 235 // '# comment line' 236 line = reader.readLine(); 237 while (line != null && (line.startsWith(" ") || line.startsWith("#"))) { 238 // Comment lines start with '#' and may occur anywhere in the file. 239 // Skip over them. 240 if (line.startsWith("#")) { 241 line = reader.readLine(); 242 continue; 243 } 244 String trimmed = line.trim(); 245 int ws = trimmed.indexOf(' '); 246 sep = trimmed.indexOf(" -> "); 247 if (ws == -1 || sep == -1) { 248 parseException("Error parse field/method line: '" + line + "'"); 249 } 250 251 String type = trimmed.substring(0, ws); 252 String clearName = trimmed.substring(ws + 1, sep); 253 String obfuscatedName = trimmed.substring(sep + 4, trimmed.length()); 254 255 // If the clearName contains '(', then this is for a method instead of a 256 // field. 257 if (clearName.indexOf('(') == -1) { 258 classData.addField(obfuscatedName, clearName); 259 } else { 260 // For methods, the type is of the form: [#:[#:]]<returnType> 261 int obfuscatedLine = 0; 262 // The end of the obfuscated line range. 263 // If line does not contain explicit end range, e.g #:, it is equivalent to #:#: 264 int obfuscatedLineEnd = 0; 265 int colon = type.indexOf(':'); 266 if (colon != -1) { 267 obfuscatedLine = Integer.parseInt(type.substring(0, colon)); 268 obfuscatedLineEnd = obfuscatedLine; 269 type = type.substring(colon + 1); 270 } 271 colon = type.indexOf(':'); 272 if (colon != -1) { 273 obfuscatedLineEnd = Integer.parseInt(type.substring(0, colon)); 274 type = type.substring(colon + 1); 275 } 276 277 // For methods, the clearName is of the form: <clearName><sig>[:#[:#]] 278 int op = clearName.indexOf('('); 279 int cp = clearName.indexOf(')'); 280 if (op == -1 || cp == -1) { 281 parseException("Error parse method line: '" + line + "'"); 282 } 283 284 String sig = clearName.substring(op, cp + 1); 285 286 int clearLine = obfuscatedLine; 287 colon = clearName.lastIndexOf(':'); 288 if (colon != -1) { 289 clearLine = Integer.parseInt(clearName.substring(colon + 1)); 290 clearName = clearName.substring(0, colon); 291 } 292 293 colon = clearName.lastIndexOf(':'); 294 if (colon != -1) { 295 clearLine = Integer.parseInt(clearName.substring(colon + 1)); 296 clearName = clearName.substring(0, colon); 297 } 298 299 clearName = clearName.substring(0, op); 300 301 String clearSig = fromProguardSignature(sig + type); 302 classData.addFrame(obfuscatedName, clearName, clearSig, 303 obfuscatedLine, obfuscatedLineEnd, clearLine); 304 } 305 306 line = reader.readLine(); 307 } 308 } 309 reader.close(); 310 } 311 312 /** 313 * Returns the deobfuscated version of the given obfuscated class name. 314 * If this proguard mapping does not include information about how to 315 * deobfuscate the obfuscated class name, the obfuscated class name 316 * is returned. 317 * 318 * @param obfuscatedClassName the obfuscated class name to deobfuscate 319 * @return the deobfuscated class name. 320 */ getClassName(String obfuscatedClassName)321 public String getClassName(String obfuscatedClassName) { 322 // Class names for arrays may have trailing [] that need to be 323 // stripped before doing the lookup. 324 String baseName = obfuscatedClassName; 325 String arraySuffix = ""; 326 while (baseName.endsWith(ARRAY_SYMBOL)) { 327 arraySuffix += ARRAY_SYMBOL; 328 baseName = baseName.substring(0, baseName.length() - ARRAY_SYMBOL.length()); 329 } 330 331 ClassData classData = mClassesFromObfuscatedName.get(baseName); 332 String clearBaseName = classData == null ? baseName : classData.getClearName(); 333 return clearBaseName + arraySuffix; 334 } 335 336 /** 337 * Returns the deobfuscated version of the obfuscated field name for the 338 * given deobfuscated class name. 339 * If this proguard mapping does not include information about how to 340 * deobfuscate the obfuscated field name, the obfuscated field name is 341 * returned. 342 * 343 * @param clearClass the deobfuscated name of the class the field belongs to 344 * @param obfuscatedField the obfuscated field name to deobfuscate 345 * @return the deobfuscated field name. 346 */ getFieldName(String clearClass, String obfuscatedField)347 public String getFieldName(String clearClass, String obfuscatedField) { 348 ClassData classData = mClassesFromClearName.get(clearClass); 349 if (classData == null) { 350 return obfuscatedField; 351 } 352 return classData.getField(obfuscatedField); 353 } 354 355 /** 356 * Returns the deobfuscated version of the obfuscated stack frame 357 * information for the given deobfuscated class name. 358 * If this proguard mapping does not include information about how to 359 * deobfuscate the obfuscated stack frame information, the obfuscated stack 360 * frame information is returned. 361 * 362 * @param clearClassName the deobfuscated name of the class the stack frame's 363 * method belongs to 364 * @param obfuscatedMethodName the obfuscated method name to deobfuscate 365 * @param obfuscatedSignature the obfuscated method signature to deobfuscate 366 * @param obfuscatedFilename the obfuscated file name to deobfuscate. 367 * @param obfuscatedLine the obfuscated line number to deobfuscate. 368 * @return the deobfuscated stack frame information. 369 */ getFrame(String clearClassName, String obfuscatedMethodName, String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine)370 public Frame getFrame(String clearClassName, String obfuscatedMethodName, 371 String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine) { 372 String clearSignature = getSignature(obfuscatedSignature); 373 ClassData classData = mClassesFromClearName.get(clearClassName); 374 if (classData == null) { 375 return new Frame(obfuscatedMethodName, clearSignature, 376 obfuscatedFilename, obfuscatedLine); 377 } 378 return classData.getFrame(clearClassName, obfuscatedMethodName, clearSignature, 379 obfuscatedFilename, obfuscatedLine); 380 } 381 382 // Converts a proguard-formatted method signature into a Java formatted 383 // method signature. fromProguardSignature(String sig)384 private static String fromProguardSignature(String sig) throws ParseException { 385 if (sig.startsWith("(")) { 386 int end = sig.indexOf(')'); 387 if (end == -1) { 388 parseException("Error parsing signature: " + sig); 389 } 390 391 StringBuilder converted = new StringBuilder(); 392 converted.append('('); 393 if (end > 1) { 394 for (String arg : sig.substring(1, end).split(",")) { 395 converted.append(fromProguardSignature(arg)); 396 } 397 } 398 converted.append(')'); 399 converted.append(fromProguardSignature(sig.substring(end + 1))); 400 return converted.toString(); 401 } else if (sig.endsWith(ARRAY_SYMBOL)) { 402 return "[" + fromProguardSignature(sig.substring(0, sig.length() - 2)); 403 } else if (sig.equals("boolean")) { 404 return "Z"; 405 } else if (sig.equals("byte")) { 406 return "B"; 407 } else if (sig.equals("char")) { 408 return "C"; 409 } else if (sig.equals("short")) { 410 return "S"; 411 } else if (sig.equals("int")) { 412 return "I"; 413 } else if (sig.equals("long")) { 414 return "J"; 415 } else if (sig.equals("float")) { 416 return "F"; 417 } else if (sig.equals("double")) { 418 return "D"; 419 } else if (sig.equals("void")) { 420 return "V"; 421 } else { 422 return "L" + sig.replace('.', '/') + ";"; 423 } 424 } 425 426 // Return a clear signature for the given obfuscated signature. getSignature(String obfuscatedSig)427 private String getSignature(String obfuscatedSig) { 428 StringBuilder builder = new StringBuilder(); 429 for (int i = 0; i < obfuscatedSig.length(); i++) { 430 if (obfuscatedSig.charAt(i) == 'L') { 431 int e = obfuscatedSig.indexOf(';', i); 432 builder.append('L'); 433 String cls = obfuscatedSig.substring(i + 1, e).replace('/', '.'); 434 builder.append(getClassName(cls).replace('.', '/')); 435 builder.append(';'); 436 i = e; 437 } else { 438 builder.append(obfuscatedSig.charAt(i)); 439 } 440 } 441 return builder.toString(); 442 } 443 444 // Return a file name for the given clear class name. getFileName(String clearClass)445 private static String getFileName(String clearClass) { 446 String filename = clearClass; 447 int dot = filename.lastIndexOf('.'); 448 if (dot != -1) { 449 filename = filename.substring(dot + 1); 450 } 451 452 int dollar = filename.indexOf('$'); 453 if (dollar != -1) { 454 filename = filename.substring(0, dollar); 455 } 456 return filename + ".java"; 457 } 458 } 459