1// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file 2// for details. All rights reserved. Use of this source code is governed by a 3// BSD-style license that can be found in the LICENSE file. 4package com.android.tools.r8.naming; 5 6import com.android.tools.r8.graph.DexAnnotation; 7import com.android.tools.r8.graph.DexClass; 8import com.android.tools.r8.graph.DexProgramClass; 9import com.android.tools.r8.graph.DexString; 10import com.android.tools.r8.graph.DexType; 11import com.android.tools.r8.graph.DexValue; 12import com.android.tools.r8.graph.DexValue.DexValueType; 13import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness; 14import com.android.tools.r8.shaking.RootSetBuilder.RootSet; 15import com.android.tools.r8.utils.DescriptorUtils; 16import com.android.tools.r8.utils.StringUtils; 17import com.google.common.collect.Maps; 18import com.google.common.collect.Sets; 19import java.util.Collections; 20import java.util.HashMap; 21import java.util.Iterator; 22import java.util.List; 23import java.util.Map; 24import java.util.Set; 25 26public class ClassNameMinifier { 27 28 private final AppInfoWithLiveness appInfo; 29 private final RootSet rootSet; 30 private final String packagePrefix; 31 private final Set<DexString> usedTypeNames = Sets.newIdentityHashSet(); 32 33 private final Map<DexType, DexString> renaming = Maps.newIdentityHashMap(); 34 private final Map<String, NamingState> states = new HashMap<>(); 35 private final List<String> dictionary; 36 private final boolean keepInnerClassStructure; 37 38 public ClassNameMinifier(AppInfoWithLiveness appInfo, RootSet rootSet, String packagePrefix, 39 List<String> dictionary, boolean keepInnerClassStructure) { 40 this.appInfo = appInfo; 41 this.rootSet = rootSet; 42 this.packagePrefix = packagePrefix; 43 this.dictionary = dictionary; 44 this.keepInnerClassStructure = keepInnerClassStructure; 45 } 46 47 public Map<DexType, DexString> computeRenaming() { 48 Iterable<DexProgramClass> classes = appInfo.classes(); 49 // Collect names we have to keep. 50 for (DexClass clazz : appInfo.classes()) { 51 if (rootSet.noObfuscation.contains(clazz)) { 52 assert !renaming.containsKey(clazz.type); 53 registerClassAsUsed(clazz.type); 54 } 55 } 56 for (DexClass clazz : appInfo.classes()) { 57 if (!renaming.containsKey(clazz.type)) { 58 DexString renamed = computeName(clazz); 59 renaming.put(clazz.type, renamed); 60 } 61 } 62 appInfo.dexItemFactory.forAllTypes(this::renameArrayTypeIfNeeded); 63 64 return Collections.unmodifiableMap(renaming); 65 } 66 67 /** 68 * Registers the given type as used. 69 * <p> 70 * When {@link #keepInnerClassStructure} is true, keeping the name of an inner class will 71 * automatically also keep the name of the outer class, as otherwise the structure would be 72 * invalidated. 73 */ 74 private void registerClassAsUsed(DexType type) { 75 renaming.put(type, type.descriptor); 76 usedTypeNames.add(type.descriptor); 77 if (keepInnerClassStructure) { 78 DexType outerClass = getOutClassForType(type); 79 if (outerClass != null) { 80 if (!renaming.containsKey(outerClass)) { 81 // The outer class was not previously kept. We have to do this now. 82 registerClassAsUsed(outerClass); 83 } 84 } 85 } 86 } 87 88 private DexType getOutClassForType(DexType type) { 89 DexClass clazz = appInfo.definitionFor(type); 90 if (clazz == null) { 91 return null; 92 } 93 DexAnnotation annotation = 94 clazz.annotations.getFirstMatching(appInfo.dexItemFactory.annotationEnclosingClass); 95 if (annotation != null) { 96 assert annotation.annotation.elements.length == 1; 97 DexValue value = annotation.annotation.elements[0].value; 98 return ((DexValueType) value).value; 99 } 100 // We do not need to preserve the names for local or anonymous classes, as they do not result 101 // in a member type declaration and hence cannot be referenced as nested classes in 102 // method signatures. 103 // See https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.5. 104 return null; 105 } 106 107 private DexString computeName(DexClass clazz) { 108 NamingState state = null; 109 if (keepInnerClassStructure) { 110 // When keeping the nesting structure of inner classes, we have to insert the name 111 // of the outer class for the $ prefix. 112 DexType outerClass = getOutClassForType(clazz.type); 113 if (outerClass != null) { 114 state = getStateForOuterClass(outerClass); 115 } 116 } 117 if (state == null) { 118 String packageName = getPackageNameFor(clazz); 119 state = getStateFor(packageName); 120 } 121 return state.nextTypeName(); 122 } 123 124 private String getPackageNameFor(DexClass clazz) { 125 if ((packagePrefix == null) || rootSet.keepPackageName.contains(clazz)) { 126 return clazz.type.getPackageDescriptor(); 127 } else { 128 return packagePrefix; 129 } 130 } 131 132 private NamingState getStateFor(String packageName) { 133 return states.computeIfAbsent(packageName, NamingState::new); 134 } 135 136 private NamingState getStateForOuterClass(DexType outer) { 137 String prefix = DescriptorUtils 138 .getClassBinaryNameFromDescriptor(outer.toDescriptorString()); 139 return states.computeIfAbsent(prefix, k -> { 140 // Create a naming state with this classes renaming as prefix. 141 DexString renamed = renaming.get(outer); 142 if (renamed == null) { 143 // The outer class has not been renamed yet, so rename the outer class first. 144 DexClass outerClass = appInfo.definitionFor(outer); 145 if (outerClass == null) { 146 renamed = outer.descriptor; 147 } else { 148 renamed = computeName(outerClass); 149 renaming.put(outer, renamed); 150 } 151 } 152 String binaryName = DescriptorUtils.getClassBinaryNameFromDescriptor(renamed.toString()); 153 return new NamingState(binaryName, "$"); 154 }); 155 } 156 157 private void renameArrayTypeIfNeeded(DexType type) { 158 if (type.isArrayType()) { 159 DexType base = type.toBaseType(appInfo.dexItemFactory); 160 DexString value = renaming.get(base); 161 if (value != null) { 162 int dimensions = type.descriptor.numberOfLeadingSquareBrackets(); 163 StringBuilder builder = new StringBuilder(); 164 for (int i = 0; i < dimensions; i++) { 165 builder.append('['); 166 } 167 builder.append(value.toString()); 168 DexString descriptor = appInfo.dexItemFactory.createString(builder.toString()); 169 renaming.put(type, descriptor); 170 } 171 } 172 } 173 174 private class NamingState { 175 176 private final char[] packagePrefix; 177 private final String separator; 178 private int typeCounter = 1; 179 private Iterator<String> dictionaryIterator; 180 181 NamingState(String packageName) { 182 this(packageName, "/"); 183 } 184 185 NamingState(String packageName, String separator) { 186 this.packagePrefix = ("L" + packageName + (packageName.isEmpty() ? "" : separator)) 187 .toCharArray(); 188 this.separator = separator; 189 this.dictionaryIterator = dictionary.iterator(); 190 } 191 192 public char[] getPackagePrefix() { 193 return packagePrefix; 194 } 195 196 protected String nextSuggestedName() { 197 StringBuilder nextName = new StringBuilder(); 198 if (dictionaryIterator.hasNext()) { 199 nextName.append(getPackagePrefix()).append(dictionaryIterator.next()).append(';'); 200 return nextName.toString(); 201 } else { 202 return StringUtils.numberToIdentifier(packagePrefix, typeCounter++, true); 203 } 204 } 205 206 private DexString nextTypeName() { 207 DexString candidate; 208 do { 209 candidate = appInfo.dexItemFactory.createString(nextSuggestedName()); 210 } while (usedTypeNames.contains(candidate)); 211 return candidate; 212 } 213 } 214} 215