| // Copyright (c) 2021, 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.apimodel; |
| |
| import static com.android.tools.r8.androidapi.AndroidApiDataAccess.constantPoolHash; |
| import static com.android.tools.r8.androidapi.AndroidApiLevelHashingDatabaseImpl.getNonExistingDescriptor; |
| import static com.android.tools.r8.androidapi.AndroidApiLevelHashingDatabaseImpl.getUniqueDescriptorForReference; |
| import static com.android.tools.r8.lightir.ByteUtils.isU2; |
| import static com.android.tools.r8.lightir.ByteUtils.setBitAtIndex; |
| import static com.android.tools.r8.utils.MapUtils.ignoreKey; |
| import static org.junit.Assert.assertEquals; |
| |
| import com.android.tools.r8.TestBase; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.androidapi.AndroidApiDataAccess; |
| import com.android.tools.r8.androidapi.GenerateCovariantReturnTypeMethodsTest.CovariantMethodsInJarResult; |
| import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsedApiClass; |
| import com.android.tools.r8.errors.Unreachable; |
| import com.android.tools.r8.graph.AppInfoWithClassHierarchy; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexField; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.graph.DexLibraryClass; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexReference; |
| import com.android.tools.r8.graph.DexString; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.references.ClassReference; |
| import com.android.tools.r8.utils.AndroidApiLevel; |
| import com.android.tools.r8.utils.AndroidApp; |
| import com.android.tools.r8.utils.IntBox; |
| import com.android.tools.r8.utils.Pair; |
| import com.android.tools.r8.utils.ThrowingBiConsumer; |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataOutputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| public class AndroidApiHashingDatabaseBuilderGenerator extends TestBase { |
| |
| /** |
| * Generate the information needed for looking up api level of references in the android.jar. This |
| * method will generate one single database file where the format is as follows (uX is X number of |
| * unsigned bytes): |
| * |
| * <pre> |
| * constant_pool_size: u4 |
| * constant_pool: [constant_pool_size * payload_entry] |
| * constant_pool_map: [0..max_hash(DexString) * payload_entry] |
| * api_map: [0..max_hash(DexReference) * payload_entry] |
| * payload raw data. |
| * |
| * payload_entry: u4:relative_offset_from_payload_start + u2:length |
| * </pre> |
| * |
| * For hash_definitions and entries see {@code AndroidApiDataAccess}. |
| */ |
| public static void generate( |
| List<ParsedApiClass> apiClasses, Path pathToApiLevels, AndroidApiLevel androidJarApiLevel) |
| throws Exception { |
| Map<ClassReference, Map<DexMethod, AndroidApiLevel>> methodMap = new HashMap<>(); |
| Map<ClassReference, Map<DexField, AndroidApiLevel>> fieldMap = new HashMap<>(); |
| Map<ClassReference, ParsedApiClass> lookupMap = new HashMap<>(); |
| |
| Map<DexReference, AndroidApiLevel> referenceMap = new HashMap<>(); |
| |
| Path androidJar = ToolHelper.getAndroidJar(androidJarApiLevel); |
| AppView<AppInfoWithClassHierarchy> appView = |
| computeAppViewWithClassHierarchy(AndroidApp.builder().addLibraryFile(androidJar).build()); |
| DexItemFactory factory = appView.dexItemFactory(); |
| |
| CovariantMethodsInJarResult covariantMethodsInJar = CovariantMethodsInJarResult.create(); |
| |
| for (ParsedApiClass apiClass : apiClasses) { |
| Map<DexMethod, AndroidApiLevel> methodsForApiClass = new HashMap<>(); |
| apiClass.visitMethodReferences( |
| (apiLevel, methods) -> { |
| methods.forEach( |
| method -> methodsForApiClass.put(factory.createMethod(method), apiLevel)); |
| }); |
| covariantMethodsInJar.visitCovariantMethodsForHolder( |
| apiClass.getClassReference(), |
| methodReferenceWithApiLevel -> { |
| DexMethod method = |
| factory.createMethod(methodReferenceWithApiLevel.getMethodReference()); |
| if (!methodsForApiClass.containsKey(method)) { |
| apiClass.amendCovariantMethod( |
| methodReferenceWithApiLevel.getMethodReference(), |
| methodReferenceWithApiLevel.getApiLevel()); |
| methodsForApiClass.put(method, methodReferenceWithApiLevel.getApiLevel()); |
| } |
| }); |
| Map<DexField, AndroidApiLevel> fieldsForApiClass = new HashMap<>(); |
| apiClass.visitFieldReferences( |
| (apiLevel, fields) -> { |
| fields.forEach(field -> fieldsForApiClass.put(factory.createField(field), apiLevel)); |
| }); |
| methodMap.put(apiClass.getClassReference(), methodsForApiClass); |
| fieldMap.put(apiClass.getClassReference(), fieldsForApiClass); |
| lookupMap.put(apiClass.getClassReference(), apiClass); |
| |
| referenceMap.put( |
| factory.createType(apiClass.getClassReference().getDescriptor()), apiClass.getApiLevel()); |
| } |
| |
| for (ParsedApiClass apiClass : apiClasses) { |
| computeAllReferencesInHierarchy( |
| lookupMap, |
| factory, |
| factory.createType(apiClass.getClassReference().getDescriptor()), |
| apiClass, |
| AndroidApiLevel.B, |
| referenceMap); |
| } |
| |
| assert ensureAllPublicMethodsAreMapped( |
| appView, lookupMap, methodMap, fieldMap, referenceMap, androidJar); |
| |
| try (FileOutputStream fileOutputStream = new FileOutputStream(pathToApiLevels.toFile())) { |
| DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); |
| generateDatabase(referenceMap, dataOutputStream); |
| } |
| } |
| |
| private static boolean ensureAllPublicMethodsAreMapped( |
| AppView<AppInfoWithClassHierarchy> appView, |
| Map<ClassReference, ParsedApiClass> lookupMap, |
| Map<ClassReference, Map<DexMethod, AndroidApiLevel>> methodMap, |
| Map<ClassReference, Map<DexField, AndroidApiLevel>> fieldMap, |
| Map<DexReference, AndroidApiLevel> referenceMap, |
| Path androidJar) { |
| Map<DexType, String> missingMemberInformation = new IdentityHashMap<>(); |
| for (DexLibraryClass clazz : appView.app().asDirect().libraryClasses()) { |
| ParsedApiClass parsedApiClass = lookupMap.get(clazz.getClassReference()); |
| if (parsedApiClass == null) { |
| if (clazz.isPublic()) { |
| missingMemberInformation.put(clazz.getType(), "Could not be found in " + androidJar); |
| } |
| continue; |
| } |
| StringBuilder classBuilder = new StringBuilder(); |
| Map<DexField, AndroidApiLevel> fieldMapForClass = fieldMap.get(clazz.getClassReference()); |
| assert fieldMapForClass != null; |
| clazz.forEachClassField( |
| field -> { |
| if (field.getAccessFlags().isPublic() |
| && referenceMap.get(field.getReference()) == null |
| && !field.toSourceString().contains("this$0")) { |
| classBuilder.append(" ").append(field).append(" is missing\n"); |
| } |
| }); |
| Map<DexMethod, AndroidApiLevel> methodMapForClass = methodMap.get(clazz.getClassReference()); |
| assert methodMapForClass != null; |
| clazz.forEachClassMethod( |
| method -> { |
| if (method.getAccessFlags().isPublic() |
| && referenceMap.get(method.getReference()) == null |
| && !appView.dexItemFactory().objectMembers.isObjectMember(method.getReference())) { |
| classBuilder.append(" ").append(method).append(" is missing\n"); |
| } |
| }); |
| if (classBuilder.length() > 0) { |
| missingMemberInformation.put(clazz.getType(), classBuilder.toString()); |
| } |
| } |
| |
| // api-versions.xml do not encode all members of StringBuffers and StringBuilders, check that we |
| // only have missing definitions for those two classes. |
| assert missingMemberInformation.size() == 7; |
| assert missingMemberInformation.containsKey(appView.dexItemFactory().stringBufferType); |
| assert missingMemberInformation.containsKey(appView.dexItemFactory().stringBuilderType); |
| // TODO(b/231126636): api-versions.xml has missing definitions for the below classes. |
| assert missingMemberInformation.containsKey( |
| appView.dexItemFactory().createType("Ljava/util/concurrent/ConcurrentHashMap$KeySetView;")); |
| assert missingMemberInformation.containsKey( |
| appView.dexItemFactory().createType("Ljava/time/chrono/ThaiBuddhistDate;")); |
| assert missingMemberInformation.containsKey( |
| appView.dexItemFactory().createType("Ljava/time/chrono/HijrahDate;")); |
| assert missingMemberInformation.containsKey( |
| appView.dexItemFactory().createType("Ljava/time/chrono/JapaneseDate;")); |
| assert missingMemberInformation.containsKey( |
| appView.dexItemFactory().createType("Ljava/time/chrono/MinguoDate;")); |
| return true; |
| } |
| |
| private static class ConstantPool { |
| |
| private final IntBox intBox = new IntBox(0); |
| private final Map<DexString, Integer> pool = new LinkedHashMap<>(); |
| |
| public int getOrAdd(DexString string) { |
| return pool.computeIfAbsent(string, ignored -> intBox.getAndIncrement()); |
| } |
| |
| public void forEach(ThrowingBiConsumer<DexString, Integer, IOException> consumer) |
| throws IOException { |
| for (Entry<DexString, Integer> entry : pool.entrySet()) { |
| consumer.accept(entry.getKey(), entry.getValue()); |
| } |
| } |
| |
| public int size() { |
| return pool.size(); |
| } |
| } |
| |
| private static int setUniqueConstantPoolEntry(int id) { |
| return setBitAtIndex(id, 32); |
| } |
| |
| public static void generateDatabase( |
| Map<DexReference, AndroidApiLevel> referenceMap, DataOutputStream outputStream) |
| throws Exception { |
| Map<Integer, List<Pair<DexReference, AndroidApiLevel>>> generationMap = new HashMap<>(); |
| ConstantPool constantPool = new ConstantPool(); |
| |
| int constantPoolHashMapSize = 1 << AndroidApiDataAccess.entrySizeInBitsForConstantPoolMap(); |
| int apiHashMapSize = 1 << AndroidApiDataAccess.entrySizeInBitsForApiLevelMap(); |
| |
| for (Entry<DexReference, AndroidApiLevel> entry : referenceMap.entrySet()) { |
| int newCode = AndroidApiDataAccess.apiLevelHash(entry.getKey()); |
| assert newCode >= 0 && newCode <= apiHashMapSize; |
| generationMap |
| .computeIfAbsent(newCode, ignoreKey(ArrayList::new)) |
| .add(Pair.create(entry.getKey(), entry.getValue())); |
| } |
| |
| Set<String> uniqueHashes = new HashSet<>(); |
| Map<Integer, Pair<Integer, Integer>> offsetMap = new HashMap<>(); |
| ByteArrayOutputStream payload = new ByteArrayOutputStream(); |
| |
| // Serialize api map into payload. This will also generate the entire needed constant pool. |
| for (Entry<Integer, List<Pair<DexReference, AndroidApiLevel>>> entry : |
| generationMap.entrySet()) { |
| int startingOffset = payload.size(); |
| int length = serializeIntoPayload(entry.getValue(), payload, constantPool, uniqueHashes); |
| offsetMap.put(entry.getKey(), Pair.create(startingOffset, length)); |
| } |
| |
| // Write constant pool size <u4:size>. |
| outputStream.writeInt(constantPool.size()); |
| |
| // Write constant pool consisting of <u4:payload_offset><u2:length>. |
| assertEquals(AndroidApiDataAccess.constantPoolOffset(), outputStream.size()); |
| IntBox lastReadIndex = new IntBox(-1); |
| constantPool.forEach( |
| (string, id) -> { |
| assert id > lastReadIndex.getAndIncrement(); |
| outputStream.writeInt(payload.size()); |
| outputStream.writeShort(string.content.length); |
| payload.write(string.content); |
| }); |
| |
| // Serialize hash lookup table for constant pool. |
| Map<Integer, List<Integer>> constantPoolLookupTable = new HashMap<>(); |
| constantPool.forEach( |
| (string, id) -> { |
| int constantPoolHash = constantPoolHash(string); |
| assert constantPoolHash >= 0 && constantPoolHash <= constantPoolHashMapSize; |
| constantPoolLookupTable |
| .computeIfAbsent(constantPoolHash, ignoreKey(ArrayList::new)) |
| .add(id); |
| }); |
| |
| int[] constantPoolEntries = new int[constantPoolHashMapSize]; |
| int[] constantPoolEntryLengths = new int[constantPoolHashMapSize]; |
| for (Entry<Integer, List<Integer>> entry : constantPoolLookupTable.entrySet()) { |
| // Tag if we have a unique value |
| if (entry.getValue().size() == 1) { |
| int id = entry.getValue().get(0); |
| constantPoolEntries[entry.getKey()] = setUniqueConstantPoolEntry(id); |
| } else { |
| constantPoolEntries[entry.getKey()] = payload.size(); |
| ByteArrayOutputStream temp = new ByteArrayOutputStream(); |
| for (Integer id : entry.getValue()) { |
| temp.write(intToShortEncodedByteArray(id)); |
| } |
| payload.write(temp.toByteArray()); |
| constantPoolEntryLengths[entry.getKey()] = temp.size(); |
| } |
| } |
| // Write constant pool lookup entries consisting of <u4:payload_offset><u2:length> |
| assertEquals( |
| AndroidApiDataAccess.constantPoolHashMapOffset(constantPool.size()), outputStream.size()); |
| for (int i = 0; i < constantPoolEntries.length; i++) { |
| outputStream.writeInt(constantPoolEntries[i]); |
| outputStream.writeShort(constantPoolEntryLengths[i]); |
| } |
| |
| int[] apiOffsets = new int[apiHashMapSize]; |
| int[] apiOffsetLengths = new int[apiHashMapSize]; |
| for (Entry<Integer, Pair<Integer, Integer>> hashIndexAndOffset : offsetMap.entrySet()) { |
| assert apiOffsets[hashIndexAndOffset.getKey()] == 0; |
| Pair<Integer, Integer> value = hashIndexAndOffset.getValue(); |
| int offset = value.getFirst(); |
| int length = value.getSecond(); |
| apiOffsets[hashIndexAndOffset.getKey()] = offset; |
| apiOffsetLengths[hashIndexAndOffset.getKey()] = length; |
| } |
| |
| // Write api lookup entries consisting of <u4:payload_offset><u2:length> |
| assertEquals( |
| AndroidApiDataAccess.apiLevelHashMapOffset(constantPool.size()), outputStream.size()); |
| for (int i = 0; i < apiOffsets.length; i++) { |
| outputStream.writeInt(apiOffsets[i]); |
| outputStream.writeShort(apiOffsetLengths[i]); |
| } |
| |
| // Write the payload. |
| outputStream.write(payload.toByteArray()); |
| } |
| |
| /** This will serialize a collection of DexReferences and apis into a byte stream. */ |
| private static int serializeIntoPayload( |
| List<Pair<DexReference, AndroidApiLevel>> pairs, |
| ByteArrayOutputStream payload, |
| ConstantPool constantPool, |
| Set<String> seen) |
| throws IOException { |
| ByteArrayOutputStream temp = new ByteArrayOutputStream(); |
| for (Pair<DexReference, AndroidApiLevel> pair : pairs) { |
| byte[] uniqueDescriptorForReference = |
| getUniqueDescriptorForReference(pair.getFirst(), constantPool::getOrAdd); |
| assert uniqueDescriptorForReference != getNonExistingDescriptor(); |
| if (!seen.add(Arrays.toString(uniqueDescriptorForReference))) { |
| throw new Unreachable("Hash is not unique"); |
| } |
| temp.write(intToShortEncodedByteArray(uniqueDescriptorForReference.length)); |
| temp.write(uniqueDescriptorForReference); |
| temp.write((byte) pair.getSecond().getLevel()); |
| } |
| byte[] tempArray = temp.toByteArray(); |
| payload.write(tempArray); |
| return tempArray.length; |
| } |
| |
| public static byte[] intToShortEncodedByteArray(int value) { |
| assert isU2(value); |
| byte[] bytes = new byte[2]; |
| bytes[0] = (byte) (value >> 8); |
| bytes[1] = (byte) value; |
| return bytes; |
| } |
| |
| private static void computeAllReferencesInHierarchy( |
| Map<ClassReference, ParsedApiClass> lookupMap, |
| DexItemFactory factory, |
| DexType holder, |
| ParsedApiClass apiClass, |
| AndroidApiLevel linkLevel, |
| Map<DexReference, AndroidApiLevel> additionMap) { |
| if (!apiClass.getClassReference().getDescriptor().equals(factory.objectDescriptor.toString())) { |
| apiClass.visitMethodReferences( |
| (apiLevel, methodReferences) -> { |
| methodReferences.forEach( |
| methodReference -> { |
| addIfNewOrApiLevelIsLower( |
| linkLevel, |
| additionMap, |
| apiLevel, |
| factory.createMethod(methodReference).withHolder(holder, factory)); |
| }); |
| }); |
| apiClass.visitFieldReferences( |
| (apiLevel, fieldReferences) -> { |
| fieldReferences.forEach( |
| fieldReference -> { |
| addIfNewOrApiLevelIsLower( |
| linkLevel, |
| additionMap, |
| apiLevel, |
| factory.createField(fieldReference).withHolder(holder, factory)); |
| }); |
| }); |
| apiClass.visitSuperType( |
| (superType, apiLevel) -> { |
| computeAllReferencesInHierarchy( |
| lookupMap, |
| factory, |
| holder, |
| lookupMap.get(superType), |
| linkLevel.max(apiLevel), |
| additionMap); |
| }); |
| apiClass.visitInterface( |
| (iFace, apiLevel) -> { |
| computeAllReferencesInHierarchy( |
| lookupMap, |
| factory, |
| holder, |
| lookupMap.get(iFace), |
| linkLevel.max(apiLevel), |
| additionMap); |
| }); |
| } |
| } |
| |
| private static void addIfNewOrApiLevelIsLower( |
| AndroidApiLevel linkLevel, |
| Map<DexReference, AndroidApiLevel> additionMap, |
| AndroidApiLevel apiLevel, |
| DexReference member) { |
| AndroidApiLevel currentApiLevel = apiLevel.max(linkLevel); |
| AndroidApiLevel existingApiLevel = additionMap.get(member); |
| if (existingApiLevel == null || currentApiLevel.isLessThanOrEqualTo(existingApiLevel)) { |
| additionMap.put(member, currentApiLevel); |
| } |
| } |
| } |