xref: /aosp_15_r20/build/soong/scripts/hiddenapi/generate_hiddenapi_lists.py (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1#!/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"""Generate API lists for non-SDK API enforcement."""
17import argparse
18from collections import defaultdict, namedtuple
19import re
20import sys
21
22# Names of flags recognized by the `hiddenapi` tool.
23FLAG_SDK = 'sdk'
24FLAG_UNSUPPORTED = 'unsupported'
25FLAG_BLOCKED = 'blocked'
26FLAG_MAX_TARGET_O = 'max-target-o'
27FLAG_MAX_TARGET_P = 'max-target-p'
28FLAG_MAX_TARGET_Q = 'max-target-q'
29FLAG_MAX_TARGET_R = 'max-target-r'
30FLAG_MAX_TARGET_S = 'max-target-s'
31FLAG_CORE_PLATFORM_API = 'core-platform-api'
32FLAG_PUBLIC_API = 'public-api'
33FLAG_SYSTEM_API = 'system-api'
34FLAG_TEST_API = 'test-api'
35
36# List of all known flags.
37FLAGS_API_LIST = [
38    FLAG_SDK,
39    FLAG_UNSUPPORTED,
40    FLAG_BLOCKED,
41    FLAG_MAX_TARGET_O,
42    FLAG_MAX_TARGET_P,
43    FLAG_MAX_TARGET_Q,
44    FLAG_MAX_TARGET_R,
45    FLAG_MAX_TARGET_S,
46]
47ALL_FLAGS = FLAGS_API_LIST + [
48    FLAG_CORE_PLATFORM_API,
49    FLAG_PUBLIC_API,
50    FLAG_SYSTEM_API,
51    FLAG_TEST_API,
52]
53
54FLAGS_API_LIST_SET = set(FLAGS_API_LIST)
55ALL_FLAGS_SET = set(ALL_FLAGS)
56
57# Option specified after one of FLAGS_API_LIST to indicate that
58# only known and otherwise unassigned entries should be assign the
59# given flag.
60# For example, the max-target-P list is checked in as it was in P,
61# but signatures have changes since then. The flag instructs this
62# script to skip any entries which do not exist any more.
63FLAG_IGNORE_CONFLICTS = 'ignore-conflicts'
64
65# Option specified after one of FLAGS_API_LIST to express that all
66# apis within a given set of packages should be assign the given flag.
67FLAG_PACKAGES = 'packages'
68
69# Option specified after one of FLAGS_API_LIST to indicate an extra
70# tag that should be added to the matching APIs.
71FLAG_TAG = 'tag'
72
73# Regex patterns of fields/methods used in serialization. These are
74# considered public API despite being hidden.
75SERIALIZATION_PATTERNS = [
76    r'readObject\(Ljava/io/ObjectInputStream;\)V',
77    r'readObjectNoData\(\)V',
78    r'readResolve\(\)Ljava/lang/Object;',
79    r'serialVersionUID:J',
80    r'serialPersistentFields:\[Ljava/io/ObjectStreamField;',
81    r'writeObject\(Ljava/io/ObjectOutputStream;\)V',
82    r'writeReplace\(\)Ljava/lang/Object;',
83]
84
85# Single regex used to match serialization API. It combines all the
86# SERIALIZATION_PATTERNS into a single regular expression.
87SERIALIZATION_REGEX = re.compile(r'.*->(' + '|'.join(SERIALIZATION_PATTERNS) +
88                                 r')$')
89
90# Predicates to be used with filter_apis.
91HAS_NO_API_LIST_ASSIGNED = \
92    lambda api,flags: not FLAGS_API_LIST_SET.intersection(flags)
93
94IS_SERIALIZATION = lambda api, flags: SERIALIZATION_REGEX.match(api)
95
96
97class StoreOrderedOptions(argparse.Action):
98    """An argparse action that stores a number of option arguments in the order
99
100    that they were specified.
101    """
102
103    def __call__(self, parser, args, values, option_string=None):
104        items = getattr(args, self.dest, None)
105        if items is None:
106            items = []
107        items.append([option_string.lstrip('-'), values])
108        setattr(args, self.dest, items)
109
110
111def get_args():
112    """Parses command line arguments.
113
114    Returns:
115        Namespace: dictionary of parsed arguments
116    """
117    parser = argparse.ArgumentParser()
118    parser.add_argument('--output', required=True)
119    parser.add_argument(
120        '--csv',
121        nargs='*',
122        default=[],
123        metavar='CSV_FILE',
124        help='CSV files to be merged into output')
125
126    for flag in ALL_FLAGS:
127        parser.add_argument(
128            '--' + flag,
129            dest='ordered_flags',
130            metavar='TXT_FILE',
131            action=StoreOrderedOptions,
132            help='lists of entries with flag "' + flag + '"')
133    parser.add_argument(
134        '--' + FLAG_IGNORE_CONFLICTS,
135        dest='ordered_flags',
136        nargs=0,
137        action=StoreOrderedOptions,
138        help='Indicates that only known and otherwise unassigned '
139        'entries should be assign the given flag. Must follow a list of '
140        'entries and applies to the preceding such list.')
141    parser.add_argument(
142        '--' + FLAG_PACKAGES,
143        dest='ordered_flags',
144        nargs=0,
145        action=StoreOrderedOptions,
146        help='Indicates that the previous list of entries '
147        'is a list of packages. All members in those packages will be given '
148        'the flag. Must follow a list of entries and applies to the preceding '
149        'such list.')
150    parser.add_argument(
151        '--' + FLAG_TAG,
152        dest='ordered_flags',
153        nargs=1,
154        action=StoreOrderedOptions,
155        help='Adds an extra tag to the previous list of entries. '
156        'Must follow a list of entries and applies to the preceding such list.')
157
158    return parser.parse_args()
159
160
161def read_lines(filename):
162    """Reads entire file and return it as a list of lines.
163
164    Lines which begin with a hash are ignored.
165
166    Args:
167        filename (string): Path to the file to read from.
168
169    Returns:
170        Lines of the file as a list of string.
171    """
172    with open(filename, 'r') as f:
173        lines = f.readlines()
174    lines = [line for line in lines if not line.startswith('#')]
175    lines = [line.strip() for line in lines]
176    return set(lines)
177
178
179def write_lines(filename, lines):
180    """Writes list of lines into a file, overwriting the file if it exists.
181
182    Args:
183        filename (string): Path to the file to be writing into.
184        lines (list): List of strings to write into the file.
185    """
186    lines = [line + '\n' for line in lines]
187    with open(filename, 'w') as f:
188        f.writelines(lines)
189
190
191def extract_package(signature):
192    """Extracts the package from a signature.
193
194    Args:
195        signature (string): JNI signature of a method or field.
196
197    Returns:
198        The package name of the class containing the field/method.
199    """
200    full_class_name = signature.split(';->')[0]
201    # Example: Landroid/hardware/radio/V1_2/IRadio$Proxy
202    if full_class_name[0] != 'L':
203        raise ValueError("Expected to start with 'L': %s"
204                         % full_class_name)
205    full_class_name = full_class_name[1:]
206    # If full_class_name doesn't contain '/', then package_name will be ''.
207    package_name = full_class_name.rpartition('/')[0]
208    return package_name.replace('/', '.')
209
210
211class FlagsDict:
212
213    def __init__(self):
214        self._dict_keyset = set()
215        self._dict = defaultdict(set)
216
217    def _check_entries_set(self, keys_subset, source):
218        assert isinstance(keys_subset, set)
219        assert keys_subset.issubset(self._dict_keyset), (
220            'Error: {} specifies signatures not present in code:\n'
221            '{}'
222            'Please visit go/hiddenapi for more information.').format(
223                source, ''.join(
224                    ['  ' + str(x) + '\n' for x in
225                     keys_subset - self._dict_keyset]))
226
227    def _check_flags_set(self, flags_subset, source):
228        assert isinstance(flags_subset, set)
229        assert flags_subset.issubset(ALL_FLAGS_SET), (
230            'Error processing: {}\n'
231            'The following flags were not recognized: \n'
232            '{}\n'
233            'Please visit go/hiddenapi for more information.').format(
234                source, '\n'.join(flags_subset - ALL_FLAGS_SET))
235
236    def filter_apis(self, filter_fn):
237        """Returns APIs which match a given predicate.
238
239        This is a helper function which allows to filter on both signatures
240        (keys) and
241        flags (values). The built-in filter() invokes the lambda only with
242        dict's keys.
243
244        Args:
245            filter_fn : Function which takes two arguments (signature/flags) and
246              returns a boolean.
247
248        Returns:
249            A set of APIs which match the predicate.
250        """
251        return {x for x in self._dict_keyset if filter_fn(x, self._dict[x])}
252
253    def get_valid_subset_of_unassigned_apis(self, api_subset):
254        """Sanitizes a key set input to only include keys which exist in the
255
256        dictionary and have not been assigned any API list flags.
257
258        Args:
259            entries_subset (set/list): Key set to be sanitized.
260
261        Returns:
262            Sanitized key set.
263        """
264        assert isinstance(api_subset, set)
265        return api_subset.intersection(
266            self.filter_apis(HAS_NO_API_LIST_ASSIGNED))
267
268    def generate_csv(self):
269        """Constructs CSV entries from a dictionary.
270
271        Old versions of flags are used to generate the file.
272
273        Returns:
274            List of lines comprising a CSV file. See "parse_and_merge_csv" for
275            format description.
276        """
277        lines = []
278        for api in self._dict:
279            flags = sorted(self._dict[api])
280            lines.append(','.join([api] + flags))
281        return sorted(lines)
282
283    def parse_and_merge_csv(self, csv_lines, source='<unknown>'):
284        """Parses CSV entries and merges them into a given dictionary.
285
286        The expected CSV format is:
287            <api signature>,<flag1>,<flag2>,...,<flagN>
288
289        Args:
290            csv_lines (list of strings): Lines read from a CSV file.
291            source (string): Origin of `csv_lines`. Will be printed in error
292              messages.
293        Throws: AssertionError if parsed flags are invalid.
294        """
295        # Split CSV lines into arrays of values.
296        csv_values = [line.split(',') for line in csv_lines]
297
298        # Update the full set of API signatures.
299        self._dict_keyset.update([csv[0] for csv in csv_values])
300
301        # Check that all flags are known.
302        csv_flags = set()
303        for csv in csv_values:
304            csv_flags.update(csv[1:])
305        self._check_flags_set(csv_flags, source)
306
307        # Iterate over all CSV lines, find entry in dict and append flags to it.
308        for csv in csv_values:
309            flags = csv[1:]
310            if (FLAG_PUBLIC_API in flags) or (FLAG_SYSTEM_API in flags):
311                flags.append(FLAG_SDK)
312            self._dict[csv[0]].update(flags)
313
314    def assign_flag(self, flag, apis, source='<unknown>', tag=None):
315        """Assigns a flag to given subset of entries.
316
317        Args:
318            flag (string): One of ALL_FLAGS.
319            apis (set): Subset of APIs to receive the flag.
320            source (string): Origin of `entries_subset`. Will be printed in
321              error messages.
322        Throws: AssertionError if parsed API signatures of flags are invalid.
323        """
324        # Check that all APIs exist in the dict.
325        self._check_entries_set(apis, source)
326
327        # Check that the flag is known.
328        self._check_flags_set(set([flag]), source)
329
330        # Iterate over the API subset, find each entry in dict and assign the
331        # flag to it.
332        for api in apis:
333            self._dict[api].add(flag)
334            if tag:
335                self._dict[api].add(tag)
336
337
338FlagFile = namedtuple('FlagFile',
339                      ('flag', 'file', 'ignore_conflicts', 'packages', 'tag'))
340
341
342def parse_ordered_flags(ordered_flags):
343    r = []
344    currentflag, file, ignore_conflicts, packages, tag = None, None, False, \
345        False, None
346    for flag_value in ordered_flags:
347        flag, value = flag_value[0], flag_value[1]
348        if flag in ALL_FLAGS_SET:
349            if currentflag:
350                r.append(
351                    FlagFile(currentflag, file, ignore_conflicts, packages,
352                             tag))
353                ignore_conflicts, packages, tag = False, False, None
354            currentflag = flag
355            file = value
356        else:
357            if currentflag is None:
358                raise argparse.ArgumentError( #pylint: disable=no-value-for-parameter
359                    '--%s is only allowed after one of %s' %
360                    (flag, ' '.join(['--%s' % f for f in ALL_FLAGS_SET])))
361            if flag == FLAG_IGNORE_CONFLICTS:
362                ignore_conflicts = True
363            elif flag == FLAG_PACKAGES:
364                packages = True
365            elif flag == FLAG_TAG:
366                tag = value[0]
367
368    if currentflag:
369        r.append(FlagFile(currentflag, file, ignore_conflicts, packages, tag))
370    return r
371
372
373def main(argv): #pylint: disable=unused-argument
374    # Parse arguments.
375    args = vars(get_args())
376    flagfiles = parse_ordered_flags(args['ordered_flags'] or [])
377
378    # Initialize API->flags dictionary.
379    flags = FlagsDict()
380
381    # Merge input CSV files into the dictionary.
382    # Do this first because CSV files produced by parsing API stubs will
383    # contain the full set of APIs. Subsequent additions from text files
384    # will be able to detect invalid entries, and/or filter all as-yet
385    # unassigned entries.
386    for filename in args['csv']:
387        flags.parse_and_merge_csv(read_lines(filename), filename)
388
389    # Combine inputs which do not require any particular order.
390    # (1) Assign serialization API to SDK.
391    flags.assign_flag(FLAG_SDK, flags.filter_apis(IS_SERIALIZATION))
392
393    # (2) Merge text files with a known flag into the dictionary.
394    for info in flagfiles:
395        if (not info.ignore_conflicts) and (not info.packages):
396            flags.assign_flag(info.flag, read_lines(info.file), info.file,
397                              info.tag)
398
399    # Merge text files where conflicts should be ignored.
400    # This will only assign the given flag if:
401    # (a) the entry exists, and
402    # (b) it has not been assigned any other flag.
403    # Because of (b), this must run after all strict assignments have been
404    # performed.
405    for info in flagfiles:
406        if info.ignore_conflicts:
407            valid_entries = flags.get_valid_subset_of_unassigned_apis(
408                read_lines(info.file))
409            flags.assign_flag(info.flag, valid_entries, filename, info.tag) #pylint: disable=undefined-loop-variable
410
411    # All members in the specified packages will be assigned the appropriate
412    # flag.
413    for info in flagfiles:
414        if info.packages:
415            packages_needing_list = set(read_lines(info.file))
416            should_add_signature_to_list = lambda sig, lists: extract_package(
417                sig) in packages_needing_list and not lists #pylint: disable=cell-var-from-loop
418            valid_entries = flags.filter_apis(should_add_signature_to_list)
419            flags.assign_flag(info.flag, valid_entries, info.file, info.tag)
420
421    # Mark all remaining entries as blocked.
422    flags.assign_flag(FLAG_BLOCKED, flags.filter_apis(HAS_NO_API_LIST_ASSIGNED))
423
424    # Write output.
425    write_lines(args['output'], flags.generate_csv())
426
427
428if __name__ == '__main__':
429    main(sys.argv)
430