|  | // Copyright (c) 2022, 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.androidapi; | 
|  |  | 
|  | import static com.android.tools.r8.lightir.ByteUtils.unsetBitAtIndex; | 
|  | import static com.android.tools.r8.utils.ZipUtils.getOffsetOfResourceInZip; | 
|  |  | 
|  | import com.android.tools.r8.DiagnosticsHandler; | 
|  | import com.android.tools.r8.dex.CompatByteBuffer; | 
|  | import com.android.tools.r8.errors.Unreachable; | 
|  | import com.android.tools.r8.graph.DexReference; | 
|  | import com.android.tools.r8.graph.DexString; | 
|  | import com.android.tools.r8.utils.ExceptionDiagnostic; | 
|  | import com.android.tools.r8.utils.InternalOptions; | 
|  | import com.android.tools.r8.utils.StringDiagnostic; | 
|  | import com.google.common.io.ByteStreams; | 
|  | import com.google.common.primitives.Ints; | 
|  | import java.io.File; | 
|  | import java.io.IOException; | 
|  | import java.io.InputStream; | 
|  | import java.net.JarURLConnection; | 
|  | import java.net.URL; | 
|  | import java.nio.MappedByteBuffer; | 
|  | import java.nio.channels.FileChannel; | 
|  | import java.nio.file.Files; | 
|  | import java.nio.file.Path; | 
|  | import java.nio.file.Paths; | 
|  | import java.nio.file.StandardOpenOption; | 
|  | import java.util.function.BiPredicate; | 
|  |  | 
|  | /** | 
|  | * Implements low-level access methods for seeking on top of the database file defined by {@code | 
|  | * AndroidApiLevelHashingDatabaseImpl} where a description of the format can also be found. | 
|  | */ | 
|  | public abstract class AndroidApiDataAccess { | 
|  |  | 
|  | private static final String RESOURCE_NAME = "resources/new_api_database.ser"; | 
|  | private static final int ENTRY_SIZE_IN_BITS_FOR_CONSTANT_POOL_MAP = 17; | 
|  | private static final int ENTRY_SIZE_IN_BITS_FOR_API_MAP = 18; | 
|  | // The payload offset is an offset into the payload defined by an integer and a length defined by | 
|  | // a short. | 
|  | private static final int PAYLOAD_OFFSET_WITH_LENGTH = 4 + 2; | 
|  | private static final byte ZERO_BYTE = (byte) 0; | 
|  |  | 
|  | public static boolean isApiDatabaseEntry(String entry) { | 
|  | return RESOURCE_NAME.equals(entry); | 
|  | } | 
|  |  | 
|  | private static class PositionAndLength { | 
|  |  | 
|  | private static final PositionAndLength EMPTY = new PositionAndLength(0, 0); | 
|  |  | 
|  | private final int position; | 
|  | private final int length; | 
|  |  | 
|  | private PositionAndLength(int position, int length) { | 
|  | this.position = position; | 
|  | this.length = length; | 
|  | } | 
|  |  | 
|  | public static PositionAndLength create(int position, int length) { | 
|  | if (position == 0 && length == 0) { | 
|  | return EMPTY; | 
|  | } | 
|  | if ((position < 0 && length > 0) || (position > 0 && length == 0)) { | 
|  | assert false : "Unexpected position and length"; | 
|  | return EMPTY; | 
|  | } | 
|  | return new PositionAndLength(position, length); | 
|  | } | 
|  |  | 
|  | public static PositionAndLength create(byte[] data, int offset) { | 
|  | return create(readIntFromOffset(data, offset), readShortFromOffset(data, offset + 4)); | 
|  | } | 
|  |  | 
|  | public int getPosition() { | 
|  | return position; | 
|  | } | 
|  |  | 
|  | public int getLength() { | 
|  | return length; | 
|  | } | 
|  |  | 
|  | public boolean isEmpty() { | 
|  | return this == EMPTY; | 
|  | } | 
|  | } | 
|  |  | 
|  | public static AndroidApiDataAccess create( | 
|  | InternalOptions options, DiagnosticsHandler diagnosticsHandler) { | 
|  | URL resource = AndroidApiDataAccess.class.getClassLoader().getResource(RESOURCE_NAME); | 
|  | if (resource == null) { | 
|  | diagnosticsHandler.warning( | 
|  | new StringDiagnostic("Could not find the api database at " + RESOURCE_NAME)); | 
|  | return new AndroidApiDataAccessNoBacking(); | 
|  | } | 
|  | if (options.apiModelingOptions().useMemoryMappedByteBuffer) { | 
|  | try { | 
|  | // The resource is encoded as protocol and a path, where we should have one of either: | 
|  | // protocol: file, path: <path-to-file> | 
|  | // protocol: jar, path: file:<path-to-jar>!/<resource-name-in-jar> | 
|  | if (resource.getProtocol().equals("file")) { | 
|  | return getDataAccessFromPathAndOffset(Paths.get(resource.toURI()), 0); | 
|  | } else if (resource.getProtocol().equals("jar") && resource.getPath().startsWith("file:")) { | 
|  | // The path is on form 'file:<path-to-jar>!/<resource-name-in-jar> | 
|  | JarURLConnection jarUrl = (JarURLConnection) resource.openConnection(); | 
|  | File jarFile = new File(jarUrl.getJarFileURL().getFile()); | 
|  | String databaseEntry = jarUrl.getEntryName(); | 
|  | long offsetInJar = getOffsetOfResourceInZip(jarFile, databaseEntry); | 
|  | if (offsetInJar > 0) { | 
|  | return getDataAccessFromPathAndOffset(jarFile.toPath(), offsetInJar); | 
|  | } | 
|  | } | 
|  | // On older DEX platforms creating a new byte channel may fail: | 
|  | // Error: java.lang.NoSuchMethodError: No static method newByteChannel(Ljava/nio/file/Path; | 
|  | // [Ljava/nio/file/OpenOption;)Ljava/nio/channels/SeekableByteChannel; | 
|  | // in class Ljava/nio/file/Files | 
|  | } catch (Exception | NoSuchMethodError e) { | 
|  | diagnosticsHandler.warning(new ExceptionDiagnostic(e)); | 
|  | } | 
|  | diagnosticsHandler.warning( | 
|  | new StringDiagnostic( | 
|  | "Unable to use a memory mapped byte buffer to access the api database. Falling back" | 
|  | + " to loading the database into program which requires more memory")); | 
|  | } | 
|  | try (InputStream apiInputStream = | 
|  | AndroidApiDataAccess.class.getClassLoader().getResourceAsStream(RESOURCE_NAME)) { | 
|  | if (apiInputStream == null) { | 
|  | diagnosticsHandler.warning( | 
|  | new StringDiagnostic("Could not open the api database at " + RESOURCE_NAME)); | 
|  | return new AndroidApiDataAccessNoBacking(); | 
|  | } | 
|  | return new AndroidApiDataAccessInMemory(ByteStreams.toByteArray(apiInputStream)); | 
|  | } catch (IOException e) { | 
|  | diagnosticsHandler.warning(new ExceptionDiagnostic(e)); | 
|  | return new AndroidApiDataAccessNoBacking(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static AndroidApiDataAccessByteMapped getDataAccessFromPathAndOffset( | 
|  | Path path, long offset) throws IOException { | 
|  | FileChannel fileChannel = (FileChannel) Files.newByteChannel(path, StandardOpenOption.READ); | 
|  | MappedByteBuffer mappedByteBuffer = | 
|  | fileChannel.map(FileChannel.MapMode.READ_ONLY, offset, fileChannel.size() - offset); | 
|  | // Ensure that we can run on JDK 8 by using the CompatByteBuffer. | 
|  | return new AndroidApiDataAccessByteMapped(new CompatByteBuffer(mappedByteBuffer)); | 
|  | } | 
|  |  | 
|  | public static int entrySizeInBitsForConstantPoolMap() { | 
|  | return ENTRY_SIZE_IN_BITS_FOR_CONSTANT_POOL_MAP; | 
|  | } | 
|  |  | 
|  | public static int entrySizeInBitsForApiLevelMap() { | 
|  | return ENTRY_SIZE_IN_BITS_FOR_API_MAP; | 
|  | } | 
|  |  | 
|  | public static int apiLevelHash(DexReference reference) { | 
|  | int entrySize = entrySizeInBitsForApiLevelMap(); | 
|  | int size = 1 << (entrySize - 1); | 
|  | return (reference.hashCode() % size) + size; | 
|  | } | 
|  |  | 
|  | public static int constantPoolHash(DexString string) { | 
|  | int entrySize = entrySizeInBitsForConstantPoolMap(); | 
|  | int size = 1 << (entrySize - 1); | 
|  | return (string.hashCode() % size) + size; | 
|  | } | 
|  |  | 
|  | static int constantPoolEntrySize() { | 
|  | return PAYLOAD_OFFSET_WITH_LENGTH; | 
|  | } | 
|  |  | 
|  | static int constantPoolMapEntrySize() { | 
|  | return PAYLOAD_OFFSET_WITH_LENGTH; | 
|  | } | 
|  |  | 
|  | static int apiLevelHashMapEntrySize() { | 
|  | return PAYLOAD_OFFSET_WITH_LENGTH; | 
|  | } | 
|  |  | 
|  | /** The start of the constant pool */ | 
|  | public static int constantPoolOffset() { | 
|  | return 4; | 
|  | } | 
|  |  | 
|  | /** The start of the constant pool hash map. */ | 
|  | public static int constantPoolHashMapOffset(int constantPoolSize) { | 
|  | return (constantPoolSize * constantPoolEntrySize()) + constantPoolOffset(); | 
|  | } | 
|  |  | 
|  | /** The start of the api level hash map. */ | 
|  | public static int apiLevelHashMapOffset(int constantPoolSize) { | 
|  | int constantPoolHashMapSize = | 
|  | (1 << entrySizeInBitsForConstantPoolMap()) * constantPoolMapEntrySize(); | 
|  | return constantPoolHashMapOffset(constantPoolSize) + constantPoolHashMapSize; | 
|  | } | 
|  |  | 
|  | /** The start of the payload section. */ | 
|  | public static int payloadOffset(int constantPoolSize) { | 
|  | int apiLevelSize = (1 << entrySizeInBitsForApiLevelMap()) * apiLevelHashMapEntrySize(); | 
|  | return apiLevelHashMapOffset(constantPoolSize) + apiLevelSize; | 
|  | } | 
|  |  | 
|  | /** The actual byte index of the constant pool index. */ | 
|  | public int constantPoolIndexOffset(int index) { | 
|  | return constantPoolOffset() + (index * constantPoolEntrySize()); | 
|  | } | 
|  |  | 
|  | /** The actual byte index of the constant pool hash key. */ | 
|  | protected int constantPoolHashMapIndexOffset(int hash) { | 
|  | return constantPoolHashMapOffset(getConstantPoolSize()) + (hash * constantPoolMapEntrySize()); | 
|  | } | 
|  |  | 
|  | /** The actual byte index of the api hash key. */ | 
|  | protected int apiLevelHashMapIndexOffset(int hash) { | 
|  | return apiLevelHashMapOffset(getConstantPoolSize()) + (hash * apiLevelHashMapEntrySize()); | 
|  | } | 
|  |  | 
|  | static int readIntFromOffset(byte[] data, int offset) { | 
|  | return Ints.fromBytes(data[offset], data[offset + 1], data[offset + 2], data[offset + 3]); | 
|  | } | 
|  |  | 
|  | static int readShortFromOffset(byte[] data, int offset) { | 
|  | return Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, data[offset], data[offset + 1]); | 
|  | } | 
|  |  | 
|  | private int constantPoolSizeCache = -1; | 
|  |  | 
|  | abstract int readConstantPoolSize(); | 
|  |  | 
|  | abstract PositionAndLength readPositionAndLength(int offset); | 
|  |  | 
|  | abstract boolean payloadHasConstantPoolValue(int offset, int length, byte[] value); | 
|  |  | 
|  | abstract int payloadContainsConstantPoolValue( | 
|  | int offset, int length, byte[] value, BiPredicate<Integer, byte[]> predicate); | 
|  |  | 
|  | abstract byte readApiLevelForPayloadOffset(int offset, int length, byte[] value); | 
|  |  | 
|  | public int getConstantPoolSize() { | 
|  | if (constantPoolSizeCache == -1) { | 
|  | constantPoolSizeCache = readConstantPoolSize(); | 
|  | } | 
|  | return constantPoolSizeCache; | 
|  | } | 
|  |  | 
|  | /** When the first bit is set (position < 0) then there is a single unique result for the hash. */ | 
|  | public static boolean isUniqueConstantPoolEntry(int position) { | 
|  | return position < 0; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * If the position defines a unique result, the first byte is has the first bit set to 1 (making | 
|  | * it negative) and the actual index specified in the least significant two bytes. | 
|  | */ | 
|  | public static int getConstantPoolIndexFromUniqueConstantPoolEntry(int position) { | 
|  | assert isUniqueConstantPoolEntry(position); | 
|  | return unsetBitAtIndex(position, 32); | 
|  | } | 
|  |  | 
|  | public int getConstantPoolIndex(DexString string) { | 
|  | PositionAndLength constantPoolIndex = | 
|  | readPositionAndLength(constantPoolHashMapIndexOffset(constantPoolHash(string))); | 
|  | if (constantPoolIndex.isEmpty()) { | 
|  | return -1; | 
|  | } | 
|  | int position = constantPoolIndex.getPosition(); | 
|  | int length = constantPoolIndex.getLength(); | 
|  | if (isUniqueConstantPoolEntry(position)) { | 
|  | int nonTaggedPosition = getConstantPoolIndexFromUniqueConstantPoolEntry(position); | 
|  | if (isConstantPoolEntry(nonTaggedPosition, string.content)) { | 
|  | return nonTaggedPosition; | 
|  | } | 
|  | } else { | 
|  | assert length > 0; | 
|  | return payloadContainsConstantPoolValue( | 
|  | payloadOffset(getConstantPoolSize()) + position, | 
|  | length, | 
|  | string.content, | 
|  | this::isConstantPoolEntry); | 
|  | } | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | public boolean isConstantPoolEntry(int index, byte[] value) { | 
|  | PositionAndLength constantPoolPayloadOffset = | 
|  | readPositionAndLength(constantPoolIndexOffset(index)); | 
|  | if (constantPoolPayloadOffset.isEmpty()) { | 
|  | return false; | 
|  | } | 
|  | if (value.length != constantPoolPayloadOffset.getLength()) { | 
|  | return false; | 
|  | } | 
|  | return payloadHasConstantPoolValue( | 
|  | payloadOffset(getConstantPoolSize()) + constantPoolPayloadOffset.getPosition(), | 
|  | constantPoolPayloadOffset.getLength(), | 
|  | value); | 
|  | } | 
|  |  | 
|  | public byte getApiLevelForReference(byte[] serialized, DexReference reference) { | 
|  | PositionAndLength apiLevelPayloadOffset = | 
|  | readPositionAndLength(apiLevelHashMapIndexOffset(apiLevelHash(reference))); | 
|  | if (apiLevelPayloadOffset.isEmpty()) { | 
|  | return 0; | 
|  | } | 
|  | return readApiLevelForPayloadOffset( | 
|  | payloadOffset(getConstantPoolSize()) + apiLevelPayloadOffset.getPosition(), | 
|  | apiLevelPayloadOffset.getLength(), | 
|  | serialized); | 
|  | } | 
|  |  | 
|  | public boolean isNoBacking() { | 
|  | return false; | 
|  | } | 
|  |  | 
|  | public static class AndroidApiDataAccessByteMapped extends AndroidApiDataAccess { | 
|  |  | 
|  | private final CompatByteBuffer mappedByteBuffer; | 
|  |  | 
|  | public AndroidApiDataAccessByteMapped(CompatByteBuffer mappedByteBuffer) { | 
|  | this.mappedByteBuffer = mappedByteBuffer; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | int readConstantPoolSize() { | 
|  | return mappedByteBuffer.getInt(0); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public PositionAndLength readPositionAndLength(int offset) { | 
|  | return PositionAndLength.create( | 
|  | mappedByteBuffer.getInt(offset), mappedByteBuffer.getShort(offset + 4)); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | boolean payloadHasConstantPoolValue(int offset, int length, byte[] value) { | 
|  | assert length == value.length; | 
|  | mappedByteBuffer.position(offset); | 
|  | for (byte expected : value) { | 
|  | if (expected != mappedByteBuffer.get()) { | 
|  | return false; | 
|  | } | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | int payloadContainsConstantPoolValue( | 
|  | int offset, int length, byte[] value, BiPredicate<Integer, byte[]> predicate) { | 
|  | for (int i = offset; i < offset + length; i += 2) { | 
|  | // Do not use mappedByteBuffer.getShort() since that will add the sign. | 
|  | int index = | 
|  | Ints.fromBytes( | 
|  | ZERO_BYTE, ZERO_BYTE, mappedByteBuffer.get(i), mappedByteBuffer.get(i + 1)); | 
|  | if (predicate.test(index, value)) { | 
|  | return index; | 
|  | } | 
|  | } | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | byte readApiLevelForPayloadOffset(int offset, int length, byte[] value) { | 
|  | int currentOffset = offset; | 
|  | while (currentOffset < offset + length) { | 
|  | // Read the length | 
|  | int lengthOfEntry = | 
|  | Ints.fromBytes( | 
|  | ZERO_BYTE, | 
|  | ZERO_BYTE, | 
|  | mappedByteBuffer.get(currentOffset), | 
|  | mappedByteBuffer.get(currentOffset + 1)); | 
|  | int startPosition = currentOffset + 2; | 
|  | if (value.length == lengthOfEntry | 
|  | && payloadHasConstantPoolValue(startPosition, lengthOfEntry, value)) { | 
|  | return mappedByteBuffer.get(startPosition + lengthOfEntry); | 
|  | } | 
|  | // Advance our current position + length of entry + api level. | 
|  | currentOffset = startPosition + lengthOfEntry + 1; | 
|  | } | 
|  | return -1; | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class AndroidApiDataAccessInMemory extends AndroidApiDataAccess { | 
|  |  | 
|  | private final byte[] data; | 
|  |  | 
|  | private AndroidApiDataAccessInMemory(byte[] data) { | 
|  | this.data = data; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int readConstantPoolSize() { | 
|  | return readIntFromOffset(data, 0); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | PositionAndLength readPositionAndLength(int offset) { | 
|  | return PositionAndLength.create(data, offset); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | boolean payloadHasConstantPoolValue(int offset, int length, byte[] value) { | 
|  | if (value.length != length) { | 
|  | return false; | 
|  | } | 
|  | for (int i = 0; i < length; i++) { | 
|  | if (value[i] != data[i + offset]) { | 
|  | return false; | 
|  | } | 
|  | } | 
|  | return true; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | int payloadContainsConstantPoolValue( | 
|  | int offset, int length, byte[] value, BiPredicate<Integer, byte[]> predicate) { | 
|  | if (data.length < length) { | 
|  | return -1; | 
|  | } | 
|  | for (int i = offset; i < offset + length; i += 2) { | 
|  | int index = Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, data[i], data[i + 1]); | 
|  | if (predicate.test(index, value)) { | 
|  | return index; | 
|  | } | 
|  | } | 
|  | return -1; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | byte readApiLevelForPayloadOffset(int offset, int length, byte[] value) { | 
|  | int index = offset; | 
|  | while (index < offset + length) { | 
|  | // Read size of entry | 
|  | int lengthOfEntry = Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, data[index], data[index + 1]); | 
|  | int startIndex = index + 2; | 
|  | int endIndex = startIndex + lengthOfEntry; | 
|  | if (payloadHasConstantPoolValue(startIndex, lengthOfEntry, value)) { | 
|  | return data[endIndex]; | 
|  | } | 
|  | index = endIndex + 1; | 
|  | } | 
|  | return 0; | 
|  | } | 
|  | } | 
|  |  | 
|  | public static class AndroidApiDataAccessNoBacking extends AndroidApiDataAccess { | 
|  |  | 
|  | @Override | 
|  | int readConstantPoolSize() { | 
|  | throw new Unreachable(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | PositionAndLength readPositionAndLength(int offset) { | 
|  | throw new Unreachable(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | boolean payloadHasConstantPoolValue(int offset, int length, byte[] value) { | 
|  | throw new Unreachable(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | int payloadContainsConstantPoolValue( | 
|  | int offset, int length, byte[] value, BiPredicate<Integer, byte[]> predicate) { | 
|  | throw new Unreachable(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | byte readApiLevelForPayloadOffset(int offset, int length, byte[] value) { | 
|  | throw new Unreachable(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean isNoBacking() { | 
|  | return true; | 
|  | } | 
|  | } | 
|  | } |