| /* |
| * 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"; |
| |
| // BEGIN android-changed |
| // Use regex from latest version (4.9) because it is |
| // able to handle Android Bugreport format |
| public static final String STACK_TRACE_EXPRESSION = "(?:.*?\\bat\\s+%c.%m\\s*\\(.*?(?::%l)?\\)\\s*)|(?:(?:.*?[:\"]\\s+)?%c(?::.*)?)"; |
| // END android-changed |
| |
| 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); |
| } |
| } |