From 4e6ba919f559c2b67d32d294e59140085dd8b821 Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Fri, 31 Mar 2023 17:40:17 +0900 Subject: [PATCH] Add a tool to generate hidden module API lists The tool takes a hiddenapi list generated by soong and excludes module APIs from a stubs jar. Flag: EXEMPT host-side tool Bug: 406489362 Test: see Android.bp on how to run the tool Change-Id: Ic8ee90278acfb650b76cd1b9ab7484131f63ec8e --- tools/hiddenapi/Android.bp | 49 +++++ .../hiddenapi/HiddenNetApiList.kt | 193 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 tools/hiddenapi/Android.bp create mode 100644 tools/hiddenapi/src/com/android/connectivity/hiddenapi/HiddenNetApiList.kt diff --git a/tools/hiddenapi/Android.bp b/tools/hiddenapi/Android.bp new file mode 100644 index 0000000000..88b0662fde --- /dev/null +++ b/tools/hiddenapi/Android.bp @@ -0,0 +1,49 @@ +// +// Copyright (C) 2025 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 { + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +java_library { + name: "framework-connectivity-module-lib-stubs-dexed-for-hiddenapi-list", + static_libs: [ + "framework-tethering.stubs.module_lib", + "framework-connectivity.stubs.module_lib", + "framework-connectivity-t.stubs.module_lib", + "framework-connectivity-b.stubs.module_lib", + ], + compile_dex: true, + visibility: ["//visibility:private"], +} + +// A generator for hidden API lists for connectivity: removes module API from the blocked list. +// Run on: out/soong/.intermediates/packages/modules/Connectivity/Tethering/apex/ +// com.android.tethering-bootclasspath-fragment/android_common_apex30/ +// modular-hiddenapi/all-flags.csv +// With: java -jar NetHiddenApiListGenerator.jar \ +// all-flags.csv \ +// framework-connectivity-module-lib-stubs-dexed-for-hiddenapi-list.jar \ +// outfile.csv +java_binary_host { + name: "NetHiddenApiListGenerator", + srcs: ["src/**/*.kt"], + main_class: "com.android.connectivity.hiddenapi.HiddenNetApiList", + static_libs: [ + "kotlin-test", + ], +} diff --git a/tools/hiddenapi/src/com/android/connectivity/hiddenapi/HiddenNetApiList.kt b/tools/hiddenapi/src/com/android/connectivity/hiddenapi/HiddenNetApiList.kt new file mode 100644 index 0000000000..34eba43f3b --- /dev/null +++ b/tools/hiddenapi/src/com/android/connectivity/hiddenapi/HiddenNetApiList.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2025 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. + */ + +@file:JvmName("HiddenNetApiList") + +package com.android.connectivity.hiddenapi + +import java.io.BufferedReader +import java.io.BufferedWriter +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileReader +import java.io.FileWriter +import java.io.SequenceInputStream +import javax.xml.parsers.SAXParserFactory +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import org.xml.sax.Attributes +import org.xml.sax.helpers.DefaultHandler + +private data class Options(val clobber: Boolean) +private data class Inputs(val flagsFile: File, val moduleApiStubs: File, val outFile: File) + +fun main(args: Array) { + val (options, arguments) = args.partition { it.startsWith("-c") } + val clobber = options.contains("-c") + assertTrue( + arguments.size == 3, + "Usage: EXECNAME <-c: clobber> flagsFile moduleApiStubsFile outFile" + ) + val hiddenApi = + makeHiddenApiSet(flagsFile = File(arguments[0]), moduleApiStubs = File(arguments[1])) + + val outFile = File(arguments[2]) + if (!clobber) { + assertTrue(!outFile.exists(), "$outFile already exists") + } + + BufferedWriter(FileWriter(outFile)).use { writer -> + writer.appendLine("apis = [") + hiddenApi.forEach { + writer.appendLine(" '$it',") + } + writer.appendLine("]") + } +} + +private fun makeHiddenApiSet(flagsFile: File, moduleApiStubs: File): Set { + assertTrue(flagsFile.exists(), "Could not find flags file at $flagsFile") + + // Example method: + // Landroid/net/NetworkTemplate;->getBackwardsCompatibleMatchRule(I)I,blocked + // Example constant: + // Landroid/net/OemNetworkPreferences;->OEM_NETWORK_PREFERENCE_MAX:I,blocked + val hiddenApiRegex = Regex("""(L.+?;)->(.+)(?:(\(.*\))|:)(.+?),(.+)""") + val accessibleTags = listOf("core-platform-api", "public-api", "system-api") + val hiddenApis = mutableMapOf() + BufferedReader(FileReader(flagsFile)).use { reader -> + reader.forEachLine { + val match = assertNotNull( + hiddenApiRegex.matchEntire(it), + "$it did not match regex: $hiddenApiRegex" + ) + val flags = match.groups[5]?.value?.split(",") ?: emptyList() + if (flags.intersect(accessibleTags).isNotEmpty()) return@forEachLine // continue + val symbolClass = assertNotNull(match.groups[1]?.value) + val symbolName = assertNotNull(match.groups[2]?.value) + // Constants do not have arguments so arguments may be null + val arguments = match.groups[3]?.value + val returnType = assertNotNull(match.groups[4]?.value) + + // dexdump format is, example method: + // Landroid/net/IpPrefix;.contains:(Ljava/net/InetAddress;)Z + // Example constant: + // Landroid/net/LinkAddress;.CREATOR:Landroid/os/Parcelable$Creator; + hiddenApis["$symbolClass.$symbolName:${arguments ?: ""}$returnType"] = it + } + } + + // Module API is also considered "blocked". Remove them from the list based on the stubs. + assertTrue(moduleApiStubs.exists(), "Could not find API stubs file at $moduleApiStubs") + + val moduleApiDump = ProcessBuilder( + "dexdump", + "-l", + "xml", + "-e", + "-n", + moduleApiStubs.absolutePath.toString() + ).start() + + val parsedHandler = moduleApiDump.inputStream.use { stream -> + DexParserHandler().also { + SAXParserFactory.newInstance().newSAXParser().parse(stream, it) + } + } + hiddenApis.keys.removeAll(parsedHandler.parsedSymbols) + + return hiddenApis.values.toSet() +} + +class DexParserHandler : DefaultHandler() { + private var pkg = "" + private var clazz = "" + + private var currentElementName = "" + private var currentElementType = "" + private val currentParameters = mutableListOf() + + val parsedSymbols = mutableSetOf() + + override fun startElement( + uri: String, + localName: String, + qName: String, + attributes: Attributes + ) { + when (qName) { + "package" -> pkg = attributes.getValue("name") + "class" -> clazz = attributes.getValue("name") + "field" -> { + currentElementName = attributes.getValue("name") + currentElementType = toInternalType(attributes.getValue("type")) + } + "method" -> { + currentElementName = attributes.getValue("name") + currentElementType = toInternalType(attributes.getValue("return")) + currentParameters.clear() + } + "constructor" -> { + currentElementName = "" + currentElementType = toInternalType("void") + currentParameters.clear() + } + "parameter" -> { + currentParameters.add(toInternalType(attributes.getValue("type"))) + } + } + } + + override fun endElement(uri: String, localName: String, qName: String) { + when (qName) { + "field" -> { + parsedSymbols.add( + toInternalType("$pkg.$clazz") + + ".$currentElementName:$currentElementType" + ) + } + "method", "constructor" -> { + val parameters = currentParameters.joinToString(separator = "") + parsedSymbols.add( + toInternalType("$pkg.$clazz") + + ".$currentElementName:($parameters)$currentElementType" + ) + } + } + } + + /** + * Convert from dexdump XML output type (java.lang.String or boolean[]) to internal type + * (Ljava/lang/String; or [Z) + */ + private fun toInternalType(type: String): String { + if (type.endsWith("[]")) { + return "[" + toInternalType(type.substring(0, type.length - 2)) + } + return when (type) { + "void" -> "V" + "byte" -> "B" + "char" -> "C" + "double" -> "D" + "float" -> "F" + "int" -> "I" + "long" -> "J" + "short" -> "S" + "boolean" -> "Z" + else -> 'L' + type.replace('.', '/') + ';' + } + } +}