• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * ProGuard -- shrinking, optimization, obfuscation, and preverification
3  *             of Java bytecode.
4  *
5  * Copyright (c) 2002-2014 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 = "(?:.*?\\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      */
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         // 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      */
originalFieldName(String className, String obfuscatedFieldName, String type, StringBuffer outLine, List extraOutLines)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      */
originalMethodName(String className, String obfuscatedMethodName, int lineNumber, String type, String arguments, StringBuffer outLine, List extraOutLines)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      */
originalArguments(String obfuscatedArguments)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      */
originalType(String obfuscatedType)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      */
originalClassName(String obfuscatedClassName)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 
processClassMapping(String className, String newClassName)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 
processFieldMapping(String className, String fieldType, String fieldName, String newFieldName)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 
processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName)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 
FieldInfo(String type, String originalName)639         private FieldInfo(String type, String originalName)
640         {
641             this.type         = type;
642             this.originalName = originalName;
643         }
644 
645 
matches(String type)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 
MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName)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 
matches(int lineNumber, String type, String arguments)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      */
main(String[] args)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