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 */
21package proguard.retrace;
22
23import proguard.classfile.util.ClassUtil;
24import proguard.obfuscate.*;
25
26import java.io.*;
27import java.util.*;
28import 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 */
37public class ReTrace
38implements   MappingProcessor
39{
40    private static final String REGEX_OPTION   = "-regex";
41    private static final String VERBOSE_OPTION = "-verbose";
42
43    // BEGIN android-changed
44    // Use regex from latest version (4.9) because it is
45    // able to handle Android Bugreport format
46    public static final String STACK_TRACE_EXPRESSION = "(?:.*?\\bat\\s+%c.%m\\s*\\(.*?(?::%l)?\\)\\s*)|(?:(?:.*?[:\"]\\s+)?%c(?::.*)?)";
47    // END android-changed
48
49    private static final String REGEX_CLASS       = "\\b(?:[A-Za-z0-9_$]+\\.)*[A-Za-z0-9_$]+\\b";
50    private static final String REGEX_CLASS_SLASH = "\\b(?:[A-Za-z0-9_$]+/)*[A-Za-z0-9_$]+\\b";
51    private static final String REGEX_LINE_NUMBER = "\\b[0-9]+\\b";
52    private static final String REGEX_TYPE        = REGEX_CLASS + "(?:\\[\\])*";
53    private static final String REGEX_MEMBER      = "\\b[A-Za-z0-9_$]+\\b";
54    private static final String REGEX_ARGUMENTS   = "(?:" + REGEX_TYPE + "(?:\\s*,\\s*" + REGEX_TYPE + ")*)?";
55
56    // The class settings.
57    private final String  regularExpression;
58    private final boolean verbose;
59    private final File    mappingFile;
60    private final File    stackTraceFile;
61
62    private Map classMap       = new HashMap();
63    private Map classFieldMap  = new HashMap();
64    private Map classMethodMap = new HashMap();
65
66
67    /**
68     * Creates a new ReTrace object to process stack traces on the standard
69     * input, based on the given mapping file name.
70     * @param regularExpression the regular expression for parsing the lines in
71     *                          the stack trace.
72     * @param verbose           specifies whether the de-obfuscated stack trace
73     *                          should be verbose.
74     * @param mappingFile       the mapping file that was written out by
75     *                          ProGuard.
76     */
77    public ReTrace(String  regularExpression,
78                   boolean verbose,
79                   File    mappingFile)
80    {
81        this(regularExpression, verbose, mappingFile, null);
82    }
83
84
85    /**
86     * Creates a new ReTrace object to process a stack trace from the given file,
87     * based on the given mapping file name.
88     * @param regularExpression the regular expression for parsing the lines in
89     *                          the stack trace.
90     * @param verbose           specifies whether the de-obfuscated stack trace
91     *                          should be verbose.
92     * @param mappingFile       the mapping file that was written out by
93     *                          ProGuard.
94     * @param stackTraceFile    the optional name of the file that contains the
95     *                          stack trace.
96     */
97    public ReTrace(String  regularExpression,
98                   boolean verbose,
99                   File    mappingFile,
100                   File    stackTraceFile)
101    {
102        this.regularExpression = regularExpression;
103        this.verbose           = verbose;
104        this.mappingFile       = mappingFile;
105        this.stackTraceFile    = stackTraceFile;
106    }
107
108
109    /**
110     * Performs the subsequent ReTrace operations.
111     */
112    public void execute() throws IOException
113    {
114        // Read the mapping file.
115        MappingReader mappingReader = new MappingReader(mappingFile);
116        mappingReader.pump(this);
117
118
119        StringBuffer expressionBuffer    = new StringBuffer(regularExpression.length() + 32);
120        char[]       expressionTypes     = new char[32];
121        int          expressionTypeCount = 0;
122        int index = 0;
123        while (true)
124        {
125            int nextIndex = regularExpression.indexOf('%', index);
126            if (nextIndex < 0                             ||
127                nextIndex == regularExpression.length()-1 ||
128                expressionTypeCount == expressionTypes.length)
129            {
130                break;
131            }
132
133            expressionBuffer.append(regularExpression.substring(index, nextIndex));
134            expressionBuffer.append('(');
135
136            char expressionType = regularExpression.charAt(nextIndex + 1);
137            switch(expressionType)
138            {
139                case 'c':
140                    expressionBuffer.append(REGEX_CLASS);
141                    break;
142
143                case 'C':
144                    expressionBuffer.append(REGEX_CLASS_SLASH);
145                    break;
146
147                case 'l':
148                    expressionBuffer.append(REGEX_LINE_NUMBER);
149                    break;
150
151                case 't':
152                    expressionBuffer.append(REGEX_TYPE);
153                    break;
154
155                case 'f':
156                    expressionBuffer.append(REGEX_MEMBER);
157                    break;
158
159                case 'm':
160                    expressionBuffer.append(REGEX_MEMBER);
161                    break;
162
163                case 'a':
164                    expressionBuffer.append(REGEX_ARGUMENTS);
165                    break;
166            }
167
168            expressionBuffer.append(')');
169
170            expressionTypes[expressionTypeCount++] = expressionType;
171
172            index = nextIndex + 2;
173        }
174
175        expressionBuffer.append(regularExpression.substring(index));
176
177        Pattern pattern = Pattern.compile(expressionBuffer.toString());
178
179        // Read the stack trace file.
180        LineNumberReader reader =
181            new LineNumberReader(stackTraceFile == null ?
182                (Reader)new InputStreamReader(System.in) :
183                (Reader)new BufferedReader(new FileReader(stackTraceFile)));
184
185
186        try
187        {
188            StringBuffer outLine = new StringBuffer(256);
189            List         extraOutLines  = new ArrayList();
190
191            String className = null;
192
193            // Read the line in the stack trace.
194            while (true)
195            {
196                String line = reader.readLine();
197                if (line == null)
198                {
199                    break;
200                }
201
202                Matcher matcher = pattern.matcher(line);
203
204                if (matcher.matches())
205                {
206                    int    lineNumber = 0;
207                    String type       = null;
208                    String arguments  = null;
209
210                    // Figure out a class name, line number, type, and
211                    // arguments beforehand.
212                    for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++)
213                    {
214                        int startIndex = matcher.start(expressionTypeIndex + 1);
215                        if (startIndex >= 0)
216                        {
217                            String match = matcher.group(expressionTypeIndex + 1);
218
219                            char expressionType = expressionTypes[expressionTypeIndex];
220                            switch (expressionType)
221                            {
222                                case 'c':
223                                    className = originalClassName(match);
224                                    break;
225
226                                case 'C':
227                                    className = originalClassName(ClassUtil.externalClassName(match));
228                                    break;
229
230                                case 'l':
231                                    lineNumber = Integer.parseInt(match);
232                                    break;
233
234                                case 't':
235                                    type = originalType(match);
236                                    break;
237
238                                case 'a':
239                                    arguments = originalArguments(match);
240                                    break;
241                            }
242                        }
243                    }
244
245                    // Actually construct the output line.
246                    int lineIndex = 0;
247
248                    outLine.setLength(0);
249                    extraOutLines.clear();
250
251                    for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++)
252                    {
253                        int startIndex = matcher.start(expressionTypeIndex + 1);
254                        if (startIndex >= 0)
255                        {
256                            int    endIndex = matcher.end(expressionTypeIndex + 1);
257                            String match    = matcher.group(expressionTypeIndex + 1);
258
259                            // Copy a literal piece of input line.
260                            outLine.append(line.substring(lineIndex, startIndex));
261
262                            char expressionType = expressionTypes[expressionTypeIndex];
263                            switch (expressionType)
264                            {
265                                case 'c':
266                                    className = originalClassName(match);
267                                    outLine.append(className);
268                                    break;
269
270                                case 'C':
271                                    className = originalClassName(ClassUtil.externalClassName(match));
272                                    outLine.append(ClassUtil.internalClassName(className));
273                                    break;
274
275                                case 'l':
276                                    lineNumber = Integer.parseInt(match);
277                                    outLine.append(match);
278                                    break;
279
280                                case 't':
281                                    type = originalType(match);
282                                    outLine.append(type);
283                                    break;
284
285                                case 'f':
286                                    originalFieldName(className,
287                                                      match,
288                                                      type,
289                                                      outLine,
290                                                      extraOutLines);
291                                    break;
292
293                                case 'm':
294                                    originalMethodName(className,
295                                                       match,
296                                                       lineNumber,
297                                                       type,
298                                                       arguments,
299                                                       outLine,
300                                                       extraOutLines);
301                                    break;
302
303                                case 'a':
304                                    arguments = originalArguments(match);
305                                    outLine.append(arguments);
306                                    break;
307                            }
308
309                            // Skip the original element whose processed version
310                            // has just been appended.
311                            lineIndex = endIndex;
312                        }
313                    }
314
315                    // Copy the last literal piece of input line.
316                    outLine.append(line.substring(lineIndex));
317
318                    // Print out the main line.
319                    System.out.println(outLine);
320
321                    // Print out any additional lines.
322                    for (int extraLineIndex = 0; extraLineIndex < extraOutLines.size(); extraLineIndex++)
323                    {
324                        System.out.println(extraOutLines.get(extraLineIndex));
325                    }
326                }
327                else
328                {
329                    // Print out the original line.
330                    System.out.println(line);
331                }
332            }
333        }
334        catch (IOException ex)
335        {
336            throw new IOException("Can't read stack trace (" + ex.getMessage() + ")");
337        }
338        finally
339        {
340            if (stackTraceFile != null)
341            {
342                try
343                {
344                    reader.close();
345                }
346                catch (IOException ex)
347                {
348                    // This shouldn't happen.
349                }
350            }
351        }
352    }
353
354
355    /**
356     * Finds the original field name(s), appending the first one to the out
357     * line, and any additional alternatives to the extra lines.
358     */
359    private void originalFieldName(String       className,
360                                   String       obfuscatedFieldName,
361                                   String       type,
362                                   StringBuffer outLine,
363                                   List         extraOutLines)
364    {
365        int extraIndent = -1;
366
367        // Class name -> obfuscated field names.
368        Map fieldMap = (Map)classFieldMap.get(className);
369        if (fieldMap != null)
370        {
371            // Obfuscated field names -> fields.
372            Set fieldSet = (Set)fieldMap.get(obfuscatedFieldName);
373            if (fieldSet != null)
374            {
375                // Find all matching fields.
376                Iterator fieldInfoIterator = fieldSet.iterator();
377                while (fieldInfoIterator.hasNext())
378                {
379                    FieldInfo fieldInfo = (FieldInfo)fieldInfoIterator.next();
380                    if (fieldInfo.matches(type))
381                    {
382                        // Is this the first matching field?
383                        if (extraIndent < 0)
384                        {
385                            extraIndent = outLine.length();
386
387                            // Append the first original name.
388                            if (verbose)
389                            {
390                                outLine.append(fieldInfo.type).append(' ');
391                            }
392                            outLine.append(fieldInfo.originalName);
393                        }
394                        else
395                        {
396                            // Create an additional line with the proper
397                            // indentation.
398                            StringBuffer extraBuffer = new StringBuffer();
399                            for (int counter = 0; counter < extraIndent; counter++)
400                            {
401                                extraBuffer.append(' ');
402                            }
403
404                            // Append the alternative name.
405                            if (verbose)
406                            {
407                                extraBuffer.append(fieldInfo.type).append(' ');
408                            }
409                            extraBuffer.append(fieldInfo.originalName);
410
411                            // Store the additional line.
412                            extraOutLines.add(extraBuffer);
413                        }
414                    }
415                }
416            }
417        }
418
419        // Just append the obfuscated name if we haven't found any matching
420        // fields.
421        if (extraIndent < 0)
422        {
423            outLine.append(obfuscatedFieldName);
424        }
425    }
426
427
428    /**
429     * Finds the original method name(s), appending the first one to the out
430     * line, and any additional alternatives to the extra lines.
431     */
432    private void originalMethodName(String       className,
433                                    String       obfuscatedMethodName,
434                                    int          lineNumber,
435                                    String       type,
436                                    String       arguments,
437                                    StringBuffer outLine,
438                                    List         extraOutLines)
439    {
440        int extraIndent = -1;
441
442        // Class name -> obfuscated method names.
443        Map methodMap = (Map)classMethodMap.get(className);
444        if (methodMap != null)
445        {
446            // Obfuscated method names -> methods.
447            Set methodSet = (Set)methodMap.get(obfuscatedMethodName);
448            if (methodSet != null)
449            {
450                // Find all matching methods.
451                Iterator methodInfoIterator = methodSet.iterator();
452                while (methodInfoIterator.hasNext())
453                {
454                    MethodInfo methodInfo = (MethodInfo)methodInfoIterator.next();
455                    if (methodInfo.matches(lineNumber, type, arguments))
456                    {
457                        // Is this the first matching method?
458                        if (extraIndent < 0)
459                        {
460                            extraIndent = outLine.length();
461
462                            // Append the first original name.
463                            if (verbose)
464                            {
465                                outLine.append(methodInfo.type).append(' ');
466                            }
467                            outLine.append(methodInfo.originalName);
468                            if (verbose)
469                            {
470                                outLine.append('(').append(methodInfo.arguments).append(')');
471                            }
472                        }
473                        else
474                        {
475                            // Create an additional line with the proper
476                            // indentation.
477                            StringBuffer extraBuffer = new StringBuffer();
478                            for (int counter = 0; counter < extraIndent; counter++)
479                            {
480                                extraBuffer.append(' ');
481                            }
482
483                            // Append the alternative name.
484                            if (verbose)
485                            {
486                                extraBuffer.append(methodInfo.type).append(' ');
487                            }
488                            extraBuffer.append(methodInfo.originalName);
489                            if (verbose)
490                            {
491                                extraBuffer.append('(').append(methodInfo.arguments).append(')');
492                            }
493
494                            // Store the additional line.
495                            extraOutLines.add(extraBuffer);
496                        }
497                    }
498                }
499            }
500        }
501
502        // Just append the obfuscated name if we haven't found any matching
503        // methods.
504        if (extraIndent < 0)
505        {
506            outLine.append(obfuscatedMethodName);
507        }
508    }
509
510
511    /**
512     * Returns the original argument types.
513     */
514    private String originalArguments(String obfuscatedArguments)
515    {
516        StringBuffer originalArguments = new StringBuffer();
517
518        int startIndex = 0;
519        while (true)
520        {
521            int endIndex = obfuscatedArguments.indexOf(',', startIndex);
522            if (endIndex < 0)
523            {
524                break;
525            }
526
527            originalArguments.append(originalType(obfuscatedArguments.substring(startIndex, endIndex).trim())).append(',');
528
529            startIndex = endIndex + 1;
530        }
531
532        originalArguments.append(originalType(obfuscatedArguments.substring(startIndex).trim()));
533
534        return originalArguments.toString();
535    }
536
537
538    /**
539     * Returns the original type.
540     */
541    private String originalType(String obfuscatedType)
542    {
543        int index = obfuscatedType.indexOf('[');
544
545        return index >= 0 ?
546            originalClassName(obfuscatedType.substring(0, index)) + obfuscatedType.substring(index) :
547            originalClassName(obfuscatedType);
548    }
549
550
551    /**
552     * Returns the original class name.
553     */
554    private String originalClassName(String obfuscatedClassName)
555    {
556        String originalClassName = (String)classMap.get(obfuscatedClassName);
557
558        return originalClassName != null ?
559            originalClassName :
560            obfuscatedClassName;
561    }
562
563
564    // Implementations for MappingProcessor.
565
566    public boolean processClassMapping(String className, String newClassName)
567    {
568        // Obfuscated class name -> original class name.
569        classMap.put(newClassName, className);
570
571        return true;
572    }
573
574
575    public void processFieldMapping(String className, String fieldType, String fieldName, String newFieldName)
576    {
577        // Original class name -> obfuscated field names.
578        Map fieldMap = (Map)classFieldMap.get(className);
579        if (fieldMap == null)
580        {
581            fieldMap = new HashMap();
582            classFieldMap.put(className, fieldMap);
583        }
584
585        // Obfuscated field name -> fields.
586        Set fieldSet = (Set)fieldMap.get(newFieldName);
587        if (fieldSet == null)
588        {
589            fieldSet = new LinkedHashSet();
590            fieldMap.put(newFieldName, fieldSet);
591        }
592
593        // Add the field information.
594        fieldSet.add(new FieldInfo(fieldType,
595                                   fieldName));
596    }
597
598
599    public void processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName)
600    {
601        // Original class name -> obfuscated method names.
602        Map methodMap = (Map)classMethodMap.get(className);
603        if (methodMap == null)
604        {
605            methodMap = new HashMap();
606            classMethodMap.put(className, methodMap);
607        }
608
609        // Obfuscated method name -> methods.
610        Set methodSet = (Set)methodMap.get(newMethodName);
611        if (methodSet == null)
612        {
613            methodSet = new LinkedHashSet();
614            methodMap.put(newMethodName, methodSet);
615        }
616
617        // Add the method information.
618        methodSet.add(new MethodInfo(firstLineNumber,
619                                     lastLineNumber,
620                                     methodReturnType,
621                                     methodArguments,
622                                     methodName));
623    }
624
625
626    /**
627     * A field record.
628     */
629    private static class FieldInfo
630    {
631        private String type;
632        private String originalName;
633
634
635        private FieldInfo(String type, String originalName)
636        {
637            this.type         = type;
638            this.originalName = originalName;
639        }
640
641
642        private boolean matches(String type)
643        {
644            return
645                type == null || type.equals(this.type);
646        }
647    }
648
649
650    /**
651     * A method record.
652     */
653    private static class MethodInfo
654    {
655        private int    firstLineNumber;
656        private int    lastLineNumber;
657        private String type;
658        private String arguments;
659        private String originalName;
660
661
662        private MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName)
663        {
664            this.firstLineNumber = firstLineNumber;
665            this.lastLineNumber  = lastLineNumber;
666            this.type            = type;
667            this.arguments       = arguments;
668            this.originalName    = originalName;
669        }
670
671
672        private boolean matches(int lineNumber, String type, String arguments)
673        {
674            return
675                (lineNumber == 0    || (firstLineNumber <= lineNumber && lineNumber <= lastLineNumber) || lastLineNumber == 0) &&
676                (type       == null || type.equals(this.type))                                                                 &&
677                (arguments  == null || arguments.equals(this.arguments));
678        }
679    }
680
681
682    /**
683     * The main program for ReTrace.
684     */
685    public static void main(String[] args)
686    {
687        if (args.length < 1)
688        {
689            System.err.println("Usage: java proguard.ReTrace [-verbose] <mapping_file> [<stacktrace_file>]");
690            System.exit(-1);
691        }
692
693        String  regularExpresssion = STACK_TRACE_EXPRESSION;
694        boolean verbose            = false;
695
696        int argumentIndex = 0;
697        while (argumentIndex < args.length)
698        {
699            String arg = args[argumentIndex];
700            if (arg.equals(REGEX_OPTION))
701            {
702                regularExpresssion = args[++argumentIndex];
703            }
704            else if (arg.equals(VERBOSE_OPTION))
705            {
706                verbose = true;
707            }
708            else
709            {
710                break;
711            }
712
713            argumentIndex++;
714        }
715
716        if (argumentIndex >= args.length)
717        {
718            System.err.println("Usage: java proguard.ReTrace [-regex <regex>] [-verbose] <mapping_file> [<stacktrace_file>]");
719            System.exit(-1);
720        }
721
722        File mappingFile    = new File(args[argumentIndex++]);
723        File stackTraceFile = argumentIndex < args.length ?
724            new File(args[argumentIndex]) :
725            null;
726
727        ReTrace reTrace = new ReTrace(regularExpresssion, verbose, mappingFile, stackTraceFile);
728
729        try
730        {
731            // Execute ReTrace with its given settings.
732            reTrace.execute();
733        }
734        catch (IOException ex)
735        {
736            if (verbose)
737            {
738                // Print a verbose stack trace.
739                ex.printStackTrace();
740            }
741            else
742            {
743                // Print just the stack trace message.
744                System.err.println("Error: "+ex.getMessage());
745            }
746
747            System.exit(1);
748        }
749
750        System.exit(0);
751    }
752}
753