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