| /* |
| * 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()); |
| } |
| } |
| } |
| } |
| |
| } |