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