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