blob: a4a423ed6e3db73158920680aa2c08f73e455e65 [file] [log] [blame]
Paul Duffindfa10832021-05-13 17:31:51 +01001#!/usr/bin/env python
2#
3# Copyright (C) 2018 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""
17Verify that one set of hidden API flags is a subset of another.
18"""
19
20import argparse
21import csv
Paul Duffinc11f6672021-07-20 00:04:21 +010022import sys
Paul Duffin53a76072021-07-21 17:27:09 +010023from itertools import chain
Paul Duffindfa10832021-05-13 17:31:51 +010024
Paul Duffinc11f6672021-07-20 00:04:21 +010025class InteriorNode:
26 """
27 An interior node in a trie.
28
29 Each interior node has a dict that maps from an element of a signature to
30 either another interior node or a leaf. Each interior node represents either
31 a package, class or nested class. Class members are represented by a Leaf.
32
33 Associating the set of flags [public-api] with the signature
34 "Ljava/lang/Object;->String()Ljava/lang/String;" will cause the following
35 nodes to be created:
36 Node()
37 ^- package:java -> Node()
38 ^- package:lang -> Node()
39 ^- class:Object -> Node()
40 ^- member:String()Ljava/lang/String; -> Leaf([public-api])
41
42 Associating the set of flags [blocked,core-platform-api] with the signature
43 "Ljava/lang/Character$UnicodeScript;->of(I)Ljava/lang/Character$UnicodeScript;"
44 will cause the following nodes to be created:
45 Node()
46 ^- package:java -> Node()
47 ^- package:lang -> Node()
48 ^- class:Character -> Node()
49 ^- class:UnicodeScript -> Node()
50 ^- member:of(I)Ljava/lang/Character$UnicodeScript;
51 -> Leaf([blocked,core-platform-api])
52
53 Attributes:
54 nodes: a dict from an element of the signature to the Node/Leaf
55 containing the next element/value.
56 """
57 def __init__(self):
58 self.nodes = {}
59
60 def signatureToElements(self, signature):
61 """
62 Split a signature or a prefix into a number of elements:
63 1. The packages (excluding the leading L preceding the first package).
64 2. The class names, from outermost to innermost.
65 3. The member signature.
66
67 e.g. Ljava/lang/Character$UnicodeScript;->of(I)Ljava/lang/Character$UnicodeScript;
68 will be broken down into these elements:
69 1. package:java
70 2. package:lang
71 3. class:Character
72 4. class:UnicodeScript
73 5. member:of(I)Ljava/lang/Character$UnicodeScript;
74 """
75 # Remove the leading L.
76 # - java/lang/Character$UnicodeScript;->of(I)Ljava/lang/Character$UnicodeScript;
77 text = signature.removeprefix("L")
78 # Split the signature between qualified class name and the class member
79 # signature.
80 # 0 - java/lang/Character$UnicodeScript
81 # 1 - of(I)Ljava/lang/Character$UnicodeScript;
82 parts = text.split(";->")
83 member = parts[1:]
84 # Split the qualified class name into packages, and class name.
85 # 0 - java
86 # 1 - lang
87 # 2 - Character$UnicodeScript
88 elements = parts[0].split("/")
89 packages = elements[0:-1]
90 className = elements[-1]
91 if className == "*" or className == "**":
92 # Cannot specify a wildcard and target a specific member
93 if len(member) != 0:
94 raise Exception("Invalid signature %s: contains wildcard %s and member signature %s"
95 % (signature, className, member[0]))
96 wildcard = [className]
97 # Assemble the parts into a single list, adding prefixes to identify
98 # the different parts.
99 # 0 - package:java
100 # 1 - package:lang
101 # 2 - *
102 return list(chain(map(lambda x : "package:" + x, packages),
103 wildcard))
104 else:
105 # Split the class name into outer / inner classes
106 # 0 - Character
107 # 1 - UnicodeScript
108 classes = className.split("$")
109 # Assemble the parts into a single list, adding prefixes to identify
110 # the different parts.
111 # 0 - package:java
112 # 1 - package:lang
113 # 2 - class:Character
114 # 3 - class:UnicodeScript
115 # 4 - member:of(I)Ljava/lang/Character$UnicodeScript;
116 return list(chain(map(lambda x : "package:" + x, packages),
117 map(lambda x : "class:" + x, classes),
118 map(lambda x : "member:" + x, member)))
119
120 def add(self, signature, value):
121 """
122 Associate the value with the specific signature.
123 :param signature: the member signature
124 :param value: the value to associated with the signature
125 :return: n/a
126 """
127 # Split the signature into elements.
128 elements = self.signatureToElements(signature)
129 # Find the Node associated with the deepest class.
130 node = self
131 for element in elements[:-1]:
132 if element in node.nodes:
133 node = node.nodes[element]
134 else:
135 next = InteriorNode()
136 node.nodes[element] = next
137 node = next
138 # Add a Leaf containing the value and associate it with the member
139 # signature within the class.
140 lastElement = elements[-1]
141 if not lastElement.startswith("member:"):
142 raise Exception("Invalid signature: %s, does not identify a specific member" % signature)
143 if lastElement in node.nodes:
144 raise Exception("Duplicate signature: %s" % signature)
145 node.nodes[lastElement] = Leaf(value)
146
147 def getMatchingRows(self, pattern):
148 """
149 Get the values (plural) associated with the pattern.
150
151 e.g. If the pattern is a full signature then this will return a list
152 containing the value associated with that signature.
153
154 If the pattern is a class then this will return a list containing the
155 values associated with all members of that class.
156
157 If the pattern is a package then this will return a list containing the
158 values associated with all the members of all the classes in that
159 package and sub-packages.
160
161 If the pattern ends with "*" then the preceding part is treated as a
162 package and this will return a list containing the values associated
163 with all the members of all the classes in that package.
164
165 If the pattern ends with "**" then the preceding part is treated
166 as a package and this will return a list containing the values
167 associated with all the members of all the classes in that package and
168 all sub-packages.
169
170 :param pattern: the pattern which could be a complete signature or a
171 class, or package wildcard.
172 :return: an iterable containing all the values associated with the
173 pattern.
174 """
175 elements = self.signatureToElements(pattern)
176 node = self
177 # Include all values from this node and all its children.
178 selector = lambda x : True
179 lastElement = elements[-1]
180 if lastElement == "*" or lastElement == "**":
181 elements = elements[:-1]
182 if lastElement == "*":
183 # Do not include values from sub-packages.
184 selector = lambda x : not x.startswith("package:")
185 for element in elements:
186 if element in node.nodes:
187 node = node.nodes[element]
188 else:
189 return []
190 return chain.from_iterable(node.values(selector))
191
192 def values(self, selector):
193 """
194 :param selector: a function that can be applied to a key in the nodes
195 attribute to determine whether to return its values.
196 :return: A list of iterables of all the values associated with this
197 node and its children.
198 """
199 values = []
200 self.appendValues(values, selector)
201 return values
202
203 def appendValues(self, values, selector):
204 """
205 Append the values associated with this node and its children to the
206 list.
207
208 For each item (key, child) in nodes the child node's values are returned
209 if and only if the selector returns True when called on its key. A child
210 node's values are all the values associated with it and all its
211 descendant nodes.
212
213 :param selector: a function that can be applied to a key in the nodes
214 attribute to determine whether to return its values.
215 :param values: a list of a iterables of values.
216 """
217 for key, node in self.nodes.items():
218 if selector(key):
219 node.appendValues(values, lambda x : True)
220
221class Leaf:
222 """
223 A leaf of the trie
224
225 Attributes:
226 value: the value associated with this leaf.
227 """
228 def __init__(self, value):
229 self.value = value
230
231 def values(self, selector):
232 """
233 :return: A list of a list of the value associated with this node.
234 """
235 return [[self.value]]
236
237 def appendValues(self, values, selector):
238 """
239 Appends a list of the value associated with this node to the list.
240 :param values: a list of a iterables of values.
241 """
242 values.append([self.value])
243
Paul Duffindfa10832021-05-13 17:31:51 +0100244def dict_reader(input):
245 return csv.DictReader(input, delimiter=',', quotechar='|', fieldnames=['signature'])
246
Paul Duffinc11f6672021-07-20 00:04:21 +0100247def read_flag_trie_from_file(file):
248 with open(file, 'r') as stream:
249 return read_flag_trie_from_stream(stream)
250
251def read_flag_trie_from_stream(stream):
252 trie = InteriorNode()
253 reader = dict_reader(stream)
254 for row in reader:
255 signature = row['signature']
256 trie.add(signature, row)
257 return trie
258
259def extract_subset_from_monolithic_flags_as_dict_from_file(monolithicTrie, patternsFile):
Paul Duffin53a76072021-07-21 17:27:09 +0100260 """
261 Extract a subset of flags from the dict containing all the monolithic flags.
262
263 :param monolithicFlagsDict: the dict containing all the monolithic flags.
Paul Duffin67b9d612021-07-21 17:38:47 +0100264 :param patternsFile: a file containing a list of signature patterns that
265 define the subset.
266 :return: the dict from signature to row.
267 """
268 with open(patternsFile, 'r') as stream:
Paul Duffinc11f6672021-07-20 00:04:21 +0100269 return extract_subset_from_monolithic_flags_as_dict_from_stream(monolithicTrie, stream)
Paul Duffin67b9d612021-07-21 17:38:47 +0100270
Paul Duffinc11f6672021-07-20 00:04:21 +0100271def extract_subset_from_monolithic_flags_as_dict_from_stream(monolithicTrie, stream):
Paul Duffin67b9d612021-07-21 17:38:47 +0100272 """
Paul Duffinc11f6672021-07-20 00:04:21 +0100273 Extract a subset of flags from the trie containing all the monolithic flags.
Paul Duffin67b9d612021-07-21 17:38:47 +0100274
Paul Duffinc11f6672021-07-20 00:04:21 +0100275 :param monolithicTrie: the trie containing all the monolithic flags.
Paul Duffin67b9d612021-07-21 17:38:47 +0100276 :param stream: a stream containing a list of signature patterns that define
277 the subset.
Paul Duffin53a76072021-07-21 17:27:09 +0100278 :return: the dict from signature to row.
279 """
280 dict = {}
Paul Duffinc11f6672021-07-20 00:04:21 +0100281 for pattern in stream:
282 pattern = pattern.rstrip()
283 rows = monolithicTrie.getMatchingRows(pattern)
284 for row in rows:
285 signature = row['signature']
286 dict[signature] = row
Paul Duffin53a76072021-07-21 17:27:09 +0100287 return dict
288
Paul Duffin428c6512021-07-21 15:33:22 +0100289def read_signature_csv_from_stream_as_dict(stream):
290 """
291 Read the csv contents from the stream into a dict. The first column is assumed to be the
292 signature and used as the key. The whole row is stored as the value.
293
294 :param stream: the csv contents to read
295 :return: the dict from signature to row.
296 """
297 dict = {}
298 reader = dict_reader(stream)
299 for row in reader:
Paul Duffindfa10832021-05-13 17:31:51 +0100300 signature = row['signature']
Paul Duffin428c6512021-07-21 15:33:22 +0100301 dict[signature] = row
302 return dict
Paul Duffindfa10832021-05-13 17:31:51 +0100303
Paul Duffin428c6512021-07-21 15:33:22 +0100304def read_signature_csv_from_file_as_dict(csvFile):
305 """
306 Read the csvFile into a dict. The first column is assumed to be the
307 signature and used as the key. The whole row is stored as the value.
308
309 :param csvFile: the csv file to read
310 :return: the dict from signature to row.
311 """
312 with open(csvFile, 'r') as f:
313 return read_signature_csv_from_stream_as_dict(f)
314
315def compare_signature_flags(monolithicFlagsDict, modularFlagsDict):
316 """
317 Compare the signature flags between the two dicts.
318
319 :param monolithicFlagsDict: the dict containing the subset of the monolithic
320 flags that should be equal to the modular flags.
321 :param modularFlagsDict:the dict containing the flags produced by a single
322 bootclasspath_fragment module.
323 :return: list of mismatches., each mismatch is a tuple where the first item
324 is the signature, and the second and third items are lists of the flags from
325 modular dict, and monolithic dict respectively.
326 """
Paul Duffindfa10832021-05-13 17:31:51 +0100327 mismatchingSignatures = []
Paul Duffin53a76072021-07-21 17:27:09 +0100328 # Create a sorted set of all the signatures from both the monolithic and
329 # modular dicts.
330 allSignatures = sorted(set(chain(monolithicFlagsDict.keys(), modularFlagsDict.keys())))
331 for signature in allSignatures:
Paul Duffin428c6512021-07-21 15:33:22 +0100332 monolithicRow = monolithicFlagsDict.get(signature, {})
333 monolithicFlags = monolithicRow.get(None, [])
Paul Duffin53a76072021-07-21 17:27:09 +0100334 modularRow = modularFlagsDict.get(signature, {})
335 modularFlags = modularRow.get(None, [])
Paul Duffin428c6512021-07-21 15:33:22 +0100336 if monolithicFlags != modularFlags:
337 mismatchingSignatures.append((signature, modularFlags, monolithicFlags))
338 return mismatchingSignatures
Paul Duffindfa10832021-05-13 17:31:51 +0100339
Paul Duffin428c6512021-07-21 15:33:22 +0100340def main(argv):
Paul Duffin7be96332021-07-21 16:13:03 +0100341 args_parser = argparse.ArgumentParser(description='Verify that sets of hidden API flags are each a subset of the monolithic flag file.')
342 args_parser.add_argument('monolithicFlags', help='The monolithic flag file')
343 args_parser.add_argument('modularFlags', nargs=argparse.REMAINDER, help='Flags produced by individual bootclasspath_fragment modules')
Paul Duffin428c6512021-07-21 15:33:22 +0100344 args = args_parser.parse_args(argv[1:])
Paul Duffindfa10832021-05-13 17:31:51 +0100345
Paul Duffinc11f6672021-07-20 00:04:21 +0100346 # Read in all the flags into the trie
Paul Duffin7be96332021-07-21 16:13:03 +0100347 monolithicFlagsPath = args.monolithicFlags
Paul Duffinc11f6672021-07-20 00:04:21 +0100348 monolithicTrie = read_flag_trie_from_file(monolithicFlagsPath)
Paul Duffindfa10832021-05-13 17:31:51 +0100349
Paul Duffin53a76072021-07-21 17:27:09 +0100350 # For each subset specified on the command line, create dicts for the flags
Paul Duffinc11f6672021-07-20 00:04:21 +0100351 # provided by the subset and the corresponding flags from the complete set
352 # of flags and compare them.
Paul Duffin428c6512021-07-21 15:33:22 +0100353 failed = False
Paul Duffin67b9d612021-07-21 17:38:47 +0100354 for modularPair in args.modularFlags:
355 parts = modularPair.split(":")
356 modularFlagsPath = parts[0]
357 modularPatternsPath = parts[1]
Paul Duffin7be96332021-07-21 16:13:03 +0100358 modularFlagsDict = read_signature_csv_from_file_as_dict(modularFlagsPath)
Paul Duffinc11f6672021-07-20 00:04:21 +0100359 monolithicFlagsSubsetDict = extract_subset_from_monolithic_flags_as_dict_from_file(monolithicTrie, modularPatternsPath)
Paul Duffin53a76072021-07-21 17:27:09 +0100360 mismatchingSignatures = compare_signature_flags(monolithicFlagsSubsetDict, modularFlagsDict)
Paul Duffin428c6512021-07-21 15:33:22 +0100361 if mismatchingSignatures:
362 failed = True
363 print("ERROR: Hidden API flags are inconsistent:")
Paul Duffin7be96332021-07-21 16:13:03 +0100364 print("< " + modularFlagsPath)
365 print("> " + monolithicFlagsPath)
Paul Duffin428c6512021-07-21 15:33:22 +0100366 for mismatch in mismatchingSignatures:
367 signature = mismatch[0]
368 print()
369 print("< " + ",".join([signature]+ mismatch[1]))
370 print("> " + ",".join([signature]+ mismatch[2]))
371
372 if failed:
373 sys.exit(1)
374
375if __name__ == "__main__":
376 main(sys.argv)