1 /*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.android.tools.metalava.apilevels
17
18 import com.android.tools.metalava.SdkIdentifier
19 import javax.xml.parsers.SAXParserFactory
20 import org.xml.sax.Attributes
21 import org.xml.sax.helpers.DefaultHandler
22
23 /**
24 * The base of dessert release independent SDKs.
25 *
26 * A dessert release independent SDK is one which is not coupled to the Android dessert release
27 * numbering. Any SDK greater than or equal to this is not comparable to either each other, or to
28 * the Android dessert release. e.g. `1000000` is not the same as, later than, or earlier than
29 * SDK 31. Similarly, `1000001` is not the same as, later than, or earlier then `1000000`.
30 */
31 private const val DESSERT_RELEASE_INDEPENDENT_SDK_BASE = 1000000
32
33 /**
34 * A filter of classes, fields and methods that are allowed in and extension SDK, and for each item,
35 * what extension SDK it first appeared in. Also, a mapping between SDK name and numerical ID.
36 *
37 * Internally, the filters are represented as a tree, where each node in the tree matches a part of
38 * a package, class or member name. For example, given the patterns
39 *
40 * com.example.Foo -> [A] com.example.Foo#someMethod -> [B] com.example.Bar -> [A, C]
41 *
42 * (anything prefixed with com.example.Foo is allowed and part of the A extension, except for
43 * com.example.Foo#someMethod which is part of B; anything prefixed with com.example.Bar is part of
44 * both A and C), the internal tree looks like
45 *
46 * root -> null com -> null example -> null Foo -> [A] someMethod -> [B] Bar -> [A, C]
47 */
48 class ApiToExtensionsMap
49 private constructor(
50 private val sdkIdentifiers: Set<SdkIdentifier>,
51 private val root: Node,
52 ) {
isEmptynull53 fun isEmpty(): Boolean = root.children.isEmpty() && root.extensions.isEmpty()
54
55 fun getExtensions(clazz: ApiClass): List<String> = getExtensions(clazz.name.toDotNotation())
56
57 fun getExtensions(clazz: ApiClass, member: ApiElement): List<String> =
58 getExtensions(clazz.name.toDotNotation() + "#" + member.name.toDotNotation())
59
60 fun getExtensions(what: String): List<String> {
61 // Special case: getExtensionVersion is not part of an extension
62 val sdkExtensions = "android.os.ext.SdkExtensions"
63 if (what == sdkExtensions || what == "$sdkExtensions#getExtensionVersion") {
64 return listOf()
65 }
66
67 val parts = what.split(REGEX_DELIMITERS)
68
69 var lastSeenExtensions = root.extensions
70 var node = root.children.findNode(parts[0]) ?: return lastSeenExtensions
71 if (node.extensions.isNotEmpty()) {
72 lastSeenExtensions = node.extensions
73 }
74
75 for (part in parts.stream().skip(1)) {
76 node = node.children.findNode(part) ?: break
77 if (node.extensions.isNotEmpty()) {
78 lastSeenExtensions = node.extensions
79 }
80 }
81 return lastSeenExtensions
82 }
83
getSdkIdentifiersnull84 fun getSdkIdentifiers(): Set<SdkIdentifier> = sdkIdentifiers.toSet()
85
86 /**
87 * Construct a `sdks` attribute value
88 *
89 * `sdks` is an XML attribute on class, method and fields in the XML generated by
90 * ARG_GENERATE_API_LEVELS. It expresses in what SDKs an API exist, and in which version of each
91 * SDK it was first introduced; `sdks` replaces the `since` attribute.
92 *
93 * The format of `sdks` is
94 *
95 * sdks="ext:version[,ext:version[,...]]
96 *
97 * where <ext> is the numerical ID of the SDK, and <version> is the version in which the API was
98 * introduced.
99 *
100 * The returned string is guaranteed to be one of
101 * - list of (extensions,finalized_version) pairs + ANDROID_SDK:finalized_dessert
102 * - list of (extensions,finalized_version) pairs
103 * - ANDROID_SDK:finalized_dessert
104 * - ANDROID_SDK:next_dessert_int (for symbols not finalized anywhere)
105 *
106 * See go/mainline-sdk-api-versions-xml for more information.
107 *
108 * @param androidSince Android dessert version in which this symbol was finalized, or
109 * notFinalizedValue if this symbol has not been finalized in an Android dessert
110 * @param notFinalizedValue value used together with the Android SDK ID to indicate that this
111 * symbol has not been finalized at all
112 * @param extensions names of the SDK extensions in which this symbol has been finalized (may be
113 * non-empty even if extensionsSince is ApiElement.NEVER)
114 * @param extensionsSince the version of the SDK extensions in which this API was initially
115 * introduced (same value for all SDK extensions), or ApiElement.NEVER if this symbol has not
116 * been finalized in any SDK extension (regardless of the extensions argument)
117 * @return an `sdks` value suitable for including verbatim in XML
118 */
119 fun calculateSdksAttr(
120 androidSince: Int,
121 notFinalizedValue: Int,
122 extensions: List<String>,
123 extensionsSince: Int
124 ): String {
125 // Special case: symbol not finalized anywhere -> "ANDROID_SDK:next_dessert_int"
126 if (androidSince == notFinalizedValue && extensionsSince == ApiElement.NEVER) {
127 return "$ANDROID_PLATFORM_SDK_ID:$notFinalizedValue"
128 }
129
130 val versions = mutableSetOf<String>()
131 // Only include SDK extensions if the symbol has been finalized in at least one
132 if (extensionsSince != ApiElement.NEVER) {
133 for (ext in extensions) {
134 val ident =
135 sdkIdentifiers.find { it.shortname == ext }
136 ?: throw IllegalStateException("unknown extension SDK \"$ext\"")
137 assert(ident.id != ANDROID_PLATFORM_SDK_ID) // invariant
138 if (ident.id >= DESSERT_RELEASE_INDEPENDENT_SDK_BASE || ident.id <= androidSince) {
139 versions.add("${ident.id}:$extensionsSince")
140 }
141 }
142 }
143
144 // Only include the Android SDK in `sdks` if
145 // - the symbol has been finalized in an Android dessert, and
146 // - the symbol has been finalized in at least one SDK extension
147 if (androidSince != notFinalizedValue && versions.isNotEmpty()) {
148 versions.add("$ANDROID_PLATFORM_SDK_ID:$androidSince")
149 }
150 return versions.joinToString(",")
151 }
152
153 companion object {
154 // Hard-coded ID for the Android platform SDK. Used identically as the extension SDK IDs
155 // to express when an API first appeared in an SDK.
156 const val ANDROID_PLATFORM_SDK_ID = 0
157
158 private val REGEX_DELIMITERS = Regex("[.#$]")
159
160 /**
161 * Create an ApiToExtensionsMap from a list of text based rules.
162 *
163 * The input is XML:
164 *
165 * <?xml version="1.0" encoding="utf-8"?>
166 * <sdk-extensions-info version="1">
167 * <sdk name="<name>" shortname="<short-name>" id="<int>" reference="<constant>" />
168 * <symbol jar="<jar>" pattern="<pattern>" sdks="<sdks>" />
169 * </sdk-extensions-info>
170 *
171 * The <sdk> and <symbol> tags may be repeated.
172 * - <name> is a long name for the SDK, e.g. "R Extensions".
173 * - <short-name> is a short name for the SDK, e.g. "R-ext".
174 * - <id> is the numerical identifier for the SDK, e.g. 30. It is an error to use the
175 * Android SDK ID (0).
176 * - <jar> is the jar file symbol belongs to, named after the jar file in
177 * prebuilts/sdk/extensions/<int>/public, e.g. "framework-sdkextensions".
178 * - <constant> is a Java symbol that can be passed to `SdkExtensions.getExtensionVersion`
179 * to look up the version of the corresponding SDK, e.g.
180 * "android/os/Build$VERSION_CODES$R"
181 * - <pattern> is either '*', which matches everything, or a 'com.foo.Bar$Inner#member'
182 * string (or prefix thereof terminated before . or $), which matches anything with that
183 * prefix. Note that arguments and return values of methods are omitted (and there is no
184 * way to distinguish overloaded methods).
185 * - <sdks> is a comma separated list of SDKs in which the symbol defined by <jar> and
186 * <pattern> appears; the list items are <name> attributes of SDKs defined in the XML.
187 *
188 * It is an error to specify the same <jar> and <pattern> pair twice.
189 *
190 * A more specific <symbol> rule has higher precedence than a less specific rule.
191 *
192 * @param filterByJar jar file to limit lookups to: ignore symbols not present in this jar
193 * file
194 * @param xml XML as described above
195 * @throws IllegalArgumentException if the XML is malformed
196 */
fromXmlnull197 fun fromXml(filterByJar: String, xml: String): ApiToExtensionsMap {
198 val root = Node("<root>")
199 val sdkIdentifiers = mutableSetOf<SdkIdentifier>()
200 val allSeenExtensions = mutableSetOf<String>()
201
202 val parser = SAXParserFactory.newDefaultInstance().newSAXParser()
203 try {
204 parser.parse(
205 xml.byteInputStream(),
206 object : DefaultHandler() {
207 override fun startElement(
208 uri: String,
209 localName: String,
210 qualifiedName: String,
211 attributes: Attributes
212 ) {
213 when (qualifiedName) {
214 "sdk" -> {
215 val id = attributes.getIntOrThrow(qualifiedName, "id")
216 val shortname =
217 attributes.getStringOrThrow(qualifiedName, "shortname")
218 val name = attributes.getStringOrThrow(qualifiedName, "name")
219 val reference =
220 attributes.getStringOrThrow(qualifiedName, "reference")
221 sdkIdentifiers.add(
222 SdkIdentifier(id, shortname, name, reference)
223 )
224 }
225 "symbol" -> {
226 val jar = attributes.getStringOrThrow(qualifiedName, "jar")
227 if (jar != filterByJar) {
228 return
229 }
230 val sdks =
231 attributes
232 .getStringOrThrow(qualifiedName, "sdks")
233 .split(',')
234 if (sdks != sdks.distinct()) {
235 throw IllegalArgumentException(
236 "symbol lists the same SDK multiple times: '$sdks'"
237 )
238 }
239 allSeenExtensions.addAll(sdks)
240 val pattern =
241 attributes.getStringOrThrow(qualifiedName, "pattern")
242 if (pattern == "*") {
243 root.extensions = sdks
244 return
245 }
246 // add each part of the pattern as separate nodes, e.g. if
247 // pattern is
248 // com.example.Foo, add nodes, "com" -> "example" -> "Foo"
249 val parts = pattern.split(REGEX_DELIMITERS)
250 var node = root.children.addNode(parts[0])
251 for (name in parts.stream().skip(1)) {
252 node = node.children.addNode(name)
253 }
254 if (node.extensions.isNotEmpty()) {
255 throw IllegalArgumentException(
256 "duplicate pattern: $pattern"
257 )
258 }
259 node.extensions = sdks
260 }
261 }
262 }
263 }
264 )
265 } catch (e: Throwable) {
266 throw IllegalArgumentException("failed to parse xml", e)
267 }
268
269 // verify: the predefined Android platform SDK ID is not reused as an extension SDK ID
270 if (sdkIdentifiers.any { it.id == ANDROID_PLATFORM_SDK_ID }) {
271 throw IllegalArgumentException(
272 "bad SDK definition: the ID $ANDROID_PLATFORM_SDK_ID is reserved for the Android platform SDK"
273 )
274 }
275
276 // verify: all rules refer to declared SDKs
277 val allSdkNames = sdkIdentifiers.map { it.shortname }.toList()
278 for (ext in allSeenExtensions) {
279 if (!allSdkNames.contains(ext)) {
280 throw IllegalArgumentException("bad SDK definitions: undefined SDK $ext")
281 }
282 }
283
284 // verify: no duplicate SDK IDs
285 if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.id }.size) {
286 throw IllegalArgumentException("bad SDK definitions: duplicate SDK IDs")
287 }
288
289 // verify: no duplicate SDK names
290 if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.shortname }.size) {
291 throw IllegalArgumentException("bad SDK definitions: duplicate SDK short names")
292 }
293
294 // verify: no duplicate SDK names
295 if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.name }.size) {
296 throw IllegalArgumentException("bad SDK definitions: duplicate SDK names")
297 }
298
299 // verify: no duplicate SDK references
300 if (sdkIdentifiers.size != sdkIdentifiers.distinctBy { it.reference }.size) {
301 throw IllegalArgumentException("bad SDK definitions: duplicate SDK references")
302 }
303
304 return ApiToExtensionsMap(sdkIdentifiers, root)
305 }
306 }
307 }
308
MutableSetnull309 private fun MutableSet<Node>.addNode(name: String): Node {
310 findNode(name)?.let {
311 return it
312 }
313 val node = Node(name)
314 add(node)
315 return node
316 }
317
Attributesnull318 private fun Attributes.getStringOrThrow(tag: String, attr: String): String =
319 getValue(attr) ?: throw IllegalArgumentException("<$tag>: missing attribute: $attr")
320
321 private fun Attributes.getIntOrThrow(tag: String, attr: String): Int =
322 getStringOrThrow(tag, attr).toIntOrNull()
323 ?: throw IllegalArgumentException("<$tag>: attribute $attr: not an integer")
324
325 private fun Set<Node>.findNode(breadcrumb: String): Node? = find { it.breadcrumb == breadcrumb }
326
Stringnull327 private fun String.toDotNotation(): String = split('(')[0].replace('/', '.')
328
329 private class Node(val breadcrumb: String) {
330 var extensions: List<String> = emptyList()
331 val children: MutableSet<Node> = mutableSetOf()
332 }
333