blob: 941bd0a75594ae24151bc652c3d27afcd7bd3d7e [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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.jack.shrob.obfuscation;
import com.google.common.primitives.Chars;
import com.android.jack.Jack;
import com.android.jack.JackIOException;
import com.android.jack.ir.ast.CanBeRenamed;
import com.android.jack.ir.ast.HasName;
import com.android.jack.ir.ast.JClassOrInterface;
import com.android.jack.ir.ast.JDefinedClassOrInterface;
import com.android.jack.ir.ast.JField;
import com.android.jack.ir.ast.JMethod;
import com.android.jack.ir.ast.JPackage;
import com.android.jack.ir.ast.JSession;
import com.android.jack.ir.ast.JType;
import com.android.jack.ir.ast.JTypeLookupException;
import com.android.jack.lookup.JLookupException;
import com.android.jack.lookup.JMethodLookupException;
import com.android.jack.lookup.JNodeLookup;
import com.android.jack.shrob.proguard.GrammarActions;
import com.android.jack.transformations.request.ChangeEnclosingPackage;
import com.android.jack.transformations.request.Rename;
import com.android.jack.transformations.request.TransformationRequest;
import com.android.jack.util.NamingTools;
import com.android.sched.marker.AbstractMarkerManager;
import com.android.sched.schedulable.Transform;
import com.android.sched.util.log.LoggerFactory;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
/**
* A class that parses a mapping file and rename the remapped nodes.
*/
@Transform(add = {OriginalNameMarker.class, OriginalPackageMarker.class, KeepNameMarker.class})
public class MappingApplier {
@Nonnull
private static final char[] EMPTY_STOP_CHARS = new char[] {};
@Nonnull
private static final char[] CLASSINFO_STOP_CHARS = new char[] {':'};
@Nonnull
private static final char[] BEGIN_PARAMETER_STOP_CHARS = new char[] {'('};
@Nonnull
private static final char[] END_PARAMETER_STOP_CHARS = new char[] {',', ')'};
@Nonnull
protected final Logger logger = LoggerFactory.getLogger();
@Nonnull
private final TransformationRequest request;
public MappingApplier(@Nonnull TransformationRequest request) {
this.request = request;
}
private static boolean isClassInfo(@Nonnull String line) {
// A class info line ends with ':'.
return line.charAt(line.length() - 1) == ':';
}
private static boolean isMethodInfo(@Nonnull String line) {
// A method info line contains '('.
return line.indexOf('(') != -1;
}
private void throwException(@Nonnull File mappingFile, int lineNumber, @Nonnull String message)
throws JackIOException {
throw new JackIOException(mappingFile.getAbsolutePath() + ":" + lineNumber + ":" + message);
}
@CheckForNull
private JDefinedClassOrInterface createMappingForType(@Nonnull String oldName,
@Nonnull String newName, @Nonnull JSession session, @Nonnull File mappingFile,
int lineNumber) {
JClassOrInterface type = null;
JNodeLookup lookup = session.getLookup();
try {
String typeSignature = NamingTools.getTypeSignatureName(oldName);
type = (JClassOrInterface) lookup.getType(typeSignature);
if (!session.getTypesToEmit().contains(type)) {
logger.log(Level.WARNING, "{0}:{1}: Type {2} has a mapping but was removed",
new Object[] {mappingFile.getAbsolutePath(), Integer.valueOf(lineNumber), oldName});
return null;
}
} catch (JLookupException e) {
logger.log(Level.WARNING, "{0}:{1}: Type {2} not found",
new Object[] {mappingFile.getAbsolutePath(), Integer.valueOf(lineNumber), oldName});
}
if (type instanceof JDefinedClassOrInterface) {
JDefinedClassOrInterface clOrI = (JDefinedClassOrInterface) type;
int indexOfNewSimpleName = newName.lastIndexOf('.');
String newSimpleName, newPackageName;
if (indexOfNewSimpleName == -1) {
newPackageName = "";
newSimpleName = newName;
} else {
newPackageName = newName.substring(0, indexOfNewSimpleName).replace('.', '/');
newSimpleName = newName.substring(indexOfNewSimpleName + 1, newName.length());
}
clOrI.addMarker(new OriginalPackageMarker(clOrI.getEnclosingPackage()));
JPackage newEnclosingPackage = lookup.getOrCreatePackage(newPackageName);
request.append(new ChangeEnclosingPackage(clOrI, newEnclosingPackage));
while (newEnclosingPackage != null) {
if (!newEnclosingPackage.containsMarker(KeepNameMarker.class)) {
newEnclosingPackage.addMarker(new KeepNameMarker());
}
newEnclosingPackage = newEnclosingPackage.getEnclosingPackage();
}
rename(clOrI, mappingFile, lineNumber, newSimpleName);
return clOrI;
}
return null;
}
private int readLineInfo(@Nonnull String line, int index) {
char c = line.charAt(index);
while (Character.isDigit(c) || c == ':') {
index++;
c = line.charAt(index);
}
return index;
}
private int readName(@Nonnull String line, int index, @Nonnull char[] stopChars) {
int length = line.length();
char c = line.charAt(index);
while (!Character.isWhitespace(c) && Chars.indexOf(stopChars, c) == -1) {
if (++index < length) {
c = line.charAt(index);
} else {
break;
}
}
return index;
}
/**
* Reads this string until a whitespace or the '->' separator is read
* or until the end of the string, starting the search at the specified index.
* @param line the line to read from.
* @param index the index to start the reading from.
* @return the index of the first occurrence of a whitespace, the '->' separator
* or the end of the string.
*/
private int readNameUntilSeparatorOrWhitespace(@Nonnull String line, int index) {
int length = line.length();
char c = line.charAt(index);
while (!Character.isWhitespace(c)) {
if (c == '-' && line.charAt(index + 1) == '>') {
// We check the second char to avoid bad parsing of names containing '-'
break;
}
if (++index < length) {
c = line.charAt(index);
} else {
break;
}
}
return index;
}
private int readWhiteSpaces(@Nonnull String line, int index) {
char c = line.charAt(index);
while (Character.isWhitespace(c)) {
index++;
c = line.charAt(index);
}
return index;
}
private int readSeparator(
@Nonnull String line, int index, @Nonnull File mappingFile, int lineNumber) {
if (line.charAt(index) != '-' || line.charAt(index + 1) != '>') {
throwException(mappingFile, lineNumber,
"The mapping file is badly formatted (separator \"->\" expected)");
}
return index + 2;
}
@CheckForNull
private JDefinedClassOrInterface readClassInfo(
@Nonnull String line, @Nonnull JSession session, @Nonnull File mappingFile, int lineNumber) {
// qualifiedOldClassName -> newClassName:
try {
int startIndex = readWhiteSpaces(line, 0);
int endIndex = readNameUntilSeparatorOrWhitespace(line, startIndex);
String qualifiedOldClassName = line.substring(startIndex, endIndex);
startIndex = readWhiteSpaces(line, endIndex);
startIndex = readSeparator(line, startIndex, mappingFile, lineNumber);
startIndex = readWhiteSpaces(line, startIndex);
endIndex = readName(line, startIndex, CLASSINFO_STOP_CHARS);
String newClassName = line.substring(startIndex, endIndex);
return createMappingForType(
qualifiedOldClassName, newClassName, session, mappingFile, lineNumber);
} catch (ArrayIndexOutOfBoundsException e) {
throwException(
mappingFile, lineNumber, "The mapping file is badly formatted (class mapping expected)");
return null;
}
}
@CheckForNull
private JField findField(@Nonnull JDefinedClassOrInterface currentType,
@Nonnull String oldName, @Nonnull String typeSignature) {
List<JField> fields = currentType.getFields(oldName);
for (JField field : fields) {
if (GrammarActions.getSignatureFormatter().getName(field.getType()).equals(typeSignature)) {
return field;
}
}
return null;
}
private void readFieldInfo(@Nonnull String line,
@Nonnull JDefinedClassOrInterface currentType, @Nonnull File mappingFile, int lineNumber) {
// type oldFieldName -> newFieldName
try {
int startIndex = readWhiteSpaces(line, 0);
int endIndex = readName(line, startIndex, EMPTY_STOP_CHARS);
String typeSignature = GrammarActions.getSignature(line.substring(startIndex, endIndex));
startIndex = readWhiteSpaces(line, endIndex);
endIndex = readNameUntilSeparatorOrWhitespace(line, startIndex);
String oldName = line.substring(startIndex, endIndex);
int index = readWhiteSpaces(line, endIndex);
index = readSeparator(line, index, mappingFile, lineNumber);
startIndex = readWhiteSpaces(line, index);
endIndex = readName(line, startIndex, EMPTY_STOP_CHARS);
String newName = line.substring(startIndex, endIndex);
JField field = findField(currentType, oldName, typeSignature);
if (field != null) {
renameField(field, mappingFile, lineNumber, newName);
} else {
logger.log(Level.WARNING, "{0}:{1}: Field {2} not found in {3}", new Object[] {
mappingFile.getAbsolutePath(), Integer.valueOf(lineNumber), oldName,
Jack.getUserFriendlyFormatter().getName(currentType)});
}
} catch (ArrayIndexOutOfBoundsException e) {
throwException(
mappingFile, lineNumber, "The mapping file is badly formatted (field mapping expected)");
}
}
private void rename(@Nonnull CanBeRenamed renamable, @Nonnull File mappingFile, int lineNumber,
@Nonnull String newName) {
AbstractMarkerManager markerManager = (AbstractMarkerManager) renamable;
if (!markerManager.containsMarker(OriginalNameMarker.class)) {
markerManager.addMarker(new OriginalNameMarker(((HasName) renamable).getName()));
request.append(new Rename(renamable, newName));
}
}
protected void renameField(@Nonnull JField field, @Nonnull File mappingFile, int lineNumber,
@Nonnull String newName) {
rename(field.getId(), mappingFile, lineNumber, newName);
}
private int readChar(@Nonnull String line, int index, char expectedChar,
@Nonnull File mappingFile, int lineNumber) {
if (line.charAt(index) != expectedChar) {
throwException(mappingFile, lineNumber,
"The mapping file is badly formatted (\'" + expectedChar + "\' expected)");
}
return index + 1;
}
private void readMethodInfo(@Nonnull String line,
@Nonnull JDefinedClassOrInterface currentType, @Nonnull File mappingFile,
int lineNumber, @Nonnull JNodeLookup lookup) {
// (startLineInfo:endLineInfo:)? type oldMethodName\((type(, type)*)?\) -> newMethodName
try {
int startIndex = readWhiteSpaces(line, 0);
startIndex = readLineInfo(line, startIndex);
startIndex = readWhiteSpaces(line, startIndex);
int endIndex = readName(line, startIndex, EMPTY_STOP_CHARS);
String typeSignature = GrammarActions.getSignature(line.substring(startIndex, endIndex));
JType returnType = lookup.getType(typeSignature);
startIndex = readWhiteSpaces(line, endIndex);
endIndex = readName(line, startIndex, BEGIN_PARAMETER_STOP_CHARS);
String oldName = line.substring(startIndex, endIndex);
startIndex = readChar(line, endIndex, '(', mappingFile, lineNumber);
List<JType> args = new ArrayList<JType>();
startIndex = readWhiteSpaces(line, startIndex);
while (line.charAt(startIndex) != ')') {
endIndex = readName(line, startIndex, END_PARAMETER_STOP_CHARS);
String parameterType = GrammarActions.getSignature(line.substring(startIndex, endIndex));
args.add(lookup.getType(parameterType));
startIndex = readWhiteSpaces(line, endIndex);
if (line.charAt(startIndex) == ')') {
break;
}
startIndex = readChar(line, startIndex, ',', mappingFile, lineNumber);
startIndex = readWhiteSpaces(line, startIndex);
}
startIndex++;
startIndex = readWhiteSpaces(line, startIndex);
startIndex = readSeparator(line, startIndex, mappingFile, lineNumber);
startIndex = readWhiteSpaces(line, startIndex);
endIndex = readName(line, startIndex, EMPTY_STOP_CHARS);
String newName = line.substring(startIndex, endIndex);
try {
JMethod method = currentType.getMethod(oldName, returnType, args);
renameMethod(method, mappingFile, lineNumber, newName);
} catch (JMethodLookupException e) {
logger.log(Level.WARNING, "{0}:{1}: Method {2} not found in {3}", new Object[] {
mappingFile.getAbsolutePath(), Integer.valueOf(lineNumber), oldName,
Jack.getUserFriendlyFormatter().getName(currentType)});
}
} catch (ArrayIndexOutOfBoundsException e) {
throwException(
mappingFile, lineNumber, "The mapping file is badly formatted (method mapping expected)");
} catch (JTypeLookupException e) {
logger.log(Level.WARNING, "{0}:{1}: {2}", new Object[]{mappingFile.getAbsolutePath(),
Integer.valueOf(lineNumber), e.getMessage()});
}
}
protected void renameMethod(
@Nonnull JMethod method, @Nonnull File mappingFile, int lineNumber, @Nonnull String newName) {
String oldName = method.getName();
if (oldName.equals(NamingTools.INIT_NAME)) {
logger.log(Level.WARNING, "{0}:{1}: Constructors cannot be renamed",
new Object[] {mappingFile.getAbsolutePath(), Integer.valueOf(lineNumber)});
} else if (oldName.equals(NamingTools.STATIC_INIT_NAME)) {
logger.log(Level.WARNING, "{0}:{1}: Static initializers cannot be renamed",
new Object[] {mappingFile.getAbsolutePath(), Integer.valueOf(lineNumber)});
} else {
rename(method.getMethodId(), mappingFile, lineNumber, newName);
}
}
/**
* The mapping format must be: qualifiedOldClassName -> newClassName: type oldFieldName ->
* newFieldName (startLineInfo:endLineInfo:)? type oldMethodName\((type(, type)*)?\) ->
* newMethodName
*
* type must be in the java form (e.g. java.lang.String, boolean).
*
* @param mappingFile
* @param session
* @throws JackIOException
*/
public void applyMapping(@Nonnull File mappingFile, @Nonnull JSession session)
throws JackIOException {
LineNumberReader reader = null;
try {
reader = new LineNumberReader(new FileReader(mappingFile));
String line = reader.readLine();
JDefinedClassOrInterface currentType = null;
while (line != null) {
if (isClassInfo(line)) {
currentType = readClassInfo(line, session, mappingFile, reader.getLineNumber());
} else {
if (currentType != null) {
if (isMethodInfo(line)) {
readMethodInfo(line, currentType, mappingFile, reader.getLineNumber(),
session.getLookup());
} else {
readFieldInfo(line, currentType, mappingFile, reader.getLineNumber());
}
}
}
line = reader.readLine();
}
} catch (IOException e) {
throw new JackIOException("Error while reading mapping " + mappingFile.getAbsolutePath(), e);
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
logger.log(Level.WARNING,
"Failed to closed reader while reading mapping {0}", mappingFile.getAbsolutePath());
}
}
}
}
}