| // Copyright (c) 2017, 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 com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexAnnotation; |
| import com.android.tools.r8.graph.DexAnnotationSet; |
| import com.android.tools.r8.graph.DexClass; |
| import com.android.tools.r8.graph.DexClass.MethodSetter; |
| import com.android.tools.r8.graph.DexEncodedAnnotation; |
| import com.android.tools.r8.graph.DexEncodedField; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexField; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexProto; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.DexTypeList; |
| import com.android.tools.r8.graph.GraphLense; |
| import com.android.tools.r8.naming.MemberNaming.FieldSignature; |
| import com.android.tools.r8.naming.MemberNaming.MethodSignature; |
| import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness; |
| import com.android.tools.r8.utils.ArrayUtils; |
| import com.android.tools.r8.utils.ThrowingConsumer; |
| import com.android.tools.r8.utils.Timing; |
| import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.function.Function; |
| |
| public class ProguardMapApplier { |
| |
| private final AppInfoWithLiveness appInfo; |
| private final GraphLense previousLense; |
| private final SeedMapper seedMapper; |
| |
| public ProguardMapApplier(AppView<AppInfoWithLiveness> appView, SeedMapper seedMapper) { |
| assert appView.graphLense().isContextFreeForMethods(); |
| this.appInfo = appView.appInfo(); |
| this.previousLense = appView.graphLense(); |
| this.seedMapper = seedMapper; |
| } |
| |
| public GraphLense run(Timing timing) { |
| timing.begin("from-pg-map-to-lense"); |
| GraphLense lenseFromMap = new MapToLenseConverter().run(); |
| timing.end(); |
| timing.begin("fix-types-in-programs"); |
| GraphLense typeFixedLense = new TreeFixer(lenseFromMap).run(); |
| timing.end(); |
| return typeFixedLense; |
| } |
| |
| class MapToLenseConverter { |
| |
| private final ConflictFreeBuilder lenseBuilder; |
| private final Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>(); |
| |
| MapToLenseConverter() { |
| lenseBuilder = new ConflictFreeBuilder(); |
| } |
| |
| private GraphLense run() { |
| // To handle inherited yet undefined methods in library classes, we are traversing types in |
| // a subtyping order. That also helps us detect conflicted mappings in a diamond case: |
| // LibItfA#foo -> a, LibItfB#foo -> b, while PrgA implements LibItfA and LibItfB. |
| // For all type appearances in members, we apply class mappings on-the-fly, e.g., |
| // LibA -> a: |
| // ...foo(LibB) -> bar |
| // LibB -> b: |
| // Suppose PrgA extends LibA, and type map for LibB to b is not applied when visiting PrgA. |
| // Then, method map would look like: PrgA#foo(LibB) -> PrgA#bar(LibB), |
| // which should be: PrgA#foo(LibB) -> PrgA#bar(b). |
| // In this case, we should check class naming for LibB in the given pg-map, and if exist, |
| // should apply that naming at the time when making a mapping for PrgA#foo. |
| applyClassMappingForClasses(appInfo.dexItemFactory.objectType, null); |
| // TODO(b/64802420): maybe worklist-based visiting? |
| // Suppose PrgA implements LibItfA, LibItfB, and LibItfC; and PrgB extends PrgA. |
| // With interface hierarchy-based visiting, both program classes are visited three times. |
| DexType.forAllInterfaces( |
| appInfo.dexItemFactory, |
| itf -> applyClassMappingForInterfaces(itf, null)); |
| return lenseBuilder.build(appInfo.dexItemFactory, previousLense); |
| } |
| |
| private void applyClassMappingForClasses( |
| DexType type, ChainedClassNaming classNamingFromSuperType) { |
| ChainedClassNaming classNaming = chainClassNaming(type, classNamingFromSuperType); |
| applyClassMapping(type, classNaming); |
| type.forAllExtendsSubtypes(subtype -> { |
| applyClassMappingForClasses(subtype, classNaming); |
| }); |
| } |
| |
| private void applyClassMappingForInterfaces( |
| DexType type, ChainedClassNaming classNamingFromSuperType) { |
| ChainedClassNaming classNaming = chainClassNaming(type, classNamingFromSuperType); |
| DexClass clazz = appInfo.definitionFor(type); |
| // We account program classes that implement library interfaces (to be obfuscated). |
| if (clazz != null && clazz.isProgramClass()) { |
| applyClassMapping(type, classNaming); |
| } |
| type.forAllExtendsSubtypes(subtype -> { |
| applyClassMappingForInterfaces(subtype, classNaming); |
| }); |
| type.forAllImplementsSubtypes(subtype -> { |
| applyClassMappingForInterfaces(subtype, classNaming); |
| }); |
| } |
| |
| private ChainedClassNaming chainClassNaming( |
| DexType type, ChainedClassNaming classNamingFromSuperType) { |
| // Use super-type's mapping or update the mapping if the current type has its own mapping. |
| return seedMapper.hasMapping(type) |
| ? new ChainedClassNaming(classNamingFromSuperType, seedMapper.getClassNaming(type)) |
| : classNamingFromSuperType; |
| } |
| |
| private void applyClassMapping(DexType type, ChainedClassNaming classNaming) { |
| if (classNaming != null) { |
| if (seedMapper.hasMapping(type) && lenseBuilder.lookup(type) == type) { |
| DexType appliedType = appInfo.dexItemFactory.createType(classNaming.renamedName); |
| lenseBuilder.map(type, appliedType); |
| } |
| applyMemberMapping(type, classNaming); |
| } |
| } |
| |
| private void applyMemberMapping(DexType from, ChainedClassNaming classNaming) { |
| DexClass clazz = appInfo.definitionFor(from); |
| if (clazz == null) { |
| return; |
| } |
| |
| // We regard mappings as _complete_ if they cover literally everything, but that's too ideal. |
| // When visiting members with member mappings, obviously, there are two incomplete cases: |
| // no matched member or no matched mapping. |
| // |
| // 1. No matched member |
| // class A { // : X |
| // void foo(); // : a |
| // } |
| // |
| // class B extends A { // : Y |
| // @Override void foo(); // no mapping |
| // } |
| // |
| // For this case, we have chained class naming and move upward to search for super class's |
| // member mapping. One corner case we should be careful here is to resolve on the correct |
| // mapping, e.g., |
| // |
| // class B extends A { // : Y |
| // private void foo(); // no mapping, should be not renamed to a |
| // } |
| |
| final Set<MemberNaming.Signature> appliedMemberSignature = new HashSet<>(); |
| clazz.forEachField(encodedField -> { |
| MemberNaming memberNaming = classNaming.lookupByOriginalItem(encodedField.field); |
| if (memberNaming != null) { |
| appliedMemberSignature.add(memberNaming.getOriginalSignature()); |
| applyFieldMapping(encodedField.field, memberNaming); |
| } |
| }); |
| |
| clazz.forEachMethod(encodedMethod -> { |
| MemberNaming memberNaming = |
| classNaming.lookupByOriginalItem(encodedMethod.method, encodedMethod.isPrivateMethod()); |
| if (memberNaming != null) { |
| appliedMemberSignature.add(memberNaming.getOriginalSignature()); |
| applyMethodMapping(encodedMethod.method, memberNaming); |
| } |
| }); |
| |
| // 2. No matched mapping |
| // class A { // : X |
| // void foo(); // : a |
| // } |
| // |
| // class B extends A { // : Y |
| // // no overriding, but has mapping: void foo() -> a |
| // } |
| // |
| // We need to handle a class that extends another class where some members are not overridden, |
| // resulting in absence of definitions. References to those members need to be redirected via |
| // the lense as well. |
| // The caveat is, since such members don't exist, we pretend to see their definitions. |
| // We should ensure that they indeed don't exist. Otherwise, legitimately different members, |
| // e.g., private methods with same names, could be mapped to a wrong renamed name. |
| classNaming.forAllFieldNaming(memberNaming -> { |
| FieldSignature signature = (FieldSignature) memberNaming.getOriginalSignature(); |
| if (!signature.isQualified() && !appliedMemberSignature.contains(signature)) { |
| DexField pretendedOriginalField = signature.toDexField(appInfo.dexItemFactory, from); |
| if (appInfo.definitionFor(pretendedOriginalField) == null) { |
| applyFieldMapping(pretendedOriginalField, memberNaming); |
| } |
| } |
| }); |
| classNaming.forAllMethodNaming(memberNaming -> { |
| MethodSignature signature = (MethodSignature) memberNaming.getOriginalSignature(); |
| if (!signature.isQualified() && !appliedMemberSignature.contains(signature)) { |
| DexMethod pretendedOriginalMethod = signature.toDexMethod(appInfo.dexItemFactory, from); |
| if (appInfo.definitionFor(pretendedOriginalMethod) == null) { |
| applyMethodMapping(pretendedOriginalMethod, memberNaming); |
| } |
| } |
| }); |
| } |
| |
| private void applyFieldMapping(DexField originalField, MemberNaming memberNaming) { |
| FieldSignature appliedSignature = (FieldSignature) memberNaming.getRenamedSignature(); |
| DexField appliedField = |
| appInfo.dexItemFactory.createField( |
| applyClassMappingOnTheFly(originalField.clazz), |
| applyClassMappingOnTheFly(originalField.type), |
| appInfo.dexItemFactory.createString(appliedSignature.name)); |
| lenseBuilder.move(originalField, appliedField); |
| } |
| |
| private void applyMethodMapping(DexMethod originalMethod, MemberNaming memberNaming) { |
| MethodSignature appliedSignature = (MethodSignature) memberNaming.getRenamedSignature(); |
| DexMethod appliedMethod = |
| appInfo.dexItemFactory.createMethod( |
| applyClassMappingOnTheFly(originalMethod.holder), |
| applyClassMappingOnTheFly(originalMethod.proto), |
| appInfo.dexItemFactory.createString(appliedSignature.name)); |
| lenseBuilder.move(originalMethod, appliedMethod); |
| } |
| |
| private DexType applyClassMappingOnTheFly(DexType from) { |
| if (from.isArrayType()) { |
| DexType baseType = from.toBaseType(appInfo.dexItemFactory); |
| DexType appliedBaseType = applyClassMappingOnTheFly(baseType); |
| return from.replaceBaseType(appliedBaseType, appInfo.dexItemFactory); |
| } |
| if (seedMapper.hasMapping(from)) { |
| DexType appliedType = lenseBuilder.lookup(from); |
| if (appliedType != from) { |
| return appliedType; |
| } |
| // If not applied yet, build the type mapping here. |
| // Note that, unlike {@link #applyClassMapping}, we don't apply member mappings. |
| ClassNamingForMapApplier classNaming = seedMapper.getClassNaming(from); |
| appliedType = appInfo.dexItemFactory.createType(classNaming.renamedName); |
| lenseBuilder.map(from, appliedType); |
| return appliedType; |
| } |
| return from; |
| } |
| |
| private DexProto applyClassMappingOnTheFly(DexProto proto) { |
| return appInfo.dexItemFactory.applyClassMappingToProto( |
| proto, this::applyClassMappingOnTheFly, protoFixupCache); |
| } |
| } |
| |
| static class ChainedClassNaming extends ClassNamingForMapApplier { |
| final ChainedClassNaming superClassNaming; |
| |
| ChainedClassNaming( |
| ChainedClassNaming superClassNaming, |
| ClassNamingForMapApplier thisClassNaming) { |
| super(thisClassNaming); |
| this.superClassNaming = superClassNaming; |
| } |
| |
| @Override |
| public <T extends Throwable> void forAllMethodNaming( |
| ThrowingConsumer<MemberNaming, T> consumer) throws T { |
| super.forAllMethodNaming(consumer); |
| if (superClassNaming != null) { |
| superClassNaming.forAllMethodNaming(consumer); |
| } |
| } |
| |
| protected MemberNaming lookupByOriginalItem(DexMethod method, boolean isPrivate) { |
| // If the current method is overridable, use chained mappings. |
| if (!isPrivate) { |
| return lookupByOriginalItem(method); |
| } |
| // Otherwise, just look up the current class's mappings only. |
| return super.lookupByOriginalItem(method); |
| } |
| |
| @Override |
| protected MemberNaming lookupByOriginalItem(DexMethod method) { |
| MemberNaming memberNaming = super.lookupByOriginalItem(method); |
| if (memberNaming != null) { |
| return memberNaming; |
| } |
| // Moving up if chained. |
| if (superClassNaming != null) { |
| return superClassNaming.lookupByOriginalItem(method); |
| } |
| return null; |
| } |
| } |
| |
| class TreeFixer { |
| private final ConflictFreeBuilder lenseBuilder; |
| private final GraphLense appliedLense; |
| private final Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>(); |
| |
| TreeFixer(GraphLense appliedLense) { |
| this.lenseBuilder = new ConflictFreeBuilder(); |
| this.appliedLense = appliedLense; |
| } |
| |
| private GraphLense run() { |
| // Suppose PrgA extends LibA, and adds its own method, say foo that inputs LibA instance. |
| // If that library class and members are renamed somehow, all the inherited members in PrgA |
| // will be also renamed when applying those mappings. However, that newly added method, foo, |
| // with renamed LibA as an argument, won't be updated. Here at TreeFixer, we want to change |
| // PrgA#foo signature: from LibA to a renamed name. |
| appInfo.classes().forEach(this::fixClass); |
| appInfo.libraryClasses().forEach(this::fixClass); |
| return lenseBuilder.build(appInfo.dexItemFactory, appliedLense); |
| } |
| |
| private void fixClass(DexClass clazz) { |
| clazz.type = substituteType(clazz.type, null); |
| clazz.superType = substituteType(clazz.superType, null); |
| clazz.interfaces = substituteTypesIn(clazz.interfaces); |
| clazz.annotations = substituteTypesIn(clazz.annotations); |
| fixupMethods(clazz.directMethods(), clazz::setDirectMethod); |
| fixupMethods(clazz.virtualMethods(), clazz::setVirtualMethod); |
| clazz.setStaticFields(substituteTypesIn(clazz.staticFields())); |
| clazz.setInstanceFields(substituteTypesIn(clazz.instanceFields())); |
| } |
| |
| private void fixupMethods(List<DexEncodedMethod> methods, MethodSetter setter) { |
| if (methods == null) { |
| return; |
| } |
| for (int i = 0; i < methods.size(); i++) { |
| DexEncodedMethod encodedMethod = methods.get(i); |
| DexMethod appliedMethod = appliedLense.lookupMethod(encodedMethod.method); |
| DexType newHolderType = substituteType(appliedMethod.holder, encodedMethod); |
| DexProto newProto = substituteTypesIn(appliedMethod.proto, encodedMethod); |
| DexMethod newMethod; |
| if (newHolderType != appliedMethod.holder || newProto != appliedMethod.proto) { |
| newMethod = appInfo.dexItemFactory.createMethod( |
| substituteType(newHolderType, encodedMethod), newProto, appliedMethod.name); |
| lenseBuilder.move(encodedMethod.method, newMethod); |
| } else { |
| newMethod = appliedMethod; |
| } |
| // Explicitly fix members. |
| setter.setMethod(i, encodedMethod.toTypeSubstitutedMethod(newMethod)); |
| } |
| } |
| |
| private DexEncodedField[] substituteTypesIn(DexEncodedField[] fields) { |
| if (fields == null) { |
| return null; |
| } |
| for (int i = 0; i < fields.length; i++) { |
| DexEncodedField encodedField = fields[i]; |
| DexField appliedField = appliedLense.lookupField(encodedField.field); |
| DexType newHolderType = substituteType(appliedField.clazz, null); |
| DexType newFieldType = substituteType(appliedField.type, null); |
| DexField newField; |
| if (newHolderType != appliedField.clazz || newFieldType != appliedField.type) { |
| newField = appInfo.dexItemFactory.createField( |
| substituteType(newHolderType, null), newFieldType, appliedField.name); |
| lenseBuilder.move(encodedField.field, newField); |
| } else { |
| newField = appliedField; |
| } |
| // Explicitly fix members. |
| fields[i] = encodedField.toTypeSubstitutedField(newField); |
| } |
| return fields; |
| } |
| |
| private DexProto substituteTypesIn(DexProto proto, DexEncodedMethod context) { |
| DexProto result = protoFixupCache.get(proto); |
| if (result == null) { |
| DexType returnType = substituteType(proto.returnType, context); |
| DexType[] arguments = substituteTypesIn(proto.parameters.values, context); |
| if (arguments != null || returnType != proto.returnType) { |
| arguments = arguments == null ? proto.parameters.values : arguments; |
| result = appInfo.dexItemFactory.createProto(returnType, arguments); |
| } else { |
| result = proto; |
| } |
| protoFixupCache.put(proto, result); |
| } |
| return result; |
| } |
| |
| private DexAnnotationSet substituteTypesIn(DexAnnotationSet annotations) { |
| return annotations.rewrite(this::substituteTypesIn); |
| } |
| |
| private DexAnnotation substituteTypesIn(DexAnnotation annotation) { |
| return annotation.rewrite(this::substituteTypesIn); |
| } |
| |
| private DexEncodedAnnotation substituteTypesIn(DexEncodedAnnotation annotation) { |
| return annotation.rewrite(type -> substituteType(type, null), Function.identity()); |
| } |
| |
| private DexTypeList substituteTypesIn(DexTypeList types) { |
| if (types.isEmpty()) { |
| return types; |
| } |
| DexType[] result = substituteTypesIn(types.values, null); |
| return result == null ? types : new DexTypeList(result); |
| } |
| |
| private DexType[] substituteTypesIn(DexType[] types, DexEncodedMethod context) { |
| Map<Integer, DexType> changed = new Int2ObjectArrayMap<>(); |
| for (int i = 0; i < types.length; i++) { |
| DexType applied = substituteType(types[i], context); |
| if (applied != types[i]) { |
| changed.put(i, applied); |
| } |
| } |
| return changed.isEmpty() |
| ? null |
| : ArrayUtils.copyWithSparseChanges(DexType[].class, types, changed); |
| } |
| |
| private DexType substituteType(DexType type, DexEncodedMethod context) { |
| if (type == null) { |
| return null; |
| } |
| if (type.isArrayType()) { |
| DexType base = type.toBaseType(appInfo.dexItemFactory); |
| DexType fixed = substituteType(base, context); |
| if (base == fixed) { |
| return type; |
| } else { |
| return type.replaceBaseType(fixed, appInfo.dexItemFactory); |
| } |
| } |
| return appliedLense.lookupType(type); |
| } |
| } |
| |
| private static class ConflictFreeBuilder extends GraphLense.Builder { |
| ConflictFreeBuilder() { |
| super(); |
| } |
| |
| @Override |
| public void map(DexType from, DexType to) { |
| if (typeMap.containsKey(from)) { |
| String keptName = typeMap.get(from).getName(); |
| if (!keptName.equals(to.getName())) { |
| throw ProguardMapError.keptTypeWasRenamed(from, keptName, to.getName()); |
| } |
| } |
| super.map(from, to); |
| } |
| |
| @Override |
| public void map(DexMethod from, DexMethod to) { |
| if (methodMap.containsKey(from)) { |
| String keptName = methodMap.get(from).name.toString(); |
| if (!keptName.equals(to.name.toString())) { |
| throw ProguardMapError.keptMethodWasRenamed(from, keptName, to.name.toString()); |
| } |
| } |
| super.map(from, to); |
| } |
| |
| @Override |
| public void map(DexField from, DexField to) { |
| if (fieldMap.containsKey(from)) { |
| String keptName = fieldMap.get(from).name.toString(); |
| if (!keptName.equals(to.name.toString())) { |
| throw ProguardMapError.keptFieldWasRenamed(from, keptName, to.name.toString()); |
| } |
| } |
| super.map(from, to); |
| } |
| |
| // Helper to determine whether to apply the class mapping on-the-fly or applied already. |
| DexType lookup(DexType from) { |
| return typeMap.getOrDefault(from, from); |
| } |
| } |
| } |