blob: 708858eaffedd5ee9e7f0106f9566dfe852b6901 [file] [log] [blame]
// 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.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();
for (ParsedApiClass apiClass : apiClasses) {
Map<DexMethod, AndroidApiLevel> methodsForApiClass = new HashMap<>();
apiClass.visitMethodReferences(
(apiLevel, methods) -> {
methods.forEach(
method -> methodsForApiClass.put(factory.createMethod(method), apiLevel));
});
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);
}
}
}