blob: 632aa6fd3a5f4f81735d5d599bfe09151d948653 [file] [log] [blame]
// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.naming;
import static com.android.tools.r8.utils.DescriptorUtils.getClassBinaryNameFromDescriptor;
import static com.android.tools.r8.utils.DescriptorUtils.getDescriptorFromClassBinaryName;
import static com.android.tools.r8.utils.DescriptorUtils.getPackageBinaryNameFromJavaType;
import com.android.tools.r8.graph.DexAnnotation;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexValue;
import com.android.tools.r8.graph.DexValue.DexValueArray;
import com.android.tools.r8.graph.DexValue.DexValueString;
import com.android.tools.r8.graph.DexValue.DexValueType;
import com.android.tools.r8.naming.signature.GenericSignatureAction;
import com.android.tools.r8.naming.signature.GenericSignatureParser;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.android.tools.r8.shaking.RootSetBuilder.RootSet;
import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
class ClassNameMinifier {
private final AppInfoWithLiveness appInfo;
private final RootSet rootSet;
private final PackageObfuscationMode packageObfuscationMode;
private final Set<DexString> usedTypeNames = Sets.newIdentityHashSet();
private final Map<DexType, DexString> renaming = Maps.newIdentityHashMap();
private final Map<String, ClassNamingState> states = new HashMap<>();
private final List<String> dictionary;
private final boolean keepInnerClassStructure;
private final ClassNamingState topLevelState;
private GenericSignatureRewriter genericSignatureRewriter = new GenericSignatureRewriter();
private GenericSignatureParser<DexType> genericSignatureParser =
new GenericSignatureParser<>(genericSignatureRewriter);
ClassNameMinifier(
AppInfoWithLiveness appInfo,
RootSet rootSet,
PackageObfuscationMode packageObfuscationMode,
String packagePrefix,
List<String> dictionary,
boolean keepInnerClassStructure) {
this.appInfo = appInfo;
this.rootSet = rootSet;
this.packageObfuscationMode = packageObfuscationMode;
this.dictionary = dictionary;
this.keepInnerClassStructure = keepInnerClassStructure;
// Initialize top-level naming state.
topLevelState = new ClassNamingState(getPackageBinaryNameFromJavaType(packagePrefix));
states.computeIfAbsent("", k -> topLevelState);
}
Map<DexType, DexString> computeRenaming(Timing timing) {
Iterable<DexProgramClass> classes = appInfo.classes();
// Collect names we have to keep.
timing.begin("reserve");
for (DexClass clazz : classes) {
if (rootSet.noObfuscation.contains(clazz)) {
assert !renaming.containsKey(clazz.type);
registerClassAsUsed(clazz.type);
}
}
timing.end();
timing.begin("rename-classes");
for (DexClass clazz : classes) {
if (!renaming.containsKey(clazz.type)) {
DexString renamed = computeName(clazz);
renaming.put(clazz.type, renamed);
}
}
timing.end();
timing.begin("rename-generic");
renameTypesInGenericSignatures();
timing.end();
timing.begin("rename-arrays");
appInfo.dexItemFactory.forAllTypes(this::renameArrayTypeIfNeeded);
timing.end();
return Collections.unmodifiableMap(renaming);
}
private void renameTypesInGenericSignatures() {
for (DexClass clazz : appInfo.classes()) {
rewriteGenericSignatures(clazz.annotations.annotations,
genericSignatureParser::parseClassSignature);
clazz.forEachField(field -> rewriteGenericSignatures(
field.annotations.annotations, genericSignatureParser::parseFieldSignature));
clazz.forEachMethod(method -> rewriteGenericSignatures(
method.annotations.annotations, genericSignatureParser::parseMethodSignature));
}
}
private void rewriteGenericSignatures(DexAnnotation[] annotations, Consumer<String> parser) {
for (int i = 0; i < annotations.length; i++) {
DexAnnotation annotation = annotations[i];
if (DexAnnotation.isSignatureAnnotation(annotation, appInfo.dexItemFactory)) {
parser.accept(getSignatureFromAnnotation(annotation));
annotations[i] = DexAnnotation.createSignatureAnnotation(
genericSignatureRewriter.getRenamedSignature(),
appInfo.dexItemFactory);
}
}
}
private static String getSignatureFromAnnotation(DexAnnotation signatureAnnotation) {
DexValueArray elements = (DexValueArray) signatureAnnotation.annotation.elements[0].value;
StringBuilder signature = new StringBuilder();
for (DexValue element : elements.getValues()) {
signature.append(((DexValueString) element).value.toString());
}
return signature.toString();
}
/**
* Registers the given type as used.
* <p>
* When {@link #keepInnerClassStructure} is true, keeping the name of an inner class will
* automatically also keep the name of the outer class, as otherwise the structure would be
* invalidated.
*/
private void registerClassAsUsed(DexType type) {
renaming.put(type, type.descriptor);
usedTypeNames.add(type.descriptor);
if (keepInnerClassStructure) {
DexType outerClass = getOutClassForType(type);
if (outerClass != null) {
if (!renaming.containsKey(outerClass)) {
// The outer class was not previously kept. We have to do this now.
registerClassAsUsed(outerClass);
}
}
}
}
private DexType getOutClassForType(DexType type) {
DexClass clazz = appInfo.definitionFor(type);
if (clazz == null) {
return null;
}
DexAnnotation annotation =
clazz.annotations.getFirstMatching(appInfo.dexItemFactory.annotationEnclosingClass);
if (annotation != null) {
assert annotation.annotation.elements.length == 1;
DexValue value = annotation.annotation.elements[0].value;
return ((DexValueType) value).value;
}
// We do not need to preserve the names for local or anonymous classes, as they do not result
// in a member type declaration and hence cannot be referenced as nested classes in
// method signatures.
// See https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.5.
return null;
}
private DexString computeName(DexClass clazz) {
ClassNamingState state = null;
if (keepInnerClassStructure) {
// When keeping the nesting structure of inner classes, we have to insert the name
// of the outer class for the $ prefix.
DexType outerClass = getOutClassForType(clazz.type);
if (outerClass != null) {
state = getStateForOuterClass(outerClass);
}
}
if (state == null) {
state = getStateFor(clazz);
}
return state.nextTypeName();
}
private ClassNamingState getStateFor(DexClass clazz) {
String packageName = getPackageBinaryNameFromJavaType(clazz.type.getPackageDescriptor());
// Check whether the given class should be kept.
if (rootSet.keepPackageName.contains(clazz)) {
return states.computeIfAbsent(packageName, ClassNamingState::new);
}
ClassNamingState state = topLevelState;
switch (packageObfuscationMode) {
case NONE:
// TODO(b/36799686): general obfuscation.
state = states.computeIfAbsent(packageName, ClassNamingState::new);
break;
case REPACKAGE:
// For repackaging, all classes are repackaged to a single package.
state = topLevelState;
break;
case FLATTEN:
// For flattening, all packages are repackaged to a single package.
state = states.computeIfAbsent(packageName, k -> {
String renamedPackageName =
getClassBinaryNameFromDescriptor(topLevelState.nextSuggestedName());
return new ClassNamingState(renamedPackageName);
});
break;
}
return state;
}
private ClassNamingState getStateForOuterClass(DexType outer) {
String prefix = getClassBinaryNameFromDescriptor(outer.toDescriptorString());
return states.computeIfAbsent(prefix, k -> {
// Create a naming state with this classes renaming as prefix.
DexString renamed = renaming.get(outer);
if (renamed == null) {
// The outer class has not been renamed yet, so rename the outer class first.
DexClass outerClass = appInfo.definitionFor(outer);
if (outerClass == null) {
renamed = outer.descriptor;
} else {
renamed = computeName(outerClass);
renaming.put(outer, renamed);
}
}
String binaryName = getClassBinaryNameFromDescriptor(renamed.toString());
return new ClassNamingState(binaryName, "$");
});
}
private void renameArrayTypeIfNeeded(DexType type) {
if (type.isArrayType()) {
DexType base = type.toBaseType(appInfo.dexItemFactory);
DexString value = renaming.get(base);
if (value != null) {
int dimensions = type.descriptor.numberOfLeadingSquareBrackets();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < dimensions; i++) {
builder.append('[');
}
builder.append(value.toString());
DexString descriptor = appInfo.dexItemFactory.createString(builder.toString());
renaming.put(type, descriptor);
}
}
}
private class ClassNamingState {
private final char[] packagePrefix;
private int typeCounter = 1;
private Iterator<String> dictionaryIterator;
ClassNamingState(String packageName) {
this(packageName, "/");
}
ClassNamingState(String packageName, String separator) {
this.packagePrefix = ("L" + packageName
// L or La/b/ (or La/b/C$)
+ (packageName.isEmpty() ? "" : separator))
.toCharArray();
this.dictionaryIterator = dictionary.iterator();
}
public char[] getPackagePrefix() {
return packagePrefix;
}
String nextSuggestedName() {
StringBuilder nextName = new StringBuilder();
if (dictionaryIterator.hasNext()) {
nextName.append(packagePrefix).append(dictionaryIterator.next()).append(';');
return nextName.toString();
} else {
return StringUtils.numberToIdentifier(packagePrefix, typeCounter++, true);
}
}
private DexString nextTypeName() {
DexString candidate;
do {
candidate = appInfo.dexItemFactory.createString(nextSuggestedName());
} while (usedTypeNames.contains(candidate));
return candidate;
}
}
private class GenericSignatureRewriter implements GenericSignatureAction<DexType> {
private StringBuilder renamedSignature;
public String getRenamedSignature() {
return renamedSignature.toString();
}
@Override
public void parsedSymbol(char symbol) {
renamedSignature.append(symbol);
}
@Override
public void parsedIdentifier(String identifier) {
renamedSignature.append(identifier);
}
@Override
public DexType parsedTypeName(String name) {
DexType type = appInfo.dexItemFactory.createType(getDescriptorFromClassBinaryName(name));
DexString renamedDescriptor = renaming.getOrDefault(type, type.descriptor);
renamedSignature.append(getClassBinaryNameFromDescriptor(renamedDescriptor.toString()));
return type;
}
@Override
public DexType parsedInnerTypeName(DexType enclosingType, String name) {
assert enclosingType.isClassType();
String enclosingDescriptor = enclosingType.toDescriptorString();
DexType type =
appInfo.dexItemFactory.createType(
getDescriptorFromClassBinaryName(
getClassBinaryNameFromDescriptor(enclosingDescriptor)
+ '$' + name));
String enclosingRenamedBinaryName =
getClassBinaryNameFromDescriptor(
renaming.getOrDefault(enclosingType, enclosingType.descriptor).toString());
String renamed =
getClassBinaryNameFromDescriptor(
renaming.getOrDefault(type, type.descriptor).toString());
assert renamed.startsWith(enclosingRenamedBinaryName + '$');
String outName = renamed.substring(enclosingRenamedBinaryName.length() + 1);
renamedSignature.append(outName);
return type;
}
@Override
public void start() {
renamedSignature = new StringBuilder();
}
@Override
public void stop() {
// nothing to do
}
}
}