• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * ProGuard -- shrinking, optimization, obfuscation, and preverification
3  *             of Java bytecode.
4  *
5  * Copyright (c) 2002-2009 Eric Lafortune (eric@graphics.cornell.edu)
6  *
7  * This program is free software; you can redistribute it and/or modify it
8  * under the terms of the GNU General Public License as published by the Free
9  * Software Foundation; either version 2 of the License, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful, but WITHOUT
13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14  * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15  * more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with this program; if not, write to the Free Software Foundation, Inc.,
19  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20  */
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      */
ReTrace(String regularExpression, boolean verbose, File mappingFile)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      */
ReTrace(String regularExpression, boolean verbose, File mappingFile, File stackTraceFile)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      */
execute()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      */
originalFieldName(String className, String obfuscatedFieldName, String type, StringBuffer outLine, List extraOutLines)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      */
originalMethodName(String className, String obfuscatedMethodName, int lineNumber, String type, String arguments, StringBuffer outLine, List extraOutLines)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      */
originalArguments(String obfuscatedArguments)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      */
originalType(String obfuscatedType)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      */
originalClassName(String obfuscatedClassName)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 
processClassMapping(String className, String newClassName)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 
processFieldMapping(String className, String fieldType, String fieldName, String newFieldName)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 
processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName)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 
FieldInfo(String type, String originalName)632         private FieldInfo(String type, String originalName)
633         {
634             this.type         = type;
635             this.originalName = originalName;
636         }
637 
638 
matches(String type)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 
MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName)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 
matches(int lineNumber, String type, String arguments)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      */
main(String[] args)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