1 /* 2 * ProGuard -- shrinking, optimization, obfuscation, and preverification 3 * of Java bytecode. 4 * 5 * Copyright (c) 2002-2009 Eric Lafortune (eric (at) graphics.cornell.edu) 6 * 7 * This program is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License as published by the Free 9 * Software Foundation; either version 2 of the License, or (at your option) 10 * any later version. 11 * 12 * This program is distributed in the hope that it will be useful, but WITHOUT 13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 15 * more details. 16 * 17 * You should have received a copy of the GNU General Public License along 18 * with this program; if not, write to the Free Software Foundation, Inc., 19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 */ 21 package proguard.retrace; 22 23 import proguard.classfile.util.ClassUtil; 24 import proguard.obfuscate.*; 25 26 import java.io.*; 27 import java.util.*; 28 import java.util.regex.*; 29 30 31 /** 32 * Tool for de-obfuscating stack traces of applications that were obfuscated 33 * with ProGuard. 34 * 35 * @author Eric Lafortune 36 */ 37 public class ReTrace 38 implements MappingProcessor 39 { 40 private static final String REGEX_OPTION = "-regex"; 41 private static final String VERBOSE_OPTION = "-verbose"; 42 43 // BEGIN android-changed 44 // Use regex from latest version (4.9) because it is 45 // able to handle Android Bugreport format 46 public static final String STACK_TRACE_EXPRESSION = "(?:.*?\\bat\\s+%c.%m\\s*\\(.*?(?::%l)?\\)\\s*)|(?:(?:.*?[:\"]\\s+)?%c(?::.*)?)"; 47 // END android-changed 48 49 private static final String REGEX_CLASS = "\\b(?:[A-Za-z0-9_$]+\\.)*[A-Za-z0-9_$]+\\b"; 50 private static final String REGEX_CLASS_SLASH = "\\b(?:[A-Za-z0-9_$]+/)*[A-Za-z0-9_$]+\\b"; 51 private static final String REGEX_LINE_NUMBER = "\\b[0-9]+\\b"; 52 private static final String REGEX_TYPE = REGEX_CLASS + "(?:\\[\\])*"; 53 private static final String REGEX_MEMBER = "\\b[A-Za-z0-9_$]+\\b"; 54 private static final String REGEX_ARGUMENTS = "(?:" + REGEX_TYPE + "(?:\\s*,\\s*" + REGEX_TYPE + ")*)?"; 55 56 // The class settings. 57 private final String regularExpression; 58 private final boolean verbose; 59 private final File mappingFile; 60 private final File stackTraceFile; 61 62 private Map classMap = new HashMap(); 63 private Map classFieldMap = new HashMap(); 64 private Map classMethodMap = new HashMap(); 65 66 67 /** 68 * Creates a new ReTrace object to process stack traces on the standard 69 * input, based on the given mapping file name. 70 * @param regularExpression the regular expression for parsing the lines in 71 * the stack trace. 72 * @param verbose specifies whether the de-obfuscated stack trace 73 * should be verbose. 74 * @param mappingFile the mapping file that was written out by 75 * ProGuard. 76 */ 77 public ReTrace(String regularExpression, 78 boolean verbose, 79 File mappingFile) 80 { 81 this(regularExpression, verbose, mappingFile, null); 82 } 83 84 85 /** 86 * Creates a new ReTrace object to process a stack trace from the given file, 87 * based on the given mapping file name. 88 * @param regularExpression the regular expression for parsing the lines in 89 * the stack trace. 90 * @param verbose specifies whether the de-obfuscated stack trace 91 * should be verbose. 92 * @param mappingFile the mapping file that was written out by 93 * ProGuard. 94 * @param stackTraceFile the optional name of the file that contains the 95 * stack trace. 96 */ 97 public ReTrace(String regularExpression, 98 boolean verbose, 99 File mappingFile, 100 File stackTraceFile) 101 { 102 this.regularExpression = regularExpression; 103 this.verbose = verbose; 104 this.mappingFile = mappingFile; 105 this.stackTraceFile = stackTraceFile; 106 } 107 108 109 /** 110 * Performs the subsequent ReTrace operations. 111 */ 112 public void execute() throws IOException 113 { 114 // Read the mapping file. 115 MappingReader mappingReader = new MappingReader(mappingFile); 116 mappingReader.pump(this); 117 118 119 StringBuffer expressionBuffer = new StringBuffer(regularExpression.length() + 32); 120 char[] expressionTypes = new char[32]; 121 int expressionTypeCount = 0; 122 int index = 0; 123 while (true) 124 { 125 int nextIndex = regularExpression.indexOf('%', index); 126 if (nextIndex < 0 || 127 nextIndex == regularExpression.length()-1 || 128 expressionTypeCount == expressionTypes.length) 129 { 130 break; 131 } 132 133 expressionBuffer.append(regularExpression.substring(index, nextIndex)); 134 expressionBuffer.append('('); 135 136 char expressionType = regularExpression.charAt(nextIndex + 1); 137 switch(expressionType) 138 { 139 case 'c': 140 expressionBuffer.append(REGEX_CLASS); 141 break; 142 143 case 'C': 144 expressionBuffer.append(REGEX_CLASS_SLASH); 145 break; 146 147 case 'l': 148 expressionBuffer.append(REGEX_LINE_NUMBER); 149 break; 150 151 case 't': 152 expressionBuffer.append(REGEX_TYPE); 153 break; 154 155 case 'f': 156 expressionBuffer.append(REGEX_MEMBER); 157 break; 158 159 case 'm': 160 expressionBuffer.append(REGEX_MEMBER); 161 break; 162 163 case 'a': 164 expressionBuffer.append(REGEX_ARGUMENTS); 165 break; 166 } 167 168 expressionBuffer.append(')'); 169 170 expressionTypes[expressionTypeCount++] = expressionType; 171 172 index = nextIndex + 2; 173 } 174 175 expressionBuffer.append(regularExpression.substring(index)); 176 177 Pattern pattern = Pattern.compile(expressionBuffer.toString()); 178 179 // Read the stack trace file. 180 LineNumberReader reader = 181 new LineNumberReader(stackTraceFile == null ? 182 (Reader)new InputStreamReader(System.in) : 183 (Reader)new BufferedReader(new FileReader(stackTraceFile))); 184 185 186 try 187 { 188 StringBuffer outLine = new StringBuffer(256); 189 List extraOutLines = new ArrayList(); 190 191 String className = null; 192 193 // Read the line in the stack trace. 194 while (true) 195 { 196 String line = reader.readLine(); 197 if (line == null) 198 { 199 break; 200 } 201 202 Matcher matcher = pattern.matcher(line); 203 204 if (matcher.matches()) 205 { 206 int lineNumber = 0; 207 String type = null; 208 String arguments = null; 209 210 // Figure out a class name, line number, type, and 211 // arguments beforehand. 212 for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++) 213 { 214 int startIndex = matcher.start(expressionTypeIndex + 1); 215 if (startIndex >= 0) 216 { 217 String match = matcher.group(expressionTypeIndex + 1); 218 219 char expressionType = expressionTypes[expressionTypeIndex]; 220 switch (expressionType) 221 { 222 case 'c': 223 className = originalClassName(match); 224 break; 225 226 case 'C': 227 className = originalClassName(ClassUtil.externalClassName(match)); 228 break; 229 230 case 'l': 231 lineNumber = Integer.parseInt(match); 232 break; 233 234 case 't': 235 type = originalType(match); 236 break; 237 238 case 'a': 239 arguments = originalArguments(match); 240 break; 241 } 242 } 243 } 244 245 // Actually construct the output line. 246 int lineIndex = 0; 247 248 outLine.setLength(0); 249 extraOutLines.clear(); 250 251 for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++) 252 { 253 int startIndex = matcher.start(expressionTypeIndex + 1); 254 if (startIndex >= 0) 255 { 256 int endIndex = matcher.end(expressionTypeIndex + 1); 257 String match = matcher.group(expressionTypeIndex + 1); 258 259 // Copy a literal piece of input line. 260 outLine.append(line.substring(lineIndex, startIndex)); 261 262 char expressionType = expressionTypes[expressionTypeIndex]; 263 switch (expressionType) 264 { 265 case 'c': 266 className = originalClassName(match); 267 outLine.append(className); 268 break; 269 270 case 'C': 271 className = originalClassName(ClassUtil.externalClassName(match)); 272 outLine.append(ClassUtil.internalClassName(className)); 273 break; 274 275 case 'l': 276 lineNumber = Integer.parseInt(match); 277 outLine.append(match); 278 break; 279 280 case 't': 281 type = originalType(match); 282 outLine.append(type); 283 break; 284 285 case 'f': 286 originalFieldName(className, 287 match, 288 type, 289 outLine, 290 extraOutLines); 291 break; 292 293 case 'm': 294 originalMethodName(className, 295 match, 296 lineNumber, 297 type, 298 arguments, 299 outLine, 300 extraOutLines); 301 break; 302 303 case 'a': 304 arguments = originalArguments(match); 305 outLine.append(arguments); 306 break; 307 } 308 309 // Skip the original element whose processed version 310 // has just been appended. 311 lineIndex = endIndex; 312 } 313 } 314 315 // Copy the last literal piece of input line. 316 outLine.append(line.substring(lineIndex)); 317 318 // Print out the main line. 319 System.out.println(outLine); 320 321 // Print out any additional lines. 322 for (int extraLineIndex = 0; extraLineIndex < extraOutLines.size(); extraLineIndex++) 323 { 324 System.out.println(extraOutLines.get(extraLineIndex)); 325 } 326 } 327 else 328 { 329 // Print out the original line. 330 System.out.println(line); 331 } 332 } 333 } 334 catch (IOException ex) 335 { 336 throw new IOException("Can't read stack trace (" + ex.getMessage() + ")"); 337 } 338 finally 339 { 340 if (stackTraceFile != null) 341 { 342 try 343 { 344 reader.close(); 345 } 346 catch (IOException ex) 347 { 348 // This shouldn't happen. 349 } 350 } 351 } 352 } 353 354 355 /** 356 * Finds the original field name(s), appending the first one to the out 357 * line, and any additional alternatives to the extra lines. 358 */ 359 private void originalFieldName(String className, 360 String obfuscatedFieldName, 361 String type, 362 StringBuffer outLine, 363 List extraOutLines) 364 { 365 int extraIndent = -1; 366 367 // Class name -> obfuscated field names. 368 Map fieldMap = (Map)classFieldMap.get(className); 369 if (fieldMap != null) 370 { 371 // Obfuscated field names -> fields. 372 Set fieldSet = (Set)fieldMap.get(obfuscatedFieldName); 373 if (fieldSet != null) 374 { 375 // Find all matching fields. 376 Iterator fieldInfoIterator = fieldSet.iterator(); 377 while (fieldInfoIterator.hasNext()) 378 { 379 FieldInfo fieldInfo = (FieldInfo)fieldInfoIterator.next(); 380 if (fieldInfo.matches(type)) 381 { 382 // Is this the first matching field? 383 if (extraIndent < 0) 384 { 385 extraIndent = outLine.length(); 386 387 // Append the first original name. 388 if (verbose) 389 { 390 outLine.append(fieldInfo.type).append(' '); 391 } 392 outLine.append(fieldInfo.originalName); 393 } 394 else 395 { 396 // Create an additional line with the proper 397 // indentation. 398 StringBuffer extraBuffer = new StringBuffer(); 399 for (int counter = 0; counter < extraIndent; counter++) 400 { 401 extraBuffer.append(' '); 402 } 403 404 // Append the alternative name. 405 if (verbose) 406 { 407 extraBuffer.append(fieldInfo.type).append(' '); 408 } 409 extraBuffer.append(fieldInfo.originalName); 410 411 // Store the additional line. 412 extraOutLines.add(extraBuffer); 413 } 414 } 415 } 416 } 417 } 418 419 // Just append the obfuscated name if we haven't found any matching 420 // fields. 421 if (extraIndent < 0) 422 { 423 outLine.append(obfuscatedFieldName); 424 } 425 } 426 427 428 /** 429 * Finds the original method name(s), appending the first one to the out 430 * line, and any additional alternatives to the extra lines. 431 */ 432 private void originalMethodName(String className, 433 String obfuscatedMethodName, 434 int lineNumber, 435 String type, 436 String arguments, 437 StringBuffer outLine, 438 List extraOutLines) 439 { 440 int extraIndent = -1; 441 442 // Class name -> obfuscated method names. 443 Map methodMap = (Map)classMethodMap.get(className); 444 if (methodMap != null) 445 { 446 // Obfuscated method names -> methods. 447 Set methodSet = (Set)methodMap.get(obfuscatedMethodName); 448 if (methodSet != null) 449 { 450 // Find all matching methods. 451 Iterator methodInfoIterator = methodSet.iterator(); 452 while (methodInfoIterator.hasNext()) 453 { 454 MethodInfo methodInfo = (MethodInfo)methodInfoIterator.next(); 455 if (methodInfo.matches(lineNumber, type, arguments)) 456 { 457 // Is this the first matching method? 458 if (extraIndent < 0) 459 { 460 extraIndent = outLine.length(); 461 462 // Append the first original name. 463 if (verbose) 464 { 465 outLine.append(methodInfo.type).append(' '); 466 } 467 outLine.append(methodInfo.originalName); 468 if (verbose) 469 { 470 outLine.append('(').append(methodInfo.arguments).append(')'); 471 } 472 } 473 else 474 { 475 // Create an additional line with the proper 476 // indentation. 477 StringBuffer extraBuffer = new StringBuffer(); 478 for (int counter = 0; counter < extraIndent; counter++) 479 { 480 extraBuffer.append(' '); 481 } 482 483 // Append the alternative name. 484 if (verbose) 485 { 486 extraBuffer.append(methodInfo.type).append(' '); 487 } 488 extraBuffer.append(methodInfo.originalName); 489 if (verbose) 490 { 491 extraBuffer.append('(').append(methodInfo.arguments).append(')'); 492 } 493 494 // Store the additional line. 495 extraOutLines.add(extraBuffer); 496 } 497 } 498 } 499 } 500 } 501 502 // Just append the obfuscated name if we haven't found any matching 503 // methods. 504 if (extraIndent < 0) 505 { 506 outLine.append(obfuscatedMethodName); 507 } 508 } 509 510 511 /** 512 * Returns the original argument types. 513 */ 514 private String originalArguments(String obfuscatedArguments) 515 { 516 StringBuffer originalArguments = new StringBuffer(); 517 518 int startIndex = 0; 519 while (true) 520 { 521 int endIndex = obfuscatedArguments.indexOf(',', startIndex); 522 if (endIndex < 0) 523 { 524 break; 525 } 526 527 originalArguments.append(originalType(obfuscatedArguments.substring(startIndex, endIndex).trim())).append(','); 528 529 startIndex = endIndex + 1; 530 } 531 532 originalArguments.append(originalType(obfuscatedArguments.substring(startIndex).trim())); 533 534 return originalArguments.toString(); 535 } 536 537 538 /** 539 * Returns the original type. 540 */ 541 private String originalType(String obfuscatedType) 542 { 543 int index = obfuscatedType.indexOf('['); 544 545 return index >= 0 ? 546 originalClassName(obfuscatedType.substring(0, index)) + obfuscatedType.substring(index) : 547 originalClassName(obfuscatedType); 548 } 549 550 551 /** 552 * Returns the original class name. 553 */ 554 private String originalClassName(String obfuscatedClassName) 555 { 556 String originalClassName = (String)classMap.get(obfuscatedClassName); 557 558 return originalClassName != null ? 559 originalClassName : 560 obfuscatedClassName; 561 } 562 563 564 // Implementations for MappingProcessor. 565 566 public boolean processClassMapping(String className, String newClassName) 567 { 568 // Obfuscated class name -> original class name. 569 classMap.put(newClassName, className); 570 571 return true; 572 } 573 574 575 public void processFieldMapping(String className, String fieldType, String fieldName, String newFieldName) 576 { 577 // Original class name -> obfuscated field names. 578 Map fieldMap = (Map)classFieldMap.get(className); 579 if (fieldMap == null) 580 { 581 fieldMap = new HashMap(); 582 classFieldMap.put(className, fieldMap); 583 } 584 585 // Obfuscated field name -> fields. 586 Set fieldSet = (Set)fieldMap.get(newFieldName); 587 if (fieldSet == null) 588 { 589 fieldSet = new LinkedHashSet(); 590 fieldMap.put(newFieldName, fieldSet); 591 } 592 593 // Add the field information. 594 fieldSet.add(new FieldInfo(fieldType, 595 fieldName)); 596 } 597 598 599 public void processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName) 600 { 601 // Original class name -> obfuscated method names. 602 Map methodMap = (Map)classMethodMap.get(className); 603 if (methodMap == null) 604 { 605 methodMap = new HashMap(); 606 classMethodMap.put(className, methodMap); 607 } 608 609 // Obfuscated method name -> methods. 610 Set methodSet = (Set)methodMap.get(newMethodName); 611 if (methodSet == null) 612 { 613 methodSet = new LinkedHashSet(); 614 methodMap.put(newMethodName, methodSet); 615 } 616 617 // Add the method information. 618 methodSet.add(new MethodInfo(firstLineNumber, 619 lastLineNumber, 620 methodReturnType, 621 methodArguments, 622 methodName)); 623 } 624 625 626 /** 627 * A field record. 628 */ 629 private static class FieldInfo 630 { 631 private String type; 632 private String originalName; 633 634 635 private FieldInfo(String type, String originalName) 636 { 637 this.type = type; 638 this.originalName = originalName; 639 } 640 641 642 private boolean matches(String type) 643 { 644 return 645 type == null || type.equals(this.type); 646 } 647 } 648 649 650 /** 651 * A method record. 652 */ 653 private static class MethodInfo 654 { 655 private int firstLineNumber; 656 private int lastLineNumber; 657 private String type; 658 private String arguments; 659 private String originalName; 660 661 662 private MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName) 663 { 664 this.firstLineNumber = firstLineNumber; 665 this.lastLineNumber = lastLineNumber; 666 this.type = type; 667 this.arguments = arguments; 668 this.originalName = originalName; 669 } 670 671 672 private boolean matches(int lineNumber, String type, String arguments) 673 { 674 return 675 (lineNumber == 0 || (firstLineNumber <= lineNumber && lineNumber <= lastLineNumber) || lastLineNumber == 0) && 676 (type == null || type.equals(this.type)) && 677 (arguments == null || arguments.equals(this.arguments)); 678 } 679 } 680 681 682 /** 683 * The main program for ReTrace. 684 */ 685 public static void main(String[] args) 686 { 687 if (args.length < 1) 688 { 689 System.err.println("Usage: java proguard.ReTrace [-verbose] <mapping_file> [<stacktrace_file>]"); 690 System.exit(-1); 691 } 692 693 String regularExpresssion = STACK_TRACE_EXPRESSION; 694 boolean verbose = false; 695 696 int argumentIndex = 0; 697 while (argumentIndex < args.length) 698 { 699 String arg = args[argumentIndex]; 700 if (arg.equals(REGEX_OPTION)) 701 { 702 regularExpresssion = args[++argumentIndex]; 703 } 704 else if (arg.equals(VERBOSE_OPTION)) 705 { 706 verbose = true; 707 } 708 else 709 { 710 break; 711 } 712 713 argumentIndex++; 714 } 715 716 if (argumentIndex >= args.length) 717 { 718 System.err.println("Usage: java proguard.ReTrace [-regex <regex>] [-verbose] <mapping_file> [<stacktrace_file>]"); 719 System.exit(-1); 720 } 721 722 File mappingFile = new File(args[argumentIndex++]); 723 File stackTraceFile = argumentIndex < args.length ? 724 new File(args[argumentIndex]) : 725 null; 726 727 ReTrace reTrace = new ReTrace(regularExpresssion, verbose, mappingFile, stackTraceFile); 728 729 try 730 { 731 // Execute ReTrace with its given settings. 732 reTrace.execute(); 733 } 734 catch (IOException ex) 735 { 736 if (verbose) 737 { 738 // Print a verbose stack trace. 739 ex.printStackTrace(); 740 } 741 else 742 { 743 // Print just the stack trace message. 744 System.err.println("Error: "+ex.getMessage()); 745 } 746 747 System.exit(1); 748 } 749 750 System.exit(0); 751 } 752 } 753