blob: cab8ec4678b9789f657c840c6c3075ecdb350414 [file] [log] [blame]
/*
* Copyright (C) 2016 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ahat.proguard;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A representation of a proguard mapping for deobfuscating class names,
* field names, and stack frames.
*/
public class ProguardMap {
private static final String ARRAY_SYMBOL = "[]";
private static final Version LINE_MAPPING_BEHAVIOR_CHANGE_VERSION = new Version(3, 1, 4);
private static class FrameData {
public FrameData(String clearMethodName) {
this.clearMethodName = clearMethodName;
}
private final String clearMethodName;
private final TreeMap<Integer, LineNumberMapping> lineNumbers = new TreeMap<>();
public int getClearLine(int obfuscatedLine) {
var lineNumberEntry = lineNumbers.floorEntry(obfuscatedLine);
LineNumberMapping mapping = lineNumberEntry == null ? null : lineNumberEntry.getValue();
if (mapping != null && mapping.hasObfuscatedLine(obfuscatedLine)) {
return mapping.mapObfuscatedLine(obfuscatedLine);
} else {
return obfuscatedLine;
}
}
}
private static class LineRange {
public LineRange(int start, int end) {
this.start = start;
this.end = end;
}
public boolean hasLine(int lineNumber) {
return (lineNumber >= start && lineNumber <= end);
}
public final int start;
public final int end;
}
private static class LineNumberMapping {
public LineNumberMapping(LineRange obfuscatedRange, LineRange clearRange) {
this.obfuscatedRange = obfuscatedRange;
this.clearRange = clearRange;
}
public boolean hasObfuscatedLine(int lineNumber) {
return obfuscatedRange.hasLine(lineNumber);
}
public int mapObfuscatedLine(int lineNumber) {
int mappedLine = clearRange.start + lineNumber - obfuscatedRange.start;
if (!clearRange.hasLine(mappedLine)) {
// If the mapped line ends out outside of range, it would be past the end, so just limit it
// to the end line
return clearRange.end;
}
return mappedLine;
}
public final LineRange obfuscatedRange;
public final LineRange clearRange;
}
private static class ClassData {
private final String mClearName;
// Mapping from obfuscated field name to clear field name.
private final Map<String, String> mFields = new HashMap<String, String>();
// obfuscatedMethodName + clearSignature -> FrameData
private final Map<String, FrameData> mFrames = new HashMap<String, FrameData>();
// Constructs a ClassData object for a class with the given clear name.
public ClassData(String clearName) {
mClearName = clearName;
}
// Returns the clear name of the class.
public String getClearName() {
return mClearName;
}
public void addField(String obfuscatedName, String clearName) {
mFields.put(obfuscatedName, clearName);
}
// Get the clear name for the field in this class with the given
// obfuscated name. Returns the original obfuscated name if a clear
// name for the field could not be determined.
// TODO: Do we need to take into account the type of the field to
// propery determine the clear name?
public String getField(String obfuscatedName) {
String clearField = mFields.get(obfuscatedName);
return clearField == null ? obfuscatedName : clearField;
}
public void addFrame(String obfuscatedMethodName, String clearMethodName,
String clearSignature, LineRange obfuscatedLine, LineRange clearRange) {
String key = obfuscatedMethodName + clearSignature;
FrameData data = mFrames.get(key);
if (data == null) {
data = new FrameData(clearMethodName);
}
data.lineNumbers.put(
obfuscatedLine.start, new LineNumberMapping(obfuscatedLine, clearRange));
mFrames.put(key, data);
}
public Frame getFrame(String clearClassName, String obfuscatedMethodName,
String clearSignature, String obfuscatedFilename, int obfuscatedLine) {
String key = obfuscatedMethodName + clearSignature;
FrameData frame = mFrames.get(key);
if (frame == null) {
frame = new FrameData(obfuscatedMethodName);
}
return new Frame(frame.clearMethodName, clearSignature,
getFileName(clearClassName), frame.getClearLine(obfuscatedLine));
}
}
private Map<String, ClassData> mClassesFromClearName = new HashMap<String, ClassData>();
private Map<String, ClassData> mClassesFromObfuscatedName = new HashMap<String, ClassData>();
/**
* Information associated with a stack frame that identifies a particular
* line of source code.
*/
public static class Frame {
Frame(String method, String signature, String filename, int line) {
this.method = method;
this.signature = signature;
this.filename = filename;
this.line = line;
}
/**
* The name of the method the stack frame belongs to.
* For example, "equals".
*/
public final String method;
/**
* The signature of the method the stack frame belongs to.
* For example, "(Ljava/lang/Object;)Z".
*/
public final String signature;
/**
* The name of the file with containing the line of source that the stack
* frame refers to.
*/
public final String filename;
/**
* The line number of the code in the source file that the stack frame
* refers to.
*/
public final int line;
}
private static void parseException(String msg) throws ParseException {
throw new ParseException(msg, 0);
}
/**
* Creates a new empty proguard mapping.
* The {@link #readFromFile readFromFile} and
* {@link #readFromReader readFromReader} methods can be used to populate
* the proguard mapping with proguard mapping information.
*/
public ProguardMap() {
}
/**
* Adds the proguard mapping information in <code>mapFile</code> to this
* proguard mapping.
* The <code>mapFile</code> should be a proguard mapping file generated with
* the <code>-printmapping</code> option when proguard was run.
*
* @param mapFile the name of a file with proguard mapping information
* @throws FileNotFoundException If the <code>mapFile</code> could not be
* found
* @throws IOException If an input exception occurred.
* @throws ParseException If the <code>mapFile</code> is not a properly
* formatted proguard mapping file.
*/
public void readFromFile(File mapFile)
throws FileNotFoundException, IOException, ParseException {
readFromReader(new FileReader(mapFile));
}
/**
* Adds the proguard mapping information read from <code>mapReader</code> to
* this proguard mapping.
* <code>mapReader</code> should be a Reader of a proguard mapping file
* generated with the <code>-printmapping</code> option when proguard was run.
*
* @param mapReader a Reader for reading the proguard mapping information
* @throws IOException If an input exception occurred.
* @throws ParseException If the <code>mapFile</code> is not a properly
* formatted proguard mapping file.
*/
public void readFromReader(Reader mapReader) throws IOException, ParseException {
Version compilerVersion = new Version(0, 0, 0);
BufferedReader reader = new BufferedReader(mapReader);
String line = reader.readLine();
while (line != null) {
// Skip comment lines.
if (isCommentLine(line)) {
compilerVersion = tryParseVersion(line, compilerVersion);
line = reader.readLine();
continue;
}
// Class lines are of the form:
// 'clear.class.name -> obfuscated_class_name:'
int sep = line.indexOf(" -> ");
if (sep == -1 || sep + 5 >= line.length()) {
parseException("Error parsing class line: '" + line + "'");
}
String clearClassName = line.substring(0, sep);
String obfuscatedClassName = line.substring(sep + 4, line.length() - 1);
ClassData classData = new ClassData(clearClassName);
mClassesFromClearName.put(clearClassName, classData);
mClassesFromObfuscatedName.put(obfuscatedClassName, classData);
// After the class line comes zero or more field/method lines of the form:
// ' type clearName -> obfuscatedName'
// '# comment line'
line = reader.readLine();
while (line != null && (line.startsWith(" ") || isCommentLine(line))) {
String trimmed = line.trim();
// Comment lines may occur anywhere in the file.
// Skip over them.
if (isCommentLine(trimmed)) {
line = reader.readLine();
continue;
}
int ws = trimmed.indexOf(' ');
sep = trimmed.indexOf(" -> ");
if (ws == -1 || sep == -1) {
parseException("Error parse field/method line: '" + line + "'");
}
String type = trimmed.substring(0, ws);
String clearName = trimmed.substring(ws + 1, sep);
String obfuscatedName = trimmed.substring(sep + 4, trimmed.length());
// If the clearName contains '(', then this is for a method instead of a
// field.
if (clearName.indexOf('(') == -1) {
classData.addField(obfuscatedName, clearName);
} else {
// For methods, the type is of the form: [#:[#:]]<returnType>
int obfuscatedLineStart = 0;
// The end of the obfuscated line range.
// If line does not contain explicit end range, e.g #:, it is equivalent to #:#:
int obfuscatedLineEnd = 0;
int colon = type.indexOf(':');
if (colon != -1) {
obfuscatedLineStart = Integer.parseInt(type.substring(0, colon));
obfuscatedLineEnd = obfuscatedLineStart;
type = type.substring(colon + 1);
}
colon = type.indexOf(':');
if (colon != -1) {
obfuscatedLineEnd = Integer.parseInt(type.substring(0, colon));
type = type.substring(colon + 1);
}
LineRange obfuscatedRange = new LineRange(obfuscatedLineStart, obfuscatedLineEnd);
// For methods, the clearName is of the form: <clearName><sig>[:#[:#]]
int op = clearName.indexOf('(');
int cp = clearName.indexOf(')');
if (op == -1 || cp == -1) {
parseException("Error parse method line: '" + line + "'");
}
String sig = clearName.substring(op, cp + 1);
int clearLineStart = obfuscatedRange.start;
int clearLineEnd = obfuscatedRange.end;
colon = clearName.lastIndexOf(':');
if (colon != -1) {
if (compilerVersion.compareTo(LINE_MAPPING_BEHAVIOR_CHANGE_VERSION) < 0) {
// Before v3.1.4 if only one clear line was present, that implied a range equal to the
// obfuscated line range
clearLineStart = Integer.parseInt(clearName.substring(colon + 1));
clearLineEnd = clearLineStart + obfuscatedRange.end - obfuscatedRange.start;
} else {
// From v3.1.4 if only one clear line was present, that implies that all lines map to
// a single clear line
clearLineEnd = Integer.parseInt(clearName.substring(colon + 1));
clearLineStart = clearLineEnd;
}
clearName = clearName.substring(0, colon);
}
colon = clearName.lastIndexOf(':');
if (colon != -1) {
clearLineStart = Integer.parseInt(clearName.substring(colon + 1));
clearName = clearName.substring(0, colon);
}
LineRange clearRange = new LineRange(clearLineStart, clearLineEnd);
clearName = clearName.substring(0, op);
String clearSig = fromProguardSignature(sig + type);
classData.addFrame(obfuscatedName, clearName, clearSig, obfuscatedRange, clearRange);
}
line = reader.readLine();
}
}
reader.close();
}
private static class Version implements Comparable<Version> {
final int major;
final int minor;
final int build;
public Version(int major, int minor, int build) {
this.major = major;
this.minor = minor;
this.build = build;
}
@Override
public int compareTo(Version other) {
int compare = Integer.compare(this.major, other.major);
if (compare == 0) {
compare = Integer.compare(this.minor, other.minor);
}
if (compare == 0) {
compare = Integer.compare(this.build, other.build);
}
return compare;
}
}
private boolean isCommentLine(String line) {
// Comment lines start with '#' and my have leading whitespaces.
return line.trim().startsWith("#");
}
private Version tryParseVersion(String line, Version old) {
Pattern pattern = Pattern.compile("#\\s*compiler_version:\\s*(\\d+).(\\d+).(?:(\\d+))?");
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
String buildStr = matcher.group(3);
if (buildStr == null) {
buildStr = Integer.toString(0);
}
return new Version(
Integer.parseInt(matcher.group(1)),
Integer.parseInt(matcher.group(2)),
Integer.parseInt(buildStr));
}
return old;
}
/**
* Returns the deobfuscated version of the given obfuscated class name.
* If this proguard mapping does not include information about how to
* deobfuscate the obfuscated class name, the obfuscated class name
* is returned.
*
* @param obfuscatedClassName the obfuscated class name to deobfuscate
* @return the deobfuscated class name.
*/
public String getClassName(String obfuscatedClassName) {
// Class names for arrays may have trailing [] that need to be
// stripped before doing the lookup.
String baseName = obfuscatedClassName;
String arraySuffix = "";
while (baseName.endsWith(ARRAY_SYMBOL)) {
arraySuffix += ARRAY_SYMBOL;
baseName = baseName.substring(0, baseName.length() - ARRAY_SYMBOL.length());
}
ClassData classData = mClassesFromObfuscatedName.get(baseName);
String clearBaseName = classData == null ? baseName : classData.getClearName();
return clearBaseName + arraySuffix;
}
/**
* Returns the deobfuscated version of the obfuscated field name for the
* given deobfuscated class name.
* If this proguard mapping does not include information about how to
* deobfuscate the obfuscated field name, the obfuscated field name is
* returned.
*
* @param clearClass the deobfuscated name of the class the field belongs to
* @param obfuscatedField the obfuscated field name to deobfuscate
* @return the deobfuscated field name.
*/
public String getFieldName(String clearClass, String obfuscatedField) {
ClassData classData = mClassesFromClearName.get(clearClass);
if (classData == null) {
return obfuscatedField;
}
return classData.getField(obfuscatedField);
}
/**
* Returns the deobfuscated version of the obfuscated stack frame
* information for the given deobfuscated class name.
* If this proguard mapping does not include information about how to
* deobfuscate the obfuscated stack frame information, the obfuscated stack
* frame information is returned.
*
* @param clearClassName the deobfuscated name of the class the stack frame's
* method belongs to
* @param obfuscatedMethodName the obfuscated method name to deobfuscate
* @param obfuscatedSignature the obfuscated method signature to deobfuscate
* @param obfuscatedFilename the obfuscated file name to deobfuscate.
* @param obfuscatedLine the obfuscated line number to deobfuscate.
* @return the deobfuscated stack frame information.
*/
public Frame getFrame(String clearClassName, String obfuscatedMethodName,
String obfuscatedSignature, String obfuscatedFilename, int obfuscatedLine) {
String clearSignature = getSignature(obfuscatedSignature);
ClassData classData = mClassesFromClearName.get(clearClassName);
if (classData == null) {
return new Frame(obfuscatedMethodName, clearSignature,
obfuscatedFilename, obfuscatedLine);
}
return classData.getFrame(clearClassName, obfuscatedMethodName, clearSignature,
obfuscatedFilename, obfuscatedLine);
}
// Converts a proguard-formatted method signature into a Java formatted
// method signature.
private static String fromProguardSignature(String sig) throws ParseException {
if (sig.startsWith("(")) {
int end = sig.indexOf(')');
if (end == -1) {
parseException("Error parsing signature: " + sig);
}
StringBuilder converted = new StringBuilder();
converted.append('(');
if (end > 1) {
for (String arg : sig.substring(1, end).split(",")) {
converted.append(fromProguardSignature(arg));
}
}
converted.append(')');
converted.append(fromProguardSignature(sig.substring(end + 1)));
return converted.toString();
} else if (sig.endsWith(ARRAY_SYMBOL)) {
return "[" + fromProguardSignature(sig.substring(0, sig.length() - 2));
} else if (sig.equals("boolean")) {
return "Z";
} else if (sig.equals("byte")) {
return "B";
} else if (sig.equals("char")) {
return "C";
} else if (sig.equals("short")) {
return "S";
} else if (sig.equals("int")) {
return "I";
} else if (sig.equals("long")) {
return "J";
} else if (sig.equals("float")) {
return "F";
} else if (sig.equals("double")) {
return "D";
} else if (sig.equals("void")) {
return "V";
} else {
return "L" + sig.replace('.', '/') + ";";
}
}
// Return a clear signature for the given obfuscated signature.
private String getSignature(String obfuscatedSig) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < obfuscatedSig.length(); i++) {
if (obfuscatedSig.charAt(i) == 'L') {
int e = obfuscatedSig.indexOf(';', i);
builder.append('L');
String cls = obfuscatedSig.substring(i + 1, e).replace('/', '.');
builder.append(getClassName(cls).replace('.', '/'));
builder.append(';');
i = e;
} else {
builder.append(obfuscatedSig.charAt(i));
}
}
return builder.toString();
}
// Return a file name for the given clear class name.
private static String getFileName(String clearClass) {
String filename = clearClass;
int dot = filename.lastIndexOf('.');
if (dot != -1) {
filename = filename.substring(dot + 1);
}
int dollar = filename.indexOf('$');
if (dollar != -1) {
filename = filename.substring(0, dollar);
}
return filename + ".java";
}
}