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