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