summaryrefslogtreecommitdiff
path: root/src/proguard/retrace/ReTrace.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/proguard/retrace/ReTrace.java')
-rw-r--r--src/proguard/retrace/ReTrace.java749
1 files changed, 749 insertions, 0 deletions
diff --git a/src/proguard/retrace/ReTrace.java b/src/proguard/retrace/ReTrace.java
new file mode 100644
index 0000000..97ab27b
--- /dev/null
+++ b/src/proguard/retrace/ReTrace.java
@@ -0,0 +1,749 @@
+/*
+ * ProGuard -- shrinking, optimization, obfuscation, and preverification
+ * of Java bytecode.
+ *
+ * Copyright (c) 2002-2009 Eric Lafortune (eric@graphics.cornell.edu)
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 2 of the License, or (at your option)
+ * any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+package proguard.retrace;
+
+import proguard.classfile.util.ClassUtil;
+import proguard.obfuscate.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.regex.*;
+
+
+/**
+ * Tool for de-obfuscating stack traces of applications that were obfuscated
+ * with ProGuard.
+ *
+ * @author Eric Lafortune
+ */
+public class ReTrace
+implements MappingProcessor
+{
+ private static final String REGEX_OPTION = "-regex";
+ private static final String VERBOSE_OPTION = "-verbose";
+
+
+ public static final String STACK_TRACE_EXPRESSION = "(?:\\s*%c:.*)|(?:\\s*at\\s+%c.%m\\s*\\(.*?(?::%l)?\\)\\s*)";
+
+ private static final String REGEX_CLASS = "\\b(?:[A-Za-z0-9_$]+\\.)*[A-Za-z0-9_$]+\\b";
+ private static final String REGEX_CLASS_SLASH = "\\b(?:[A-Za-z0-9_$]+/)*[A-Za-z0-9_$]+\\b";
+ private static final String REGEX_LINE_NUMBER = "\\b[0-9]+\\b";
+ private static final String REGEX_TYPE = REGEX_CLASS + "(?:\\[\\])*";
+ private static final String REGEX_MEMBER = "\\b[A-Za-z0-9_$]+\\b";
+ private static final String REGEX_ARGUMENTS = "(?:" + REGEX_TYPE + "(?:\\s*,\\s*" + REGEX_TYPE + ")*)?";
+
+ // The class settings.
+ private final String regularExpression;
+ private final boolean verbose;
+ private final File mappingFile;
+ private final File stackTraceFile;
+
+ private Map classMap = new HashMap();
+ private Map classFieldMap = new HashMap();
+ private Map classMethodMap = new HashMap();
+
+
+ /**
+ * Creates a new ReTrace object to process stack traces on the standard
+ * input, based on the given mapping file name.
+ * @param regularExpression the regular expression for parsing the lines in
+ * the stack trace.
+ * @param verbose specifies whether the de-obfuscated stack trace
+ * should be verbose.
+ * @param mappingFile the mapping file that was written out by
+ * ProGuard.
+ */
+ public ReTrace(String regularExpression,
+ boolean verbose,
+ File mappingFile)
+ {
+ this(regularExpression, verbose, mappingFile, null);
+ }
+
+
+ /**
+ * Creates a new ReTrace object to process a stack trace from the given file,
+ * based on the given mapping file name.
+ * @param regularExpression the regular expression for parsing the lines in
+ * the stack trace.
+ * @param verbose specifies whether the de-obfuscated stack trace
+ * should be verbose.
+ * @param mappingFile the mapping file that was written out by
+ * ProGuard.
+ * @param stackTraceFile the optional name of the file that contains the
+ * stack trace.
+ */
+ public ReTrace(String regularExpression,
+ boolean verbose,
+ File mappingFile,
+ File stackTraceFile)
+ {
+ this.regularExpression = regularExpression;
+ this.verbose = verbose;
+ this.mappingFile = mappingFile;
+ this.stackTraceFile = stackTraceFile;
+ }
+
+
+ /**
+ * Performs the subsequent ReTrace operations.
+ */
+ public void execute() throws IOException
+ {
+ // Read the mapping file.
+ MappingReader mappingReader = new MappingReader(mappingFile);
+ mappingReader.pump(this);
+
+
+ StringBuffer expressionBuffer = new StringBuffer(regularExpression.length() + 32);
+ char[] expressionTypes = new char[32];
+ int expressionTypeCount = 0;
+ int index = 0;
+ while (true)
+ {
+ int nextIndex = regularExpression.indexOf('%', index);
+ if (nextIndex < 0 ||
+ nextIndex == regularExpression.length()-1 ||
+ expressionTypeCount == expressionTypes.length)
+ {
+ break;
+ }
+
+ expressionBuffer.append(regularExpression.substring(index, nextIndex));
+ expressionBuffer.append('(');
+
+ char expressionType = regularExpression.charAt(nextIndex + 1);
+ switch(expressionType)
+ {
+ case 'c':
+ expressionBuffer.append(REGEX_CLASS);
+ break;
+
+ case 'C':
+ expressionBuffer.append(REGEX_CLASS_SLASH);
+ break;
+
+ case 'l':
+ expressionBuffer.append(REGEX_LINE_NUMBER);
+ break;
+
+ case 't':
+ expressionBuffer.append(REGEX_TYPE);
+ break;
+
+ case 'f':
+ expressionBuffer.append(REGEX_MEMBER);
+ break;
+
+ case 'm':
+ expressionBuffer.append(REGEX_MEMBER);
+ break;
+
+ case 'a':
+ expressionBuffer.append(REGEX_ARGUMENTS);
+ break;
+ }
+
+ expressionBuffer.append(')');
+
+ expressionTypes[expressionTypeCount++] = expressionType;
+
+ index = nextIndex + 2;
+ }
+
+ expressionBuffer.append(regularExpression.substring(index));
+
+ Pattern pattern = Pattern.compile(expressionBuffer.toString());
+
+ // Read the stack trace file.
+ LineNumberReader reader =
+ new LineNumberReader(stackTraceFile == null ?
+ (Reader)new InputStreamReader(System.in) :
+ (Reader)new BufferedReader(new FileReader(stackTraceFile)));
+
+
+ try
+ {
+ StringBuffer outLine = new StringBuffer(256);
+ List extraOutLines = new ArrayList();
+
+ String className = null;
+
+ // Read the line in the stack trace.
+ while (true)
+ {
+ String line = reader.readLine();
+ if (line == null)
+ {
+ break;
+ }
+
+ Matcher matcher = pattern.matcher(line);
+
+ if (matcher.matches())
+ {
+ int lineNumber = 0;
+ String type = null;
+ String arguments = null;
+
+ // Figure out a class name, line number, type, and
+ // arguments beforehand.
+ for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++)
+ {
+ int startIndex = matcher.start(expressionTypeIndex + 1);
+ if (startIndex >= 0)
+ {
+ String match = matcher.group(expressionTypeIndex + 1);
+
+ char expressionType = expressionTypes[expressionTypeIndex];
+ switch (expressionType)
+ {
+ case 'c':
+ className = originalClassName(match);
+ break;
+
+ case 'C':
+ className = originalClassName(ClassUtil.externalClassName(match));
+ break;
+
+ case 'l':
+ lineNumber = Integer.parseInt(match);
+ break;
+
+ case 't':
+ type = originalType(match);
+ break;
+
+ case 'a':
+ arguments = originalArguments(match);
+ break;
+ }
+ }
+ }
+
+ // Actually construct the output line.
+ int lineIndex = 0;
+
+ outLine.setLength(0);
+ extraOutLines.clear();
+
+ for (int expressionTypeIndex = 0; expressionTypeIndex < expressionTypeCount; expressionTypeIndex++)
+ {
+ int startIndex = matcher.start(expressionTypeIndex + 1);
+ if (startIndex >= 0)
+ {
+ int endIndex = matcher.end(expressionTypeIndex + 1);
+ String match = matcher.group(expressionTypeIndex + 1);
+
+ // Copy a literal piece of input line.
+ outLine.append(line.substring(lineIndex, startIndex));
+
+ char expressionType = expressionTypes[expressionTypeIndex];
+ switch (expressionType)
+ {
+ case 'c':
+ className = originalClassName(match);
+ outLine.append(className);
+ break;
+
+ case 'C':
+ className = originalClassName(ClassUtil.externalClassName(match));
+ outLine.append(ClassUtil.internalClassName(className));
+ break;
+
+ case 'l':
+ lineNumber = Integer.parseInt(match);
+ outLine.append(match);
+ break;
+
+ case 't':
+ type = originalType(match);
+ outLine.append(type);
+ break;
+
+ case 'f':
+ originalFieldName(className,
+ match,
+ type,
+ outLine,
+ extraOutLines);
+ break;
+
+ case 'm':
+ originalMethodName(className,
+ match,
+ lineNumber,
+ type,
+ arguments,
+ outLine,
+ extraOutLines);
+ break;
+
+ case 'a':
+ arguments = originalArguments(match);
+ outLine.append(arguments);
+ break;
+ }
+
+ // Skip the original element whose processed version
+ // has just been appended.
+ lineIndex = endIndex;
+ }
+ }
+
+ // Copy the last literal piece of input line.
+ outLine.append(line.substring(lineIndex));
+
+ // Print out the main line.
+ System.out.println(outLine);
+
+ // Print out any additional lines.
+ for (int extraLineIndex = 0; extraLineIndex < extraOutLines.size(); extraLineIndex++)
+ {
+ System.out.println(extraOutLines.get(extraLineIndex));
+ }
+ }
+ else
+ {
+ // Print out the original line.
+ System.out.println(line);
+ }
+ }
+ }
+ catch (IOException ex)
+ {
+ throw new IOException("Can't read stack trace (" + ex.getMessage() + ")");
+ }
+ finally
+ {
+ if (stackTraceFile != null)
+ {
+ try
+ {
+ reader.close();
+ }
+ catch (IOException ex)
+ {
+ // This shouldn't happen.
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Finds the original field name(s), appending the first one to the out
+ * line, and any additional alternatives to the extra lines.
+ */
+ private void originalFieldName(String className,
+ String obfuscatedFieldName,
+ String type,
+ StringBuffer outLine,
+ List extraOutLines)
+ {
+ int extraIndent = -1;
+
+ // Class name -> obfuscated field names.
+ Map fieldMap = (Map)classFieldMap.get(className);
+ if (fieldMap != null)
+ {
+ // Obfuscated field names -> fields.
+ Set fieldSet = (Set)fieldMap.get(obfuscatedFieldName);
+ if (fieldSet != null)
+ {
+ // Find all matching fields.
+ Iterator fieldInfoIterator = fieldSet.iterator();
+ while (fieldInfoIterator.hasNext())
+ {
+ FieldInfo fieldInfo = (FieldInfo)fieldInfoIterator.next();
+ if (fieldInfo.matches(type))
+ {
+ // Is this the first matching field?
+ if (extraIndent < 0)
+ {
+ extraIndent = outLine.length();
+
+ // Append the first original name.
+ if (verbose)
+ {
+ outLine.append(fieldInfo.type).append(' ');
+ }
+ outLine.append(fieldInfo.originalName);
+ }
+ else
+ {
+ // Create an additional line with the proper
+ // indentation.
+ StringBuffer extraBuffer = new StringBuffer();
+ for (int counter = 0; counter < extraIndent; counter++)
+ {
+ extraBuffer.append(' ');
+ }
+
+ // Append the alternative name.
+ if (verbose)
+ {
+ extraBuffer.append(fieldInfo.type).append(' ');
+ }
+ extraBuffer.append(fieldInfo.originalName);
+
+ // Store the additional line.
+ extraOutLines.add(extraBuffer);
+ }
+ }
+ }
+ }
+ }
+
+ // Just append the obfuscated name if we haven't found any matching
+ // fields.
+ if (extraIndent < 0)
+ {
+ outLine.append(obfuscatedFieldName);
+ }
+ }
+
+
+ /**
+ * Finds the original method name(s), appending the first one to the out
+ * line, and any additional alternatives to the extra lines.
+ */
+ private void originalMethodName(String className,
+ String obfuscatedMethodName,
+ int lineNumber,
+ String type,
+ String arguments,
+ StringBuffer outLine,
+ List extraOutLines)
+ {
+ int extraIndent = -1;
+
+ // Class name -> obfuscated method names.
+ Map methodMap = (Map)classMethodMap.get(className);
+ if (methodMap != null)
+ {
+ // Obfuscated method names -> methods.
+ Set methodSet = (Set)methodMap.get(obfuscatedMethodName);
+ if (methodSet != null)
+ {
+ // Find all matching methods.
+ Iterator methodInfoIterator = methodSet.iterator();
+ while (methodInfoIterator.hasNext())
+ {
+ MethodInfo methodInfo = (MethodInfo)methodInfoIterator.next();
+ if (methodInfo.matches(lineNumber, type, arguments))
+ {
+ // Is this the first matching method?
+ if (extraIndent < 0)
+ {
+ extraIndent = outLine.length();
+
+ // Append the first original name.
+ if (verbose)
+ {
+ outLine.append(methodInfo.type).append(' ');
+ }
+ outLine.append(methodInfo.originalName);
+ if (verbose)
+ {
+ outLine.append('(').append(methodInfo.arguments).append(')');
+ }
+ }
+ else
+ {
+ // Create an additional line with the proper
+ // indentation.
+ StringBuffer extraBuffer = new StringBuffer();
+ for (int counter = 0; counter < extraIndent; counter++)
+ {
+ extraBuffer.append(' ');
+ }
+
+ // Append the alternative name.
+ if (verbose)
+ {
+ extraBuffer.append(methodInfo.type).append(' ');
+ }
+ extraBuffer.append(methodInfo.originalName);
+ if (verbose)
+ {
+ extraBuffer.append('(').append(methodInfo.arguments).append(')');
+ }
+
+ // Store the additional line.
+ extraOutLines.add(extraBuffer);
+ }
+ }
+ }
+ }
+ }
+
+ // Just append the obfuscated name if we haven't found any matching
+ // methods.
+ if (extraIndent < 0)
+ {
+ outLine.append(obfuscatedMethodName);
+ }
+ }
+
+
+ /**
+ * Returns the original argument types.
+ */
+ private String originalArguments(String obfuscatedArguments)
+ {
+ StringBuffer originalArguments = new StringBuffer();
+
+ int startIndex = 0;
+ while (true)
+ {
+ int endIndex = obfuscatedArguments.indexOf(',', startIndex);
+ if (endIndex < 0)
+ {
+ break;
+ }
+
+ originalArguments.append(originalType(obfuscatedArguments.substring(startIndex, endIndex).trim())).append(',');
+
+ startIndex = endIndex + 1;
+ }
+
+ originalArguments.append(originalType(obfuscatedArguments.substring(startIndex).trim()));
+
+ return originalArguments.toString();
+ }
+
+
+ /**
+ * Returns the original type.
+ */
+ private String originalType(String obfuscatedType)
+ {
+ int index = obfuscatedType.indexOf('[');
+
+ return index >= 0 ?
+ originalClassName(obfuscatedType.substring(0, index)) + obfuscatedType.substring(index) :
+ originalClassName(obfuscatedType);
+ }
+
+
+ /**
+ * Returns the original class name.
+ */
+ private String originalClassName(String obfuscatedClassName)
+ {
+ String originalClassName = (String)classMap.get(obfuscatedClassName);
+
+ return originalClassName != null ?
+ originalClassName :
+ obfuscatedClassName;
+ }
+
+
+ // Implementations for MappingProcessor.
+
+ public boolean processClassMapping(String className, String newClassName)
+ {
+ // Obfuscated class name -> original class name.
+ classMap.put(newClassName, className);
+
+ return true;
+ }
+
+
+ public void processFieldMapping(String className, String fieldType, String fieldName, String newFieldName)
+ {
+ // Original class name -> obfuscated field names.
+ Map fieldMap = (Map)classFieldMap.get(className);
+ if (fieldMap == null)
+ {
+ fieldMap = new HashMap();
+ classFieldMap.put(className, fieldMap);
+ }
+
+ // Obfuscated field name -> fields.
+ Set fieldSet = (Set)fieldMap.get(newFieldName);
+ if (fieldSet == null)
+ {
+ fieldSet = new LinkedHashSet();
+ fieldMap.put(newFieldName, fieldSet);
+ }
+
+ // Add the field information.
+ fieldSet.add(new FieldInfo(fieldType,
+ fieldName));
+ }
+
+
+ public void processMethodMapping(String className, int firstLineNumber, int lastLineNumber, String methodReturnType, String methodName, String methodArguments, String newMethodName)
+ {
+ // Original class name -> obfuscated method names.
+ Map methodMap = (Map)classMethodMap.get(className);
+ if (methodMap == null)
+ {
+ methodMap = new HashMap();
+ classMethodMap.put(className, methodMap);
+ }
+
+ // Obfuscated method name -> methods.
+ Set methodSet = (Set)methodMap.get(newMethodName);
+ if (methodSet == null)
+ {
+ methodSet = new LinkedHashSet();
+ methodMap.put(newMethodName, methodSet);
+ }
+
+ // Add the method information.
+ methodSet.add(new MethodInfo(firstLineNumber,
+ lastLineNumber,
+ methodReturnType,
+ methodArguments,
+ methodName));
+ }
+
+
+ /**
+ * A field record.
+ */
+ private static class FieldInfo
+ {
+ private String type;
+ private String originalName;
+
+
+ private FieldInfo(String type, String originalName)
+ {
+ this.type = type;
+ this.originalName = originalName;
+ }
+
+
+ private boolean matches(String type)
+ {
+ return
+ type == null || type.equals(this.type);
+ }
+ }
+
+
+ /**
+ * A method record.
+ */
+ private static class MethodInfo
+ {
+ private int firstLineNumber;
+ private int lastLineNumber;
+ private String type;
+ private String arguments;
+ private String originalName;
+
+
+ private MethodInfo(int firstLineNumber, int lastLineNumber, String type, String arguments, String originalName)
+ {
+ this.firstLineNumber = firstLineNumber;
+ this.lastLineNumber = lastLineNumber;
+ this.type = type;
+ this.arguments = arguments;
+ this.originalName = originalName;
+ }
+
+
+ private boolean matches(int lineNumber, String type, String arguments)
+ {
+ return
+ (lineNumber == 0 || (firstLineNumber <= lineNumber && lineNumber <= lastLineNumber) || lastLineNumber == 0) &&
+ (type == null || type.equals(this.type)) &&
+ (arguments == null || arguments.equals(this.arguments));
+ }
+ }
+
+
+ /**
+ * The main program for ReTrace.
+ */
+ public static void main(String[] args)
+ {
+ if (args.length < 1)
+ {
+ System.err.println("Usage: java proguard.ReTrace [-verbose] <mapping_file> [<stacktrace_file>]");
+ System.exit(-1);
+ }
+
+ String regularExpresssion = STACK_TRACE_EXPRESSION;
+ boolean verbose = false;
+
+ int argumentIndex = 0;
+ while (argumentIndex < args.length)
+ {
+ String arg = args[argumentIndex];
+ if (arg.equals(REGEX_OPTION))
+ {
+ regularExpresssion = args[++argumentIndex];
+ }
+ else if (arg.equals(VERBOSE_OPTION))
+ {
+ verbose = true;
+ }
+ else
+ {
+ break;
+ }
+
+ argumentIndex++;
+ }
+
+ if (argumentIndex >= args.length)
+ {
+ System.err.println("Usage: java proguard.ReTrace [-regex <regex>] [-verbose] <mapping_file> [<stacktrace_file>]");
+ System.exit(-1);
+ }
+
+ File mappingFile = new File(args[argumentIndex++]);
+ File stackTraceFile = argumentIndex < args.length ?
+ new File(args[argumentIndex]) :
+ null;
+
+ ReTrace reTrace = new ReTrace(regularExpresssion, verbose, mappingFile, stackTraceFile);
+
+ try
+ {
+ // Execute ReTrace with its given settings.
+ reTrace.execute();
+ }
+ catch (IOException ex)
+ {
+ if (verbose)
+ {
+ // Print a verbose stack trace.
+ ex.printStackTrace();
+ }
+ else
+ {
+ // Print just the stack trace message.
+ System.err.println("Error: "+ex.getMessage());
+ }
+
+ System.exit(1);
+ }
+
+ System.exit(0);
+ }
+}