Home | History | Annotate | Download | only in retrace
      1 /*
      2  * ProGuard -- shrinking, optimization, obfuscation, and preverification
      3  *             of Java bytecode.
      4  *
      5  * Copyright (c) 2002-2014 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 = "(?:.*?\\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         // Construct the regular expression.
    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         // Open 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         // Read and process the lines of the stack trace.
    183         try
    184         {
    185             StringBuffer outLine       = new StringBuffer(256);
    186             List         extraOutLines = new ArrayList();
    187 
    188             String className = null;
    189 
    190             // Read all lines from the stack trace.
    191             while (true)
    192             {
    193                 // Read a line.
    194                 String line = reader.readLine();
    195                 if (line == null)
    196                 {
    197                     break;
    198                 }
    199 
    200                 // Try to match it against the regular expression.
    201                 Matcher matcher = pattern.matcher(line);
    202 
    203                 if (matcher.matches())
    204                 {
    205                     // The line matched the regular expression.
    206                     int    lineNumber = 0;
    207                     String type       = null;
    208                     String arguments  = null;
    209 
    210                     // Extract a class name, a line number, a type, and
    211                     // arguments.
    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                     // Deconstruct the input line and reconstruct the output
    246                     // line. Also collect any additional output lines for this
    247                     // line.
    248                     int lineIndex = 0;
    249 
    250                     outLine.setLength(0);
    251                     extraOutLines.clear();
    252 
    253                     for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++)
    254                     {
    255                         int startIndex = matcher.start(expressionTypeIndex + 1);
    256                         if (startIndex >= 0)
    257                         {
    258                             int    endIndex = matcher.end(expressionTypeIndex + 1);
    259                             String match    = matcher.group(expressionTypeIndex + 1);
    260 
    261                             // Copy a literal piece of the input line.
    262                             outLine.append(line.substring(lineIndex, startIndex));
    263 
    264                             // Copy a matched and translated piece of the input line.
    265                             char expressionType = expressionTypes[expressionTypeIndex];
    266                             switch (expressionType)
    267                             {
    268                                 case 'c':
    269                                     className = originalClassName(match);
    270                                     outLine.append(className);
    271                                     break;
    272 
    273                                 case 'C':
    274                                     className = originalClassName(ClassUtil.externalClassName(match));
    275                                     outLine.append(ClassUtil.internalClassName(className));
    276                                     break;
    277 
    278                                 case 'l':
    279                                     lineNumber = Integer.parseInt(match);
    280                                     outLine.append(match);
    281                                     break;
    282 
    283                                 case 't':
    284                                     type = originalType(match);
    285                                     outLine.append(type);
    286                                     break;
    287 
    288                                 case 'f':
    289                                     originalFieldName(className,
    290                                                       match,
    291                                                       type,
    292                                                       outLine,
    293                                                       extraOutLines);
    294                                     break;
    295 
    296                                 case 'm':
    297                                     originalMethodName(className,
    298                                                        match,
    299                                                        lineNumber,
    300                                                        type,
    301                                                        arguments,
    302                                                        outLine,
    303                                                        extraOutLines);
    304                                     break;
    305 
    306                                 case 'a':
    307                                     arguments = originalArguments(match);
    308                                     outLine.append(arguments);
    309                                     break;
    310                             }
    311 
    312                             // Skip the original element whose processed version
    313                             // has just been appended.
    314                             lineIndex = endIndex;
    315                         }
    316                     }
    317 
    318                     // Copy the last literal piece of the input line.
    319                     outLine.append(line.substring(lineIndex));
    320 
    321                     // Print out the processed line.
    322                     System.out.println(outLine);
    323 
    324                     // Print out any additional lines.
    325                     for (int extraLineIndex = 0; extraLineIndex < extraOutLines.size(); extraLineIndex++)
    326                     {
    327                         System.out.println(extraOutLines.get(extraLineIndex));
    328                     }
    329                 }
    330                 else
    331                 {
    332                     // The line didn't match the regular expression.
    333                     // Print out the original line.
    334                     System.out.println(line);
    335                 }
    336             }
    337         }
    338         catch (IOException ex)
    339         {
    340             throw new IOException("Can't read stack trace (" + ex.getMessage() + ")");
    341         }
    342         finally
    343         {
    344             if (stackTraceFile != null)
    345             {
    346                 try
    347                 {
    348                     reader.close();
    349                 }
    350                 catch (IOException ex)
    351                 {
    352                     // This shouldn't happen.
    353                 }
    354             }
    355         }
    356     }
    357 
    358 
    359     /**
    360      * Finds the original field name(s), appending the first one to the out
    361      * line, and any additional alternatives to the extra lines.
    362      */
    363     private void originalFieldName(String       className,
    364                                    String       obfuscatedFieldName,
    365                                    String       type,
    366                                    StringBuffer outLine,
    367                                    List         extraOutLines)
    368     {
    369         int extraIndent = -1;
    370 
    371         // Class name -> obfuscated field names.
    372         Map fieldMap = (Map)classFieldMap.get(className);
    373         if (fieldMap != null)
    374         {
    375             // Obfuscated field names -> fields.
    376             Set fieldSet = (Set)fieldMap.get(obfuscatedFieldName);
    377             if (fieldSet != null)
    378             {
    379                 // Find all matching fields.
    380                 Iterator fieldInfoIterator = fieldSet.iterator();
    381                 while (fieldInfoIterator.hasNext())
    382                 {
    383                     FieldInfo fieldInfo = (FieldInfo)fieldInfoIterator.next();
    384                     if (fieldInfo.matches(type))
    385                     {
    386                         // Is this the first matching field?
    387                         if (extraIndent < 0)
    388                         {
    389                             extraIndent = outLine.length();
    390 
    391                             // Append the first original name.
    392                             if (verbose)
    393                             {
    394                                 outLine.append(fieldInfo.type).append(' ');
    395                             }
    396                             outLine.append(fieldInfo.originalName);
    397                         }
    398                         else
    399                         {
    400                             // Create an additional line with the proper
    401                             // indentation.
    402                             StringBuffer extraBuffer = new StringBuffer();
    403                             for (int counter = 0; counter < extraIndent; counter++)
    404                             {
    405                                 extraBuffer.append(' ');
    406                             }
    407 
    408                             // Append the alternative name.
    409                             if (verbose)
    410                             {
    411                                 extraBuffer.append(fieldInfo.type).append(' ');
    412                             }
    413                             extraBuffer.append(fieldInfo.originalName);
    414 
    415                             // Store the additional line.
    416                             extraOutLines.add(extraBuffer);
    417                         }
    418                     }
    419                 }
    420             }
    421         }
    422 
    423         // Just append the obfuscated name if we haven't found any matching
    424         // fields.
    425         if (extraIndent < 0)
    426         {
    427             outLine.append(obfuscatedFieldName);
    428         }
    429     }
    430 
    431 
    432     /**
    433      * Finds the original method name(s), appending the first one to the out
    434      * line, and any additional alternatives to the extra lines.
    435      */
    436     private void originalMethodName(String       className,
    437                                     String       obfuscatedMethodName,
    438                                     int          lineNumber,
    439                                     String       type,
    440                                     String       arguments,
    441                                     StringBuffer outLine,
    442                                     List         extraOutLines)
    443     {
    444         int extraIndent = -1;
    445 
    446         // Class name -> obfuscated method names.
    447         Map methodMap = (Map)classMethodMap.get(className);
    448         if (methodMap != null)
    449         {
    450             // Obfuscated method names -> methods.
    451             Set methodSet = (Set)methodMap.get(obfuscatedMethodName);
    452             if (methodSet != null)
    453             {
    454                 // Find all matching methods.
    455                 Iterator methodInfoIterator = methodSet.iterator();
    456                 while (methodInfoIterator.hasNext())
    457                 {
    458                     MethodInfo methodInfo = (MethodInfo)methodInfoIterator.next();
    459                     if (methodInfo.matches(lineNumber, type, arguments))
    460                     {
    461                         // Is this the first matching method?
    462                         if (extraIndent < 0)
    463                         {
    464                             extraIndent = outLine.length();
    465 
    466                             // Append the first original name.
    467                             if (verbose)
    468                             {
    469                                 outLine.append(methodInfo.type).append(' ');
    470                             }
    471                             outLine.append(methodInfo.originalName);
    472                             if (verbose)
    473                             {
    474                                 outLine.append('(').append(methodInfo.arguments).append(')');
    475                             }
    476                         }
    477                         else
    478                         {
    479                             // Create an additional line with the proper
    480                             // indentation.
    481                             StringBuffer extraBuffer = new StringBuffer();
    482                             for (int counter = 0; counter < extraIndent; counter++)
    483                             {
    484                                 extraBuffer.append(' ');
    485                             }
    486 
    487                             // Append the alternative name.
    488                             if (verbose)
    489                             {
    490                                 extraBuffer.append(methodInfo.type).append(' ');
    491                             }
    492                             extraBuffer.append(methodInfo.originalName);
    493                             if (verbose)
    494                             {
    495                                 extraBuffer.append('(').append(methodInfo.arguments).append(')');
    496                             }
    497 
    498                             // Store the additional line.
    499                             extraOutLines.add(extraBuffer);
    500                         }
    501                     }
    502                 }
    503             }
    504         }
    505 
    506         // Just append the obfuscated name if we haven't found any matching
    507         // methods.
    508         if (extraIndent < 0)
    509         {
    510             outLine.append(obfuscatedMethodName);
    511         }
    512     }
    513 
    514 
    515     /**
    516      * Returns the original argument types.
    517      */
    518     private String originalArguments(String obfuscatedArguments)
    519     {
    520         StringBuffer originalArguments = new StringBuffer();
    521 
    522         int startIndex = 0;
    523         while (true)
    524         {
    525             int endIndex = obfuscatedArguments.indexOf(',', startIndex);
    526             if (endIndex < 0)
    527             {
    528                 break;
    529             }
    530 
    531             originalArguments.append(originalType(obfuscatedArguments.substring(startIndex, endIndex).trim())).append(',');
    532 
    533             startIndex = endIndex + 1;
    534         }
    535 
    536         originalArguments.append(originalType(obfuscatedArguments.substring(startIndex).trim()));
    537 
    538         return originalArguments.toString();
    539     }
    540 
    541 
    542     /**
    543      * Returns the original type.
    544      */
    545     private String originalType(String obfuscatedType)
    546     {
    547         int index = obfuscatedType.indexOf('[');
    548 
    549         return index >= 0 ?
    550             originalClassName(obfuscatedType.substring(0, index)) + obfuscatedType.substring(index) :
    551             originalClassName(obfuscatedType);
    552     }
    553 
    554 
    555     /**
    556      * Returns the original class name.
    557      */
    558     private String originalClassName(String obfuscatedClassName)
    559     {
    560         String originalClassName = (String)classMap.get(obfuscatedClassName);
    561 
    562         return originalClassName != null ?
    563             originalClassName :
    564             obfuscatedClassName;
    565     }
    566 
    567 
    568     // Implementations for MappingProcessor.
    569 
    570     public boolean processClassMapping(String className, String newClassName)
    571     {
    572         // Obfuscated class name -> original class name.
    573         classMap.put(newClassName, className);
    574 
    575         return true;
    576     }
    577 
    578 
    579     public void processFieldMapping(String className, String fieldType, String fieldName, String newFieldName)
    580     {
    581         // Original class name -> obfuscated field names.
    582         Map fieldMap = (Map)classFieldMap.get(className);
    583         if (fieldMap == null)
    584         {
    585             fieldMap = new HashMap();
    586             classFieldMap.put(className, fieldMap);
    587         }
    588 
    589         // Obfuscated field name -> fields.
    590         Set fieldSet = (Set)fieldMap.get(newFieldName);
    591         if (fieldSet == null)
    592         {
    593             fieldSet = new LinkedHashSet();
    594             fieldMap.put(newFieldName, fieldSet);
    595         }
    596 
    597         // Add the field information.
    598         fieldSet.add(new FieldInfo(fieldType,
    599                                    fieldName));
    600     }
    601 
    602 
    603     public void processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName)
    604     {
    605         // Original class name -> obfuscated method names.
    606         Map methodMap = (Map)classMethodMap.get(className);
    607         if (methodMap == null)
    608         {
    609             methodMap = new HashMap();
    610             classMethodMap.put(className, methodMap);
    611         }
    612 
    613         // Obfuscated method name -> methods.
    614         Set methodSet = (Set)methodMap.get(newMethodName);
    615         if (methodSet == null)
    616         {
    617             methodSet = new LinkedHashSet();
    618             methodMap.put(newMethodName, methodSet);
    619         }
    620 
    621         // Add the method information.
    622         methodSet.add(new MethodInfo(firstLineNumber,
    623                                      lastLineNumber,
    624                                      methodReturnType,
    625                                      methodArguments,
    626                                      methodName));
    627     }
    628 
    629 
    630     /**
    631      * A field record.
    632      */
    633     private static class FieldInfo
    634     {
    635         private String type;
    636         private String originalName;
    637 
    638 
    639         private FieldInfo(String type, String originalName)
    640         {
    641             this.type         = type;
    642             this.originalName = originalName;
    643         }
    644 
    645 
    646         private boolean matches(String type)
    647         {
    648             return
    649                 type == null || type.equals(this.type);
    650         }
    651     }
    652 
    653 
    654     /**
    655      * A method record.
    656      */
    657     private static class MethodInfo
    658     {
    659         private int    firstLineNumber;
    660         private int    lastLineNumber;
    661         private String type;
    662         private String arguments;
    663         private String originalName;
    664 
    665 
    666         private MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName)
    667         {
    668             this.firstLineNumber = firstLineNumber;
    669             this.lastLineNumber  = lastLineNumber;
    670             this.type            = type;
    671             this.arguments       = arguments;
    672             this.originalName    = originalName;
    673         }
    674 
    675 
    676         private boolean matches(int lineNumber, String type, String arguments)
    677         {
    678             return
    679                 (lineNumber == 0    || (firstLineNumber <= lineNumber && lineNumber <= lastLineNumber) || lastLineNumber == 0) &&
    680                 (type       == null || type.equals(this.type))                                                                 &&
    681                 (arguments  == null || arguments.equals(this.arguments));
    682         }
    683     }
    684 
    685 
    686     /**
    687      * The main program for ReTrace.
    688      */
    689     public static void main(String[] args)
    690     {
    691         if (args.length < 1)
    692         {
    693             System.err.println("Usage: java proguard.ReTrace [-verbose] <mapping_file> [<stacktrace_file>]");
    694             System.exit(-1);
    695         }
    696 
    697         String  regularExpresssion = STACK_TRACE_EXPRESSION;
    698         boolean verbose            = false;
    699 
    700         int argumentIndex = 0;
    701         while (argumentIndex < args.length)
    702         {
    703             String arg = args[argumentIndex];
    704             if (arg.equals(REGEX_OPTION))
    705             {
    706                 regularExpresssion = args[++argumentIndex];
    707             }
    708             else if (arg.equals(VERBOSE_OPTION))
    709             {
    710                 verbose = true;
    711             }
    712             else
    713             {
    714                 break;
    715             }
    716 
    717             argumentIndex++;
    718         }
    719 
    720         if (argumentIndex >= args.length)
    721         {
    722             System.err.println("Usage: java proguard.ReTrace [-regex <regex>] [-verbose] <mapping_file> [<stacktrace_file>]");
    723             System.exit(-1);
    724         }
    725 
    726         File mappingFile    = new File(args[argumentIndex++]);
    727         File stackTraceFile = argumentIndex < args.length ?
    728             new File(args[argumentIndex]) :
    729             null;
    730 
    731         ReTrace reTrace = new ReTrace(regularExpresssion, verbose, mappingFile, stackTraceFile);
    732 
    733         try
    734         {
    735             // Execute ReTrace with its given settings.
    736             reTrace.execute();
    737         }
    738         catch (IOException ex)
    739         {
    740             if (verbose)
    741             {
    742                 // Print a verbose stack trace.
    743                 ex.printStackTrace();
    744             }
    745             else
    746             {
    747                 // Print just the stack trace message.
    748                 System.err.println("Error: "+ex.getMessage());
    749             }
    750 
    751             System.exit(1);
    752         }
    753 
    754         System.exit(0);
    755     }
    756 }
    757