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 
     44     public static final String STACK_TRACE_EXPRESSION = "(?:\\s*%c:.*)|(?:\\s*at\\s+%c.%m\\s*\\(.*?(?::%l)?\\)\\s*)";
     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