blob: 771c3361b5a29cd7e93465b16d43c770628612cd [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.DESCRIPTOR_PACKAGE_SEPARATOR;
import static com.android.tools.r8.utils.DescriptorUtils.INNER_CLASS_SEPARATOR;
import static com.android.tools.r8.utils.DescriptorUtils.computeInnerClassSeparator;
import static com.android.tools.r8.utils.DescriptorUtils.getClassBinaryNameFromDescriptor;
import static com.android.tools.r8.utils.DescriptorUtils.getPackageBinaryNameFromJavaType;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.InnerClassAttribute;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.ProguardPackageNameList;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate;
class ClassNameMinifier {
private final AppView<AppInfoWithLiveness> appView;
private final ClassNamingStrategy classNamingStrategy;
private final PackageNamingStrategy packageNamingStrategy;
private final Iterable<? extends DexClass> classes;
private final boolean isAccessModificationAllowed;
private final Set<String> noObfuscationPrefixes = Sets.newHashSet();
private final Set<String> usedPackagePrefixes = Sets.newHashSet();
private final Set<String> usedTypeNames = Sets.newHashSet();
private final Map<DexType, DexString> renaming = Maps.newIdentityHashMap();
private final Map<String, Namespace> states = new HashMap<>();
private final boolean keepInnerClassStructure;
private final Namespace topLevelState;
private final boolean allowMixedCaseNaming;
private final Predicate<String> isUsed;
ClassNameMinifier(
AppView<AppInfoWithLiveness> appView,
ClassNamingStrategy classNamingStrategy,
PackageNamingStrategy packageNamingStrategy,
Iterable<? extends DexClass> classes) {
this.appView = appView;
this.classNamingStrategy = classNamingStrategy;
this.packageNamingStrategy = packageNamingStrategy;
this.classes = classes;
InternalOptions options = appView.options();
this.isAccessModificationAllowed =
options.getProguardConfiguration().isAccessModificationAllowed();
this.keepInnerClassStructure = options.keepInnerClassStructure();
// Initialize top-level naming state.
topLevelState = new Namespace("");
String newPackageDescriptor =
StringUtils.replaceAll(options.getProguardConfiguration().getPackagePrefix(), ".", "/");
if (!newPackageDescriptor.isEmpty()) {
registerPackagePrefixesAsUsed(newPackageDescriptor, false);
}
states.put("", topLevelState);
if (options.getProguardConfiguration().hasDontUseMixedCaseClassnames()) {
allowMixedCaseNaming = false;
isUsed = candidate -> usedTypeNames.contains(candidate.toLowerCase());
} else {
allowMixedCaseNaming = true;
isUsed = usedTypeNames::contains;
}
}
private void setUsedTypeName(String typeName) {
usedTypeNames.add(allowMixedCaseNaming ? typeName : typeName.toLowerCase());
}
static class ClassRenaming {
final Map<String, String> packageRenaming;
final Map<DexType, DexString> classRenaming;
private ClassRenaming(
Map<DexType, DexString> classRenaming, Map<String, String> packageRenaming) {
this.classRenaming = classRenaming;
this.packageRenaming = packageRenaming;
}
}
ClassRenaming computeRenaming(Timing timing) {
// Collect names we have to keep.
timing.begin("reserve");
for (DexClass clazz : classes) {
DexString descriptor = classNamingStrategy.reservedDescriptor(clazz.type);
if (descriptor != null) {
assert !renaming.containsKey(clazz.type);
registerClassAsUsed(clazz.type, descriptor);
}
}
timing.end();
timing.begin("rename-classes");
for (DexClass clazz : classes) {
if (!renaming.containsKey(clazz.type)) {
DexString renamed = computeName(clazz.type);
renaming.put(clazz.type, renamed);
// If the class is a member class and it has used $ separator, its renamed name should have
// the same separator (as long as inner-class attribute is honored).
assert !keepInnerClassStructure
|| !clazz.isMemberClass()
|| !clazz.type.getInternalName().contains(String.valueOf(INNER_CLASS_SEPARATOR))
|| renamed.toString().contains(String.valueOf(INNER_CLASS_SEPARATOR))
|| classNamingStrategy.isRenamedByApplyMapping(clazz.type)
: clazz.toSourceString() + " -> " + renamed;
}
}
timing.end();
timing.begin("rename-dangling-types");
for (DexClass clazz : classes) {
renameDanglingTypes(clazz);
}
timing.end();
return new ClassRenaming(Collections.unmodifiableMap(renaming), getPackageRenaming());
}
private Map<String, String> getPackageRenaming() {
ImmutableMap.Builder<String, String> packageRenaming = ImmutableMap.builder();
for (Entry<String, Namespace> entry : states.entrySet()) {
String originalPackageName = entry.getKey();
String minifiedPackageName = entry.getValue().getPackageName();
if (!minifiedPackageName.equals(originalPackageName)) {
packageRenaming.put(originalPackageName, minifiedPackageName);
}
}
return packageRenaming.build();
}
private void renameDanglingTypes(DexClass clazz) {
clazz.forEachMethod(this::renameDanglingTypesInMethod);
clazz.forEachField(this::renameDanglingTypesInField);
}
private void renameDanglingTypesInField(DexEncodedField field) {
renameDanglingType(field.field.type);
}
private void renameDanglingTypesInMethod(DexEncodedMethod method) {
DexProto proto = method.method.proto;
renameDanglingType(proto.returnType);
for (DexType type : proto.parameters.values) {
renameDanglingType(type);
}
}
private void renameDanglingType(DexType type) {
if (appView.appInfo().wasPruned(type) && !renaming.containsKey(type)) {
// We have a type that is defined in the program source but is only used in a proto or
// return type. As we don't need the class, we can rename it to anything as long as it is
// unique.
assert appView.definitionFor(type) == null;
DexString descriptor = classNamingStrategy.reservedDescriptor(type);
renaming.put(type, descriptor != null ? descriptor : topLevelState.nextTypeName(type));
}
}
private void registerClassAsUsed(DexType type, DexString descriptor) {
renaming.put(type, descriptor);
registerPackagePrefixesAsUsed(
getParentPackagePrefix(getClassBinaryNameFromDescriptor(descriptor.toSourceString())),
isAccessModificationAllowed);
setUsedTypeName(descriptor.toString());
if (keepInnerClassStructure) {
DexType outerClass = getOutClassForType(type);
if (outerClass != null) {
if (!renaming.containsKey(outerClass)
&& classNamingStrategy.reservedDescriptor(outerClass) == null) {
// The outer class was not previously kept and will not be kept.
// We have to force keep the outer class now.
registerClassAsUsed(outerClass, outerClass.descriptor);
}
}
}
}
/** Registers the given package prefix and all of parent packages as used. */
private void registerPackagePrefixesAsUsed(String packagePrefix, boolean isMinificationAllowed) {
// If -allowaccessmodification is not set, we may keep classes in their original packages,
// accounting for package-private accesses.
if (!isMinificationAllowed) {
noObfuscationPrefixes.add(packagePrefix);
}
String usedPrefix = packagePrefix;
while (usedPrefix.length() > 0) {
usedPackagePrefixes.add(usedPrefix);
usedPrefix = getParentPackagePrefix(usedPrefix);
}
}
private DexType getOutClassForType(DexType type) {
DexClass clazz = appView.definitionFor(type);
if (clazz == null) {
return null;
}
// For DEX inputs this could result in returning the outer class of a local class since we
// can't distinguish it from a member class based on just the enclosing-class annotation.
// We could filter out the local classes by looking for a corresponding entry in the
// inner-classes attribute table which must exist only for member classes. Since DEX files
// are not a supported input for R8 we just return the outer class in both cases.
InnerClassAttribute attribute = clazz.getInnerClassAttributeForThisClass();
if (attribute == null) {
return null;
}
return attribute.getLiveContext(appView);
}
private DexString computeName(DexType type) {
Namespace state = null;
if (keepInnerClassStructure) {
// When keeping the nesting structure of inner classes, bind this type to the live context.
// Note that such live context might not be always the enclosing class. E.g., a local class
// does not have a direct enclosing class, but we use the holder of the enclosing method here.
DexType outerClass = getOutClassForType(type);
if (outerClass != null) {
DexClass clazz = appView.definitionFor(type);
assert clazz != null;
InnerClassAttribute attribute = clazz.getInnerClassAttributeForThisClass();
assert attribute != null;
// Note that, to be consistent with the way inner-class attribute is written via minifier
// lens, we are using attribute's outer-class, not the live context.
String separator =
computeInnerClassSeparator(attribute.getOuter(), type, attribute.getInnerName());
if (separator == null) {
separator = String.valueOf(INNER_CLASS_SEPARATOR);
}
state = getStateForOuterClass(outerClass, separator);
}
}
if (state == null) {
state = getStateForClass(type);
}
return state.nextTypeName(type);
}
private Namespace getStateForClass(DexType type) {
String packageName = getPackageBinaryNameFromJavaType(type.getPackageDescriptor());
// Check whether the given class should be kept.
// or check whether the given class belongs to a package that is kept for another class.
ProguardPackageNameList keepPackageNames =
appView.options().getProguardConfiguration().getKeepPackageNamesPatterns();
if (noObfuscationPrefixes.contains(packageName) || keepPackageNames.matches(type)) {
return states.computeIfAbsent(packageName, Namespace::new);
}
return getStateForPackagePrefix(packageName);
}
private Namespace getStateForPackagePrefix(String prefix) {
Namespace state = states.get(prefix);
if (state == null) {
// Calculate the parent package prefix, e.g., La/b/c -> La/b
String parentPackage = getParentPackagePrefix(prefix);
Namespace superState;
if (noObfuscationPrefixes.contains(parentPackage)) {
// Restore a state for parent package prefix if it should be kept.
superState = states.computeIfAbsent(parentPackage, Namespace::new);
} else {
// Create a state for parent package prefix, if necessary, in a recursive manner.
// That recursion should end when the parent package hits the top-level, "".
superState = getStateForPackagePrefix(parentPackage);
}
// From the super state, get a renamed package prefix for the current level.
String renamedPackagePrefix = superState.nextPackagePrefix();
// Create a new state, which corresponds to a new name space, for the current level.
state = new Namespace(renamedPackagePrefix);
states.put(prefix, state);
}
return state;
}
private Namespace getStateForOuterClass(DexType outer, String innerClassSeparator) {
String prefix = getClassBinaryNameFromDescriptor(outer.toDescriptorString());
Namespace state = states.get(prefix);
if (state == null) {
// 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.
// Note that here we proceed unconditionally---w/o regards to the existence of the outer
// class: it could be the case that the outer class is not renamed because it's shrunk.
// Even though that's the case, we can _implicitly_ assign a new name to the outer class
// and then use that renamed name as a base prefix for the current inner class.
renamed = computeName(outer);
renaming.put(outer, renamed);
}
String binaryName = getClassBinaryNameFromDescriptor(renamed.toString());
state = new Namespace(binaryName, innerClassSeparator);
states.put(prefix, state);
}
return state;
}
protected class Namespace implements InternalNamingState {
private final String packageName;
private final char[] packagePrefix;
private int dictionaryIndex = 0;
private int nameIndex = 1;
Namespace(String packageName) {
this(packageName, String.valueOf(DESCRIPTOR_PACKAGE_SEPARATOR));
}
Namespace(String packageName, String separator) {
this.packageName = packageName;
this.packagePrefix = ("L" + packageName
// L or La/b/ (or La/b/C$)
+ (packageName.isEmpty() ? "" : separator))
.toCharArray();
}
public String getPackageName() {
return packageName;
}
DexString nextTypeName(DexType type) {
DexString candidate = classNamingStrategy.next(type, packagePrefix, this, isUsed);
assert !usedTypeNames.contains(candidate.toString());
setUsedTypeName(candidate.toString());
return candidate;
}
String nextPackagePrefix() {
String next = packageNamingStrategy.next(packagePrefix, this, usedPackagePrefixes::contains);
assert !usedPackagePrefixes.contains(next);
usedPackagePrefixes.add(next);
return next;
}
@Override
public int getDictionaryIndex() {
return dictionaryIndex;
}
@Override
public int incrementDictionaryIndex() {
return dictionaryIndex++;
}
@Override
public int incrementNameIndex(boolean isDirectMethodCall) {
assert !isDirectMethodCall;
return nameIndex++;
}
}
protected interface ClassNamingStrategy {
DexString next(
DexType type, char[] packagePrefix, InternalNamingState state, Predicate<String> isUsed);
/**
* Returns the reserved descriptor for a type. If the type is not allowed to be obfuscated
* (minified) it will return the original type descriptor. If applymapping is used, it will try
* to return the applied name such that it can be reserved. Otherwise, if there are no
* reservations, it will return null.
*
* @param type The type to find a reserved descriptor for
* @return The reserved descriptor
*/
DexString reservedDescriptor(DexType type);
boolean isRenamedByApplyMapping(DexType type);
}
protected interface PackageNamingStrategy {
String next(char[] packagePrefix, InternalNamingState state, Predicate<String> isUsed);
}
/**
* Compute parent package prefix from the given package prefix.
*
* @param packagePrefix i.e. "Ljava/lang"
* @return parent package prefix i.e. "Ljava"
*/
static String getParentPackagePrefix(String packagePrefix) {
int i = packagePrefix.lastIndexOf(DescriptorUtils.DESCRIPTOR_PACKAGE_SEPARATOR);
if (i < 0) {
return "";
}
return packagePrefix.substring(0, i);
}
}