blob: 2f1e53ca50655574b7c55debd4ecb282c9072d0b [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
20import os
21import sys
22import re
23
David Brazdil89bf0f22018-10-30 18:21:24 +000024# Names of flags recognized by the `hiddenapi` tool.
25FLAG_WHITELIST = "whitelist"
26FLAG_GREYLIST = "greylist"
27FLAG_BLACKLIST = "blacklist"
28FLAG_GREYLIST_MAX_O = "greylist-max-o"
29
30# List of all known flags.
31FLAGS = [
32 FLAG_WHITELIST,
33 FLAG_GREYLIST,
34 FLAG_BLACKLIST,
35 FLAG_GREYLIST_MAX_O,
36]
37FLAGS_SET = set(FLAGS)
38
39# Suffix used in command line args to express that only known and
40# otherwise unassigned entries should be assign the given flag.
41# For example, the P dark greylist is checked in as it was in P,
42# but signatures have changes since then. The flag instructs this
43# script to skip any entries which do not exist any more.
44FLAG_IGNORE_CONFLICTS_SUFFIX = "-ignore-conflicts"
45
46# Regex patterns of fields/methods used in serialization. These are
47# considered public API despite being hidden.
48SERIALIZATION_PATTERNS = [
49 r'readObject\(Ljava/io/ObjectInputStream;\)V',
50 r'readObjectNoData\(\)V',
51 r'readResolve\(\)Ljava/lang/Object;',
52 r'serialVersionUID:J',
53 r'serialPersistentFields:\[Ljava/io/ObjectStreamField;',
54 r'writeObject\(Ljava/io/ObjectOutputStream;\)V',
55 r'writeReplace\(\)Ljava/lang/Object;',
56]
57
58# Single regex used to match serialization API. It combines all the
59# SERIALIZATION_PATTERNS into a single regular expression.
60SERIALIZATION_REGEX = re.compile(r'.*->(' + '|'.join(SERIALIZATION_PATTERNS) + r')$')
61
62# Predicates to be used with filter_apis.
63IS_UNASSIGNED = lambda api, flags: not flags
64IS_SERIALIZATION = lambda api, flags: SERIALIZATION_REGEX.match(api)
65
David Brazdil8503b902018-08-30 13:35:03 +010066def get_args():
67 """Parses command line arguments.
68
69 Returns:
70 Namespace: dictionary of parsed arguments
71 """
72 parser = argparse.ArgumentParser()
David Brazdil89bf0f22018-10-30 18:21:24 +000073 parser.add_argument('--output', required=True)
74 parser.add_argument('--public', required=True, help='list of all public entries')
75 parser.add_argument('--private', required=True, help='list of all private entries')
76 parser.add_argument('--csv', nargs='*', default=[], metavar='CSV_FILE',
77 help='CSV files to be merged into output')
78
79 for flag in FLAGS:
80 ignore_conflicts_flag = flag + FLAG_IGNORE_CONFLICTS_SUFFIX
81 parser.add_argument('--' + flag, dest=flag, nargs='*', default=[], metavar='TXT_FILE',
82 help='lists of entries with flag "' + flag + '"')
83 parser.add_argument('--' + ignore_conflicts_flag, dest=ignore_conflicts_flag, nargs='*',
84 default=[], metavar='TXT_FILE',
85 help='lists of entries with flag "' + flag +
86 '". skip entry if missing or flag conflict.')
87
David Brazdil8503b902018-08-30 13:35:03 +010088 return parser.parse_args()
89
90def read_lines(filename):
91 """Reads entire file and return it as a list of lines.
92
David Brazdilae88d4e2018-09-06 14:46:55 +010093 Lines which begin with a hash are ignored.
94
David Brazdil8503b902018-08-30 13:35:03 +010095 Args:
96 filename (string): Path to the file to read from.
97
98 Returns:
David Brazdil89bf0f22018-10-30 18:21:24 +000099 Lines of the file as a list of string.
David Brazdil8503b902018-08-30 13:35:03 +0100100 """
101 with open(filename, 'r') as f:
David Brazdil89bf0f22018-10-30 18:21:24 +0000102 lines = f.readlines();
103 lines = filter(lambda line: not line.startswith('#'), lines)
104 lines = map(lambda line: line.strip(), lines)
105 return set(lines)
David Brazdil8503b902018-08-30 13:35:03 +0100106
107def write_lines(filename, lines):
108 """Writes list of lines into a file, overwriting the file it it exists.
109
110 Args:
111 filename (string): Path to the file to be writting into.
112 lines (list): List of strings to write into the file.
113 """
David Brazdil89bf0f22018-10-30 18:21:24 +0000114 lines = map(lambda line: line + '\n', lines)
David Brazdil8503b902018-08-30 13:35:03 +0100115 with open(filename, 'w') as f:
116 f.writelines(lines)
117
David Brazdil89bf0f22018-10-30 18:21:24 +0000118class FlagsDict:
119 def __init__(self, public_api, private_api):
120 # Bootstrap the entries dictionary.
David Brazdil8503b902018-08-30 13:35:03 +0100121
David Brazdil89bf0f22018-10-30 18:21:24 +0000122 # Check that the two sets do not overlap.
123 public_api_set = set(public_api)
124 private_api_set = set(private_api)
125 assert public_api_set.isdisjoint(private_api_set), (
126 "Lists of public and private API overlap. " +
127 "This suggests an issue with the `hiddenapi` build tool.")
David Brazdil8503b902018-08-30 13:35:03 +0100128
David Brazdil89bf0f22018-10-30 18:21:24 +0000129 # Compute the whole key set
130 self._dict_keyset = public_api_set.union(private_api_set)
David Brazdil4a55eeb2018-09-11 11:09:01 +0100131
David Brazdil89bf0f22018-10-30 18:21:24 +0000132 # Create a dict that creates entries for both public and private API,
133 # and assigns public API to the whitelist.
134 self._dict = {}
135 for api in public_api:
136 self._dict[api] = set([ FLAG_WHITELIST ])
137 for api in private_api:
138 self._dict[api] = set()
David Brazdil4a55eeb2018-09-11 11:09:01 +0100139
David Brazdil89bf0f22018-10-30 18:21:24 +0000140 def _check_entries_set(self, keys_subset, source):
141 assert isinstance(keys_subset, set)
142 assert keys_subset.issubset(self._dict_keyset), (
143 "Error processing: {}\n"
144 "The following entries were unexpected:\n"
145 "{}"
146 "Please visit go/hiddenapi for more information.").format(
147 source, "".join(map(lambda x: " " + str(x), keys_subset - self._dict_keyset)))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100148
David Brazdil89bf0f22018-10-30 18:21:24 +0000149 def _check_flags_set(self, flags_subset, source):
150 assert isinstance(flags_subset, set)
151 assert flags_subset.issubset(FLAGS_SET), (
152 "Error processing: {}\n"
153 "The following flags were not recognized: \n"
154 "{}\n"
155 "Please visit go/hiddenapi for more information.").format(
156 source, "\n".join(flags_subset - FLAGS_SET))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100157
David Brazdil89bf0f22018-10-30 18:21:24 +0000158 def filter_apis(self, filter_fn):
159 """Returns APIs which match a given predicate.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100160
David Brazdil89bf0f22018-10-30 18:21:24 +0000161 This is a helper function which allows to filter on both signatures (keys) and
162 flags (values). The built-in filter() invokes the lambda only with dict's keys.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100163
David Brazdil89bf0f22018-10-30 18:21:24 +0000164 Args:
165 filter_fn : Function which takes two arguments (signature/flags) and returns a boolean.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100166
David Brazdil89bf0f22018-10-30 18:21:24 +0000167 Returns:
168 A set of APIs which match the predicate.
169 """
170 return set(filter(lambda x: filter_fn(x, self._dict[x]), self._dict_keyset))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100171
David Brazdil89bf0f22018-10-30 18:21:24 +0000172 def get_valid_subset_of_unassigned_apis(self, api_subset):
173 """Sanitizes a key set input to only include keys which exist in the dictionary
174 and have not been assigned any flags.
David Brazdil8503b902018-08-30 13:35:03 +0100175
David Brazdil89bf0f22018-10-30 18:21:24 +0000176 Args:
177 entries_subset (set/list): Key set to be sanitized.
David Brazdil8503b902018-08-30 13:35:03 +0100178
David Brazdil89bf0f22018-10-30 18:21:24 +0000179 Returns:
180 Sanitized key set.
181 """
182 assert isinstance(api_subset, set)
183 return api_subset.intersection(self.filter_apis(IS_UNASSIGNED))
David Brazdil8503b902018-08-30 13:35:03 +0100184
David Brazdil89bf0f22018-10-30 18:21:24 +0000185 def generate_csv(self):
186 """Constructs CSV entries from a dictionary.
David Brazdil8503b902018-08-30 13:35:03 +0100187
David Brazdil89bf0f22018-10-30 18:21:24 +0000188 Returns:
189 List of lines comprising a CSV file. See "parse_and_merge_csv" for format description.
190 """
191 return sorted(map(lambda api: ",".join([api] + sorted(self._dict[api])), self._dict))
David Brazdil8503b902018-08-30 13:35:03 +0100192
David Brazdil89bf0f22018-10-30 18:21:24 +0000193 def parse_and_merge_csv(self, csv_lines, source = "<unknown>"):
194 """Parses CSV entries and merges them into a given dictionary.
David Brazdil8503b902018-08-30 13:35:03 +0100195
David Brazdil89bf0f22018-10-30 18:21:24 +0000196 The expected CSV format is:
197 <api signature>,<flag1>,<flag2>,...,<flagN>
David Brazdil8503b902018-08-30 13:35:03 +0100198
David Brazdil89bf0f22018-10-30 18:21:24 +0000199 Args:
200 csv_lines (list of strings): Lines read from a CSV file.
201 source (string): Origin of `csv_lines`. Will be printed in error messages.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100202
David Brazdil89bf0f22018-10-30 18:21:24 +0000203 Throws:
204 AssertionError if parsed API signatures of flags are invalid.
205 """
206 # Split CSV lines into arrays of values.
207 csv_values = [ line.split(',') for line in csv_lines ]
208
209 # Check that all entries exist in the dict.
210 csv_keys = set([ csv[0] for csv in csv_values ])
211 self._check_entries_set(csv_keys, source)
212
213 # Check that all flags are known.
214 csv_flags = set(reduce(lambda x, y: set(x).union(y), [ csv[1:] for csv in csv_values ], []))
215 self._check_flags_set(csv_flags, source)
216
217 # Iterate over all CSV lines, find entry in dict and append flags to it.
218 for csv in csv_values:
219 self._dict[csv[0]].update(csv[1:])
220
221 def assign_flag(self, flag, apis, source="<unknown>"):
222 """Assigns a flag to given subset of entries.
223
224 Args:
225 flag (string): One of FLAGS.
226 apis (set): Subset of APIs to recieve the flag.
227 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 Brazdil89bf0f22018-10-30 18:21:24 +0000246 flags = FlagsDict(read_lines(args["public"]), read_lines(args["private"]))
David Brazdil8503b902018-08-30 13:35:03 +0100247
David Brazdil89bf0f22018-10-30 18:21:24 +0000248 # Combine inputs which do not require any particular order.
249 # (1) Assign serialization API to whitelist.
250 flags.assign_flag(FLAG_WHITELIST, flags.filter_apis(IS_SERIALIZATION))
David Brazdil8503b902018-08-30 13:35:03 +0100251
David Brazdil89bf0f22018-10-30 18:21:24 +0000252 # (2) Merge input CSV files into the dictionary.
253 for filename in args["csv"]:
254 flags.parse_and_merge_csv(read_lines(filename), filename)
David Brazdil8503b902018-08-30 13:35:03 +0100255
David Brazdil89bf0f22018-10-30 18:21:24 +0000256 # (3) Merge text files with a known flag into the dictionary.
257 for flag in FLAGS:
258 for filename in args[flag]:
259 flags.assign_flag(flag, read_lines(filename), filename)
David Brazdil8503b902018-08-30 13:35:03 +0100260
David Brazdil89bf0f22018-10-30 18:21:24 +0000261 # Merge text files where conflicts should be ignored.
262 # This will only assign the given flag if:
263 # (a) the entry exists, and
264 # (b) it has not been assigned any other flag.
265 # Because of (b), this must run after all strict assignments have been performed.
266 for flag in FLAGS:
267 for filename in args[flag + FLAG_IGNORE_CONFLICTS_SUFFIX]:
268 valid_entries = flags.get_valid_subset_of_unassigned_apis(read_lines(filename))
269 flags.assign_flag(flag, valid_entries, filename)
David Brazdil8503b902018-08-30 13:35:03 +0100270
David Brazdil89bf0f22018-10-30 18:21:24 +0000271 # Assign all remaining entries to the blacklist.
272 flags.assign_flag(FLAG_BLACKLIST, flags.filter_apis(IS_UNASSIGNED))
David Brazdil8503b902018-08-30 13:35:03 +0100273
David Brazdil89bf0f22018-10-30 18:21:24 +0000274 # Write output.
275 write_lines(args["output"], flags.generate_csv())
David Brazdil8503b902018-08-30 13:35:03 +0100276
277if __name__ == "__main__":
278 main(sys.argv)