| // 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.naming.ClassNameMapper.MissingFileAction.MISSING_FILE_IS_ERROR; |
| import static com.android.tools.r8.utils.DescriptorUtils.descriptorToJavaType; |
| |
| import com.android.tools.r8.DiagnosticsHandler; |
| import com.android.tools.r8.graph.DexField; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.IndexedDexItem; |
| import com.android.tools.r8.naming.MemberNaming.FieldSignature; |
| import com.android.tools.r8.naming.MemberNaming.MethodSignature; |
| import com.android.tools.r8.naming.MemberNaming.Signature; |
| import com.android.tools.r8.naming.mappinginformation.MapVersionMappingInformation; |
| import com.android.tools.r8.position.Position; |
| import com.android.tools.r8.utils.BiMapContainer; |
| import com.android.tools.r8.utils.ChainableStringConsumer; |
| import com.android.tools.r8.utils.Reporter; |
| import com.google.common.collect.BiMap; |
| import com.google.common.collect.ImmutableBiMap; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.io.CharSource; |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| public class ClassNameMapper implements ProguardMap { |
| |
| public enum MissingFileAction { |
| MISSING_FILE_IS_EMPTY_MAP, |
| MISSING_FILE_IS_ERROR |
| } |
| |
| public static class Builder extends ProguardMap.Builder { |
| |
| private boolean buildPreamble = false; |
| private final List<String> preamble = new ArrayList<>(); |
| private final Map<String, ClassNamingForNameMapper.Builder> mapping = new HashMap<>(); |
| private final LinkedHashSet<MapVersionMappingInformation> mapVersions = new LinkedHashSet<>(); |
| private final Map<String, String> originalSourceFiles = new HashMap<>(); |
| |
| @Override |
| public ClassNamingForNameMapper.Builder classNamingBuilder( |
| String renamedName, String originalName, Position position) { |
| ClassNamingForNameMapper.Builder classNamingBuilder = |
| ClassNamingForNameMapper.builder(renamedName, originalName, originalSourceFiles::put); |
| mapping.put(renamedName, classNamingBuilder); |
| return classNamingBuilder; |
| } |
| |
| Builder setBuildPreamble(boolean buildPreamble) { |
| this.buildPreamble = buildPreamble; |
| return this; |
| } |
| |
| @Override |
| void addPreambleLine(String line) { |
| if (buildPreamble) { |
| preamble.add(line); |
| } |
| } |
| |
| @Override |
| public ClassNameMapper build() { |
| return new ClassNameMapper( |
| buildClassNameMappings(), mapVersions, originalSourceFiles, preamble); |
| } |
| |
| private ImmutableMap<String, ClassNamingForNameMapper> buildClassNameMappings() { |
| ImmutableMap.Builder<String, ClassNamingForNameMapper> builder = ImmutableMap.builder(); |
| mapping.forEach( |
| (renamedName, valueBuilder) -> builder.put(renamedName, valueBuilder.build())); |
| return builder.build(); |
| } |
| |
| @Override |
| public ProguardMap.Builder setCurrentMapVersion(MapVersionMappingInformation mapVersion) { |
| mapVersions.add(mapVersion); |
| return this; |
| } |
| } |
| |
| public static Builder builder() { |
| return new Builder(); |
| } |
| |
| public static ClassNameMapper mapperFromFile(Path path) throws IOException { |
| return mapperFromFile(path, MISSING_FILE_IS_ERROR); |
| } |
| |
| public static ClassNameMapper mapperFromFile(Path path, MissingFileAction missingFileAction) |
| throws IOException { |
| assert missingFileAction == MissingFileAction.MISSING_FILE_IS_EMPTY_MAP |
| || missingFileAction == MISSING_FILE_IS_ERROR; |
| if (missingFileAction == MissingFileAction.MISSING_FILE_IS_EMPTY_MAP |
| && !path.toFile().exists()) { |
| return mapperFromString(""); |
| } |
| return mapperFromBufferedReader(Files.newBufferedReader(path, StandardCharsets.UTF_8), null); |
| } |
| |
| public static ClassNameMapper mapperFromString(String contents) throws IOException { |
| return mapperFromBufferedReader(CharSource.wrap(contents).openBufferedStream(), null); |
| } |
| |
| public static ClassNameMapper mapperFromStringWithPreamble(String contents) throws IOException { |
| return mapperFromBufferedReader( |
| CharSource.wrap(contents).openBufferedStream(), null, false, false, true); |
| } |
| |
| public static ClassNameMapper mapperFromFileWithPreamble( |
| Path path, MissingFileAction missingFileAction) throws Exception { |
| assert missingFileAction == MissingFileAction.MISSING_FILE_IS_EMPTY_MAP |
| || missingFileAction == MISSING_FILE_IS_ERROR; |
| if (missingFileAction == MissingFileAction.MISSING_FILE_IS_EMPTY_MAP |
| && !path.toFile().exists()) { |
| return mapperFromString(""); |
| } |
| return mapperFromBufferedReader( |
| Files.newBufferedReader(path, StandardCharsets.UTF_8), null, false, false, true); |
| } |
| |
| public static ClassNameMapper mapperFromString( |
| String contents, DiagnosticsHandler diagnosticsHandler) throws IOException { |
| return mapperFromBufferedReader( |
| CharSource.wrap(contents).openBufferedStream(), diagnosticsHandler); |
| } |
| |
| // TODO(b/241763080): Remove when 2.2 is stable. |
| @Deprecated |
| public static ClassNameMapper mapperFromStringWithExperimental(String contents) |
| throws IOException { |
| return mapperFromBufferedReader( |
| CharSource.wrap(contents).openBufferedStream(), null, false, true, true); |
| } |
| |
| public static ClassNameMapper mapperFromString( |
| String contents, |
| DiagnosticsHandler diagnosticsHandler, |
| boolean allowEmptyMappedRanges, |
| boolean allowExperimentalMapping, |
| boolean readPreamble) |
| throws IOException { |
| return mapperFromLineReader( |
| LineReader.fromBufferedReader(CharSource.wrap(contents).openBufferedStream()), |
| diagnosticsHandler, |
| allowEmptyMappedRanges, |
| allowExperimentalMapping, |
| readPreamble); |
| } |
| |
| private static ClassNameMapper mapperFromBufferedReader( |
| BufferedReader reader, DiagnosticsHandler diagnosticsHandler) throws IOException { |
| return mapperFromBufferedReader(reader, diagnosticsHandler, false, false, false); |
| } |
| |
| public static ClassNameMapper mapperFromBufferedReader( |
| BufferedReader reader, |
| DiagnosticsHandler diagnosticsHandler, |
| boolean allowEmptyMappedRanges, |
| boolean allowExperimentalMapping, |
| boolean buildPreamble) |
| throws IOException { |
| return mapperFromLineReader( |
| LineReader.fromBufferedReader(reader), |
| diagnosticsHandler, |
| allowEmptyMappedRanges, |
| allowExperimentalMapping, |
| buildPreamble); |
| } |
| |
| public static ClassNameMapper mapperFromLineReader( |
| LineReader reader, |
| DiagnosticsHandler diagnosticsHandler, |
| boolean allowEmptyMappedRanges, |
| boolean allowExperimentalMapping, |
| boolean buildPreamble) |
| throws IOException { |
| try (ProguardMapReader proguardReader = |
| new ProguardMapReader( |
| reader, |
| diagnosticsHandler != null ? diagnosticsHandler : new Reporter(), |
| allowEmptyMappedRanges, |
| allowExperimentalMapping)) { |
| ClassNameMapper.Builder builder = ClassNameMapper.builder().setBuildPreamble(buildPreamble); |
| proguardReader.parse(builder); |
| return builder.build(); |
| } |
| } |
| |
| public static ClassNameMapper mapperFromLineReaderWithFiltering( |
| LineReader reader, |
| MapVersion mapVersion, |
| DiagnosticsHandler diagnosticsHandler, |
| boolean allowEmptyMappedRanges, |
| boolean allowExperimentalMapping) |
| throws IOException { |
| try (ProguardMapReader proguardReader = |
| new ProguardMapReader( |
| reader, |
| diagnosticsHandler != null ? diagnosticsHandler : new Reporter(), |
| allowEmptyMappedRanges, |
| allowExperimentalMapping, |
| mapVersion)) { |
| ClassNameMapper.Builder builder = ClassNameMapper.builder(); |
| proguardReader.parse(builder); |
| return builder.build(); |
| } |
| } |
| |
| private final ImmutableMap<String, ClassNamingForNameMapper> classNameMappings; |
| private BiMapContainer<String, String> nameMapping; |
| private final Map<Signature, Signature> signatureMap = new HashMap<>(); |
| private final LinkedHashSet<MapVersionMappingInformation> mapVersions; |
| private final Map<String, String> originalSourceFiles; |
| private final List<String> preamble; |
| |
| private ClassNameMapper( |
| ImmutableMap<String, ClassNamingForNameMapper> classNameMappings, |
| LinkedHashSet<MapVersionMappingInformation> mapVersions, |
| Map<String, String> originalSourceFiles, |
| List<String> preamble) { |
| this.classNameMappings = classNameMappings; |
| this.mapVersions = mapVersions; |
| this.originalSourceFiles = originalSourceFiles; |
| this.preamble = preamble; |
| } |
| |
| public Map<String, ClassNamingForNameMapper> getClassNameMappings() { |
| return classNameMappings; |
| } |
| |
| public Collection<String> getPreamble() { |
| return preamble; |
| } |
| |
| private Signature canonicalizeSignature(Signature signature) { |
| Signature result = signatureMap.get(signature); |
| if (result != null) { |
| return result; |
| } |
| signatureMap.put(signature, signature); |
| return signature; |
| } |
| |
| public MethodSignature getRenamedMethodSignature(DexMethod method) { |
| DexType[] parameters = method.proto.parameters.values; |
| String[] parameterTypes = new String[parameters.length]; |
| for (int i = 0; i < parameters.length; i++) { |
| parameterTypes[i] = deobfuscateType(parameters[i].toDescriptorString()); |
| } |
| String returnType = deobfuscateType(method.proto.returnType.toDescriptorString()); |
| |
| MethodSignature signature = new MethodSignature(method.name.toString(), returnType, |
| parameterTypes); |
| return (MethodSignature) canonicalizeSignature(signature); |
| } |
| |
| public FieldSignature getRenamedFieldSignature(DexField field) { |
| String type = deobfuscateType(field.type.toDescriptorString()); |
| return (FieldSignature) canonicalizeSignature(new FieldSignature(field.name.toString(), type)); |
| } |
| |
| /** |
| * Deobfuscate a class name. |
| * |
| * <p>Returns the deobfuscated name if a mapping was found. Otherwise it returns the passed in |
| * name. |
| */ |
| public String deobfuscateClassName(String obfuscatedName) { |
| ClassNamingForNameMapper classNaming = classNameMappings.get(obfuscatedName); |
| if (classNaming == null) { |
| return obfuscatedName; |
| } |
| return classNaming.originalName; |
| } |
| |
| private String deobfuscateType(String asString) { |
| return descriptorToJavaType(asString, this); |
| } |
| |
| public String getSourceFile(String typeName) { |
| return originalSourceFiles.get(typeName); |
| } |
| |
| public ClassNameMapper combine(ClassNameMapper other) { |
| if (other == null || other.isEmpty()) { |
| return this; |
| } |
| if (this.isEmpty()) { |
| return other; |
| } |
| ImmutableMap.Builder<String, ClassNamingForNameMapper> builder = ImmutableMap.builder(); |
| Map<String, ClassNamingForNameMapper> otherClassMappings = other.getClassNameMappings(); |
| for (Entry<String, ClassNamingForNameMapper> mappingEntry : classNameMappings.entrySet()) { |
| ClassNamingForNameMapper otherMapping = otherClassMappings.get(mappingEntry.getKey()); |
| if (otherMapping == null) { |
| builder.put(mappingEntry); |
| } else { |
| builder.put(mappingEntry.getKey(), mappingEntry.getValue().combine(otherMapping)); |
| } |
| } |
| otherClassMappings.forEach( |
| (otherMappingClass, otherMapping) -> { |
| // Collisions are handled above so only take non-existing mappings. |
| if (!classNameMappings.containsKey(otherMappingClass)) { |
| builder.put(otherMappingClass, otherMapping); |
| } |
| }); |
| LinkedHashSet<MapVersionMappingInformation> newMapVersions = |
| new LinkedHashSet<>(getMapVersions()); |
| newMapVersions.addAll(other.getMapVersions()); |
| Map<String, String> newSourcesFiles = new HashMap<>(originalSourceFiles); |
| // This will overwrite existing source files but the chance of that happening should be very |
| // slim. |
| newSourcesFiles.putAll(other.originalSourceFiles); |
| |
| List<String> newPreamble = Collections.emptyList(); |
| if (!this.preamble.isEmpty() || !other.preamble.isEmpty()) { |
| newPreamble = new ArrayList<>(); |
| newPreamble.addAll(this.preamble); |
| newPreamble.addAll(other.preamble); |
| } |
| return new ClassNameMapper(builder.build(), newMapVersions, newSourcesFiles, newPreamble); |
| } |
| |
| @Override |
| public boolean hasMapping(DexType type) { |
| String decoded = descriptorToJavaType(type.descriptor.toString()); |
| return classNameMappings.containsKey(decoded); |
| } |
| |
| @Override |
| public ClassNamingForNameMapper getClassNaming(DexType type) { |
| String decoded = descriptorToJavaType(type.descriptor.toString()); |
| return classNameMappings.get(decoded); |
| } |
| |
| public ClassNamingForNameMapper getClassNaming(String obfuscatedName) { |
| return classNameMappings.get(obfuscatedName); |
| } |
| |
| public boolean isEmpty() { |
| return classNameMappings.isEmpty(); |
| } |
| |
| public ClassNameMapper sorted() { |
| ImmutableMap.Builder<String, ClassNamingForNameMapper> builder = ImmutableMap.builder(); |
| builder.orderEntriesByValue(Comparator.comparing(x -> x.originalName)); |
| classNameMappings.forEach(builder::put); |
| return new ClassNameMapper(builder.build(), mapVersions, originalSourceFiles, preamble); |
| } |
| |
| public boolean verifyIsSorted() { |
| Iterator<Entry<String, ClassNamingForNameMapper>> iterator = |
| getClassNameMappings().entrySet().iterator(); |
| Iterator<Entry<String, ClassNamingForNameMapper>> sortedIterator = |
| sorted().getClassNameMappings().entrySet().iterator(); |
| while (iterator.hasNext()) { |
| Entry<String, ClassNamingForNameMapper> entry = iterator.next(); |
| Entry<String, ClassNamingForNameMapper> otherEntry = sortedIterator.next(); |
| assert entry.getKey().equals(otherEntry.getKey()); |
| assert entry.getValue() == otherEntry.getValue(); |
| } |
| return true; |
| } |
| |
| public void write(ChainableStringConsumer consumer) { |
| // Classes should be sorted by their original name such that the generated Proguard map is |
| // deterministic (and easy to navigate manually). |
| assert verifyIsSorted(); |
| for (ClassNamingForNameMapper naming : getClassNameMappings().values()) { |
| naming.write(consumer); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder builder = new StringBuilder(); |
| write(ChainableStringConsumer.wrap(builder::append)); |
| return builder.toString(); |
| } |
| |
| public BiMapContainer<String, String> getObfuscatedToOriginalMapping() { |
| if (nameMapping == null) { |
| ImmutableBiMap.Builder<String, String> builder = ImmutableBiMap.builder(); |
| for (String name : classNameMappings.keySet()) { |
| builder.put(name, classNameMappings.get(name).originalName); |
| } |
| BiMap<String, String> classNameMappings = builder.build(); |
| nameMapping = new BiMapContainer<>(classNameMappings, classNameMappings.inverse()); |
| } |
| return nameMapping; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| return o instanceof ClassNameMapper |
| && classNameMappings.equals(((ClassNameMapper) o).classNameMappings); |
| } |
| |
| @Override |
| public int hashCode() { |
| return 31 * classNameMappings.hashCode(); |
| } |
| |
| public String originalNameOf(IndexedDexItem item) { |
| if (item instanceof DexField) { |
| return lookupName(getRenamedFieldSignature((DexField) item), ((DexField) item).holder); |
| } else if (item instanceof DexMethod) { |
| return lookupName(getRenamedMethodSignature((DexMethod) item), ((DexMethod) item).holder); |
| } else if (item instanceof DexType) { |
| return descriptorToJavaType(((DexType) item).toDescriptorString(), this); |
| } else { |
| return item.toString(); |
| } |
| } |
| |
| private String lookupName(Signature signature, DexType clazz) { |
| String decoded = descriptorToJavaType(clazz.descriptor.toString()); |
| ClassNamingForNameMapper classNaming = getClassNaming(decoded); |
| if (classNaming == null) { |
| return decoded + " " + signature.toString(); |
| } |
| MemberNaming memberNaming = classNaming.lookup(signature); |
| if (memberNaming == null) { |
| return classNaming.originalName + " " + signature; |
| } |
| return classNaming.originalName + " " + memberNaming.getOriginalSignature().toString(); |
| } |
| |
| public MethodSignature originalSignatureOf(DexMethod method) { |
| String decoded = descriptorToJavaType(method.holder.descriptor.toString()); |
| MethodSignature memberSignature = getRenamedMethodSignature(method); |
| ClassNaming classNaming = getClassNaming(decoded); |
| if (classNaming == null) { |
| return memberSignature; |
| } |
| MemberNaming memberNaming = classNaming.lookup(memberSignature); |
| if (memberNaming == null) { |
| return memberSignature; |
| } |
| return memberNaming.getOriginalSignature().asMethodSignature(); |
| } |
| |
| public FieldSignature originalSignatureOf(DexField field) { |
| String decoded = descriptorToJavaType(field.holder.descriptor.toString()); |
| FieldSignature memberSignature = getRenamedFieldSignature(field); |
| ClassNaming classNaming = getClassNaming(decoded); |
| if (classNaming == null) { |
| return memberSignature; |
| } |
| MemberNaming memberNaming = classNaming.lookup(memberSignature); |
| if (memberNaming == null) { |
| return memberSignature; |
| } |
| return memberNaming.getOriginalSignature().asFieldSignature(); |
| } |
| |
| public String originalNameOf(DexType clazz) { |
| return deobfuscateType(clazz.descriptor.toString()); |
| } |
| |
| public Set<MapVersionMappingInformation> getMapVersions() { |
| return mapVersions; |
| } |
| |
| public MapVersionMappingInformation getFirstMapVersionInformation() { |
| return mapVersions.isEmpty() ? null : mapVersions.iterator().next(); |
| } |
| } |