blob: 832ed393b11c045af9608aca6a10b32c15582bed [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.lint.checks
import com.android.sdklib.IAndroidTarget
import com.android.tools.lint.checks.ApiDatabase.CacheCreator
import com.android.tools.lint.client.api.LintClient
import com.google.common.annotations.VisibleForTesting
import java.io.File
import java.net.URL
class PrivateApiLookup private constructor(
client: LintClient,
binaryFile: File,
cacheCreator: CacheCreator
) : ApiDatabase() {
init {
readData(client, binaryFile, cacheCreator, PRIVATE_API_BINARY_FORMAT_VERSION)
}
fun getMethodRestriction(owner: String, name: String, desc: String): Restriction {
if (mData != null) {
val classNumber = findClass(owner)
if (classNumber >= 0) {
return decode(findMember(classNumber, name, desc))
}
}
return Restriction.UNKNOWN
}
fun getFieldRestriction(owner: String, name: String): Restriction {
if (mData != null) {
val classNumber = findClass(owner)
if (classNumber >= 0) {
return decode(findMember(classNumber, name, null))
}
}
return Restriction.UNKNOWN
}
private fun findMember(classNumber: Int, name: String, desc: String?): Int {
var curr = seekClassData(classNumber)
// 3 bytes for first offset
var low = get3ByteInt(mData, curr)
curr += 3
val length = get2ByteInt(mData, curr)
if (length == 0) {
return -1
}
var high = low + length
while (low < high) {
val middle = (low + high).ushr(1)
var offset = mIndices[middle]
if (DEBUG_SEARCH) {
println("Comparing string $name$desc with entry at $offset: " + dumpEntry(offset))
}
var compare: Int
if (desc != null) {
// Method
val nameLength = name.length
compare = compare(mData, offset, '('.toByte(), name, 0, nameLength)
if (compare == 0) {
offset += nameLength
val argsEnd = desc.indexOf(')')
// Only compare up to the ) -- after that we have a return value in the
// input description, which isn't there in the database.
compare =
compare(mData, offset, ')'.toByte(), desc, 0, argsEnd)
if (compare == 0) {
if (DEBUG_SEARCH) {
println("Found " + dumpEntry(offset))
}
offset += argsEnd + 1
if (mData[offset++].toInt() == 0) {
// Yes, terminated argument list: get the API level
return mData[offset].toInt()
}
}
}
} else {
// Field
val nameLength = name.length
compare = compare(mData, offset, 0.toByte(), name, 0, nameLength)
if (compare == 0) {
offset += nameLength
if (mData[offset++].toInt() == 0) {
// Yes, terminated argument list: get the API level
return mData[offset].toInt()
}
}
}
if (compare < 0) {
low = middle + 1
} else if (compare > 0) {
high = middle
} else {
assert(false) // compare == 0 already handled above
return -1
}
}
return -1
}
private fun seekClassData(classNumber: Int): Int {
val offset = mIndices[classNumber]
return offset + (mData[offset].toInt() and 0xFF)
}
companion object {
@VisibleForTesting
internal const val DEBUG_FORCE_REGENERATE_BINARY = false
private const val API_FILE_PATH = "private-apis.txt" // relative to Lint's resources dir
private const val PRIVATE_API_BINARY_FORMAT_VERSION = 0
private fun getCacheFileName(fileName: String, buildNumber: String?): String =
buildString(100) {
if (fileName.endsWith(".txt")) {
append(fileName.substring(0, fileName.length - 4))
} else {
append(fileName)
}
// Incorporate version number in the filename to avoid upgrade filename
// conflicts on Windows (such as issue #26663)
append('-').append(getBinaryFormatVersion(PRIVATE_API_BINARY_FORMAT_VERSION))
if (buildNumber != null) {
append('-').append(buildNumber.replace(' ', '_'))
}
append(".bin")
}
private fun cacheCreator(input: URL) = CacheCreator { client, binaryData ->
val begin = if (WRITE_STATS) System.currentTimeMillis() else 0
val info: Api<PrivateApiClass>
try {
info = Api.parseHiddenApi(input)
} catch (e: RuntimeException) {
client.log(e, "Can't read private API file")
return@CacheCreator false
}
if (WRITE_STATS) {
val end = System.currentTimeMillis()
println("Reading private API data took " + (end - begin) + " ms")
}
try {
writeDatabase(binaryData, info, PRIVATE_API_BINARY_FORMAT_VERSION)
return@CacheCreator true
} catch (t: Throwable) {
client.log(t, "Can't write private API cache file")
}
false
}
/**
* Returns an instance of the private API database
*
* @param client the client to associate with this database - used only for logging
* @param target the associated Android target, if known
* @return a (possibly shared) instance of the API database, or null if its data can't be found
*/
private fun get(client: LintClient, target: IAndroidTarget?): PrivateApiLookup? {
val stream = PrivateApiLookup::class.java.classLoader.getResource(API_FILE_PATH)
if (stream == null) {
client.log(null, "The API database file $API_FILE_PATH could not be found")
return null
}
// The first line contains the build number for the API data, to be used in the name of
// the binary file.
val version = stream.openStream().use { it.bufferedReader(Charsets.UTF_8).readLine() }
// TODO: Check that we don't end up locking Lint's jar file on Windows
val cacheDir = client.getCacheDir(null, true)
if (cacheDir == null) {
client.log(null, "Can't create cache dir")
return null
}
val binaryData = File(cacheDir, getCacheFileName(API_FILE_PATH, version))
val cache = cacheCreator(stream)
if (DEBUG_FORCE_REGENERATE_BINARY) {
System.err.println(
"\nTemporarily regenerating binary data unconditionally \n" +
"from $stream\nto $binaryData"
)
if (!cache.create(client, binaryData)) {
return null
}
} else if ((!binaryData.exists() || binaryData.length() == 0L)) {
if (!cache.create(client, binaryData)) {
return null
}
}
if (!binaryData.exists()) {
client.log(null, "The API database file %1\$s does not exist", binaryData)
return null
}
return PrivateApiLookup(client, binaryData, cache)
}
fun get(client: LintClient): PrivateApiLookup? {
synchronized(PrivateApiLookup::class.java) {
return get(client, null)
}
}
}
}