Home | History | Annotate | Download | only in retrace
      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