blob: 6781eba055340caa862d5eca1451e4242cb7e480 [file] [log] [blame]
David Brazdil8503b902018-08-30 13:35:03 +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"""
17Generate API lists for non-SDK API enforcement.
David Brazdil8503b902018-08-30 13:35:03 +010018"""
19import argparse
David Brazdil17d16e82018-12-13 17:00:09 +000020from collections import defaultdict
David Brazdil8503b902018-08-30 13:35:03 +010021import os
22import sys
23import re
24
David Brazdil89bf0f22018-10-30 18:21:24 +000025# Names of flags recognized by the `hiddenapi` tool.
26FLAG_WHITELIST = "whitelist"
27FLAG_GREYLIST = "greylist"
28FLAG_BLACKLIST = "blacklist"
29FLAG_GREYLIST_MAX_O = "greylist-max-o"
David Brazdil5cd148f2018-11-01 09:54:25 +000030FLAG_GREYLIST_MAX_P = "greylist-max-p"
David Brazdil439d3492018-12-07 11:49:55 +000031FLAG_CORE_PLATFORM_API = "core-platform-api"
Andrei Onea80a56602019-03-01 18:49:15 +000032FLAG_PUBLIC_API = "public-api"
33FLAG_SYSTEM_API = "system-api"
34FLAG_TEST_API = "test-api"
David Brazdil89bf0f22018-10-30 18:21:24 +000035
36# List of all known flags.
David Brazdil439d3492018-12-07 11:49:55 +000037FLAGS_API_LIST = [
David Brazdil89bf0f22018-10-30 18:21:24 +000038 FLAG_WHITELIST,
39 FLAG_GREYLIST,
40 FLAG_BLACKLIST,
41 FLAG_GREYLIST_MAX_O,
David Brazdil5cd148f2018-11-01 09:54:25 +000042 FLAG_GREYLIST_MAX_P,
David Brazdil89bf0f22018-10-30 18:21:24 +000043]
Andrei Onea80a56602019-03-01 18:49:15 +000044ALL_FLAGS = FLAGS_API_LIST + [
45 FLAG_CORE_PLATFORM_API,
46 FLAG_PUBLIC_API,
47 FLAG_SYSTEM_API,
48 FLAG_TEST_API,
49 ]
David Brazdil439d3492018-12-07 11:49:55 +000050
51FLAGS_API_LIST_SET = set(FLAGS_API_LIST)
52ALL_FLAGS_SET = set(ALL_FLAGS)
David Brazdil89bf0f22018-10-30 18:21:24 +000053
54# Suffix used in command line args to express that only known and
55# otherwise unassigned entries should be assign the given flag.
56# For example, the P dark greylist is checked in as it was in P,
57# but signatures have changes since then. The flag instructs this
58# script to skip any entries which do not exist any more.
59FLAG_IGNORE_CONFLICTS_SUFFIX = "-ignore-conflicts"
60
61# Regex patterns of fields/methods used in serialization. These are
62# considered public API despite being hidden.
63SERIALIZATION_PATTERNS = [
64 r'readObject\(Ljava/io/ObjectInputStream;\)V',
65 r'readObjectNoData\(\)V',
66 r'readResolve\(\)Ljava/lang/Object;',
67 r'serialVersionUID:J',
68 r'serialPersistentFields:\[Ljava/io/ObjectStreamField;',
69 r'writeObject\(Ljava/io/ObjectOutputStream;\)V',
70 r'writeReplace\(\)Ljava/lang/Object;',
71]
72
73# Single regex used to match serialization API. It combines all the
74# SERIALIZATION_PATTERNS into a single regular expression.
75SERIALIZATION_REGEX = re.compile(r'.*->(' + '|'.join(SERIALIZATION_PATTERNS) + r')$')
76
77# Predicates to be used with filter_apis.
David Brazdil439d3492018-12-07 11:49:55 +000078HAS_NO_API_LIST_ASSIGNED = lambda api, flags: not FLAGS_API_LIST_SET.intersection(flags)
David Brazdil89bf0f22018-10-30 18:21:24 +000079IS_SERIALIZATION = lambda api, flags: SERIALIZATION_REGEX.match(api)
80
David Brazdil8503b902018-08-30 13:35:03 +010081def get_args():
82 """Parses command line arguments.
83
84 Returns:
85 Namespace: dictionary of parsed arguments
86 """
87 parser = argparse.ArgumentParser()
David Brazdil89bf0f22018-10-30 18:21:24 +000088 parser.add_argument('--output', required=True)
David Brazdil89bf0f22018-10-30 18:21:24 +000089 parser.add_argument('--csv', nargs='*', default=[], metavar='CSV_FILE',
90 help='CSV files to be merged into output')
91
David Brazdil439d3492018-12-07 11:49:55 +000092 for flag in ALL_FLAGS:
David Brazdil89bf0f22018-10-30 18:21:24 +000093 ignore_conflicts_flag = flag + FLAG_IGNORE_CONFLICTS_SUFFIX
94 parser.add_argument('--' + flag, dest=flag, nargs='*', default=[], metavar='TXT_FILE',
95 help='lists of entries with flag "' + flag + '"')
96 parser.add_argument('--' + ignore_conflicts_flag, dest=ignore_conflicts_flag, nargs='*',
97 default=[], metavar='TXT_FILE',
98 help='lists of entries with flag "' + flag +
99 '". skip entry if missing or flag conflict.')
100
David Brazdil8503b902018-08-30 13:35:03 +0100101 return parser.parse_args()
102
103def read_lines(filename):
104 """Reads entire file and return it as a list of lines.
105
David Brazdilae88d4e2018-09-06 14:46:55 +0100106 Lines which begin with a hash are ignored.
107
David Brazdil8503b902018-08-30 13:35:03 +0100108 Args:
109 filename (string): Path to the file to read from.
110
111 Returns:
David Brazdil89bf0f22018-10-30 18:21:24 +0000112 Lines of the file as a list of string.
David Brazdil8503b902018-08-30 13:35:03 +0100113 """
114 with open(filename, 'r') as f:
David Brazdil89bf0f22018-10-30 18:21:24 +0000115 lines = f.readlines();
116 lines = filter(lambda line: not line.startswith('#'), lines)
117 lines = map(lambda line: line.strip(), lines)
118 return set(lines)
David Brazdil8503b902018-08-30 13:35:03 +0100119
120def write_lines(filename, lines):
121 """Writes list of lines into a file, overwriting the file it it exists.
122
123 Args:
124 filename (string): Path to the file to be writting into.
125 lines (list): List of strings to write into the file.
126 """
David Brazdil89bf0f22018-10-30 18:21:24 +0000127 lines = map(lambda line: line + '\n', lines)
David Brazdil8503b902018-08-30 13:35:03 +0100128 with open(filename, 'w') as f:
129 f.writelines(lines)
130
David Brazdil89bf0f22018-10-30 18:21:24 +0000131class FlagsDict:
David Brazdil17d16e82018-12-13 17:00:09 +0000132 def __init__(self):
133 self._dict_keyset = set()
134 self._dict = defaultdict(set)
David Brazdil4a55eeb2018-09-11 11:09:01 +0100135
David Brazdil89bf0f22018-10-30 18:21:24 +0000136 def _check_entries_set(self, keys_subset, source):
137 assert isinstance(keys_subset, set)
138 assert keys_subset.issubset(self._dict_keyset), (
139 "Error processing: {}\n"
140 "The following entries were unexpected:\n"
141 "{}"
142 "Please visit go/hiddenapi for more information.").format(
143 source, "".join(map(lambda x: " " + str(x), keys_subset - self._dict_keyset)))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100144
David Brazdil89bf0f22018-10-30 18:21:24 +0000145 def _check_flags_set(self, flags_subset, source):
146 assert isinstance(flags_subset, set)
David Brazdil439d3492018-12-07 11:49:55 +0000147 assert flags_subset.issubset(ALL_FLAGS_SET), (
David Brazdil89bf0f22018-10-30 18:21:24 +0000148 "Error processing: {}\n"
149 "The following flags were not recognized: \n"
150 "{}\n"
151 "Please visit go/hiddenapi for more information.").format(
David Brazdil439d3492018-12-07 11:49:55 +0000152 source, "\n".join(flags_subset - ALL_FLAGS_SET))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100153
David Brazdil89bf0f22018-10-30 18:21:24 +0000154 def filter_apis(self, filter_fn):
155 """Returns APIs which match a given predicate.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100156
David Brazdil89bf0f22018-10-30 18:21:24 +0000157 This is a helper function which allows to filter on both signatures (keys) and
158 flags (values). The built-in filter() invokes the lambda only with dict's keys.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100159
David Brazdil89bf0f22018-10-30 18:21:24 +0000160 Args:
161 filter_fn : Function which takes two arguments (signature/flags) and returns a boolean.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100162
David Brazdil89bf0f22018-10-30 18:21:24 +0000163 Returns:
164 A set of APIs which match the predicate.
165 """
166 return set(filter(lambda x: filter_fn(x, self._dict[x]), self._dict_keyset))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100167
David Brazdil89bf0f22018-10-30 18:21:24 +0000168 def get_valid_subset_of_unassigned_apis(self, api_subset):
169 """Sanitizes a key set input to only include keys which exist in the dictionary
David Brazdil439d3492018-12-07 11:49:55 +0000170 and have not been assigned any API list flags.
David Brazdil8503b902018-08-30 13:35:03 +0100171
David Brazdil89bf0f22018-10-30 18:21:24 +0000172 Args:
173 entries_subset (set/list): Key set to be sanitized.
David Brazdil8503b902018-08-30 13:35:03 +0100174
David Brazdil89bf0f22018-10-30 18:21:24 +0000175 Returns:
176 Sanitized key set.
177 """
178 assert isinstance(api_subset, set)
David Brazdil439d3492018-12-07 11:49:55 +0000179 return api_subset.intersection(self.filter_apis(HAS_NO_API_LIST_ASSIGNED))
David Brazdil8503b902018-08-30 13:35:03 +0100180
David Brazdil89bf0f22018-10-30 18:21:24 +0000181 def generate_csv(self):
182 """Constructs CSV entries from a dictionary.
David Brazdil8503b902018-08-30 13:35:03 +0100183
David Brazdil89bf0f22018-10-30 18:21:24 +0000184 Returns:
185 List of lines comprising a CSV file. See "parse_and_merge_csv" for format description.
186 """
187 return sorted(map(lambda api: ",".join([api] + sorted(self._dict[api])), self._dict))
David Brazdil8503b902018-08-30 13:35:03 +0100188
David Brazdil89bf0f22018-10-30 18:21:24 +0000189 def parse_and_merge_csv(self, csv_lines, source = "<unknown>"):
190 """Parses CSV entries and merges them into a given dictionary.
David Brazdil8503b902018-08-30 13:35:03 +0100191
David Brazdil89bf0f22018-10-30 18:21:24 +0000192 The expected CSV format is:
193 <api signature>,<flag1>,<flag2>,...,<flagN>
David Brazdil8503b902018-08-30 13:35:03 +0100194
David Brazdil89bf0f22018-10-30 18:21:24 +0000195 Args:
196 csv_lines (list of strings): Lines read from a CSV file.
197 source (string): Origin of `csv_lines`. Will be printed in error messages.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100198
David Brazdil89bf0f22018-10-30 18:21:24 +0000199 Throws:
David Brazdil17d16e82018-12-13 17:00:09 +0000200 AssertionError if parsed flags are invalid.
David Brazdil89bf0f22018-10-30 18:21:24 +0000201 """
202 # Split CSV lines into arrays of values.
203 csv_values = [ line.split(',') for line in csv_lines ]
204
David Brazdil17d16e82018-12-13 17:00:09 +0000205 # Update the full set of API signatures.
206 self._dict_keyset.update([ csv[0] for csv in csv_values ])
David Brazdil89bf0f22018-10-30 18:21:24 +0000207
208 # Check that all flags are known.
209 csv_flags = set(reduce(lambda x, y: set(x).union(y), [ csv[1:] for csv in csv_values ], []))
210 self._check_flags_set(csv_flags, source)
211
212 # Iterate over all CSV lines, find entry in dict and append flags to it.
213 for csv in csv_values:
Andrei Onea80a56602019-03-01 18:49:15 +0000214 flags = csv[1:]
215 if (FLAG_PUBLIC_API in flags) or (FLAG_SYSTEM_API in flags):
216 flags.append(FLAG_WHITELIST)
217 elif FLAG_TEST_API in flags:
218 flags.append(FLAG_GREYLIST)
219 self._dict[csv[0]].update(flags)
David Brazdil89bf0f22018-10-30 18:21:24 +0000220
221 def assign_flag(self, flag, apis, source="<unknown>"):
222 """Assigns a flag to given subset of entries.
223
224 Args:
David Brazdil439d3492018-12-07 11:49:55 +0000225 flag (string): One of ALL_FLAGS.
Andrei Onea80a56602019-03-01 18:49:15 +0000226 apis (set): Subset of APIs to receive the flag.
David Brazdil89bf0f22018-10-30 18:21:24 +0000227 source (string): Origin of `entries_subset`. Will be printed in error messages.
228
229 Throws:
230 AssertionError if parsed API signatures of flags are invalid.
231 """
232 # Check that all APIs exist in the dict.
233 self._check_entries_set(apis, source)
234
235 # Check that the flag is known.
236 self._check_flags_set(set([ flag ]), source)
237
238 # Iterate over the API subset, find each entry in dict and assign the flag to it.
239 for api in apis:
240 self._dict[api].add(flag)
David Brazdil4a55eeb2018-09-11 11:09:01 +0100241
David Brazdil8503b902018-08-30 13:35:03 +0100242def main(argv):
David Brazdil89bf0f22018-10-30 18:21:24 +0000243 # Parse arguments.
244 args = vars(get_args())
David Brazdil8503b902018-08-30 13:35:03 +0100245
David Brazdil17d16e82018-12-13 17:00:09 +0000246 # Initialize API->flags dictionary.
247 flags = FlagsDict()
248
249 # Merge input CSV files into the dictionary.
250 # Do this first because CSV files produced by parsing API stubs will
251 # contain the full set of APIs. Subsequent additions from text files
252 # will be able to detect invalid entries, and/or filter all as-yet
253 # unassigned entries.
254 for filename in args["csv"]:
255 flags.parse_and_merge_csv(read_lines(filename), filename)
David Brazdil8503b902018-08-30 13:35:03 +0100256
David Brazdil89bf0f22018-10-30 18:21:24 +0000257 # Combine inputs which do not require any particular order.
258 # (1) Assign serialization API to whitelist.
259 flags.assign_flag(FLAG_WHITELIST, flags.filter_apis(IS_SERIALIZATION))
David Brazdil8503b902018-08-30 13:35:03 +0100260
David Brazdil17d16e82018-12-13 17:00:09 +0000261 # (2) Merge text files with a known flag into the dictionary.
David Brazdil439d3492018-12-07 11:49:55 +0000262 for flag in ALL_FLAGS:
David Brazdil89bf0f22018-10-30 18:21:24 +0000263 for filename in args[flag]:
264 flags.assign_flag(flag, read_lines(filename), filename)
David Brazdil8503b902018-08-30 13:35:03 +0100265
David Brazdil89bf0f22018-10-30 18:21:24 +0000266 # Merge text files where conflicts should be ignored.
267 # This will only assign the given flag if:
268 # (a) the entry exists, and
269 # (b) it has not been assigned any other flag.
270 # Because of (b), this must run after all strict assignments have been performed.
David Brazdil439d3492018-12-07 11:49:55 +0000271 for flag in ALL_FLAGS:
David Brazdil89bf0f22018-10-30 18:21:24 +0000272 for filename in args[flag + FLAG_IGNORE_CONFLICTS_SUFFIX]:
273 valid_entries = flags.get_valid_subset_of_unassigned_apis(read_lines(filename))
274 flags.assign_flag(flag, valid_entries, filename)
David Brazdil8503b902018-08-30 13:35:03 +0100275
David Brazdil89bf0f22018-10-30 18:21:24 +0000276 # Assign all remaining entries to the blacklist.
David Brazdil439d3492018-12-07 11:49:55 +0000277 flags.assign_flag(FLAG_BLACKLIST, flags.filter_apis(HAS_NO_API_LIST_ASSIGNED))
David Brazdil8503b902018-08-30 13:35:03 +0100278
David Brazdil89bf0f22018-10-30 18:21:24 +0000279 # Write output.
280 write_lines(args["output"], flags.generate_csv())
David Brazdil8503b902018-08-30 13:35:03 +0100281
282if __name__ == "__main__":
283 main(sys.argv)