blob: 2a8f69525eb8d47c0c214c94166cbed37d730b6c [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"
David Brazdil89bf0f22018-10-30 18:21:24 +000032
33# List of all known flags.
David Brazdil439d3492018-12-07 11:49:55 +000034FLAGS_API_LIST = [
David Brazdil89bf0f22018-10-30 18:21:24 +000035 FLAG_WHITELIST,
36 FLAG_GREYLIST,
37 FLAG_BLACKLIST,
38 FLAG_GREYLIST_MAX_O,
David Brazdil5cd148f2018-11-01 09:54:25 +000039 FLAG_GREYLIST_MAX_P,
David Brazdil89bf0f22018-10-30 18:21:24 +000040]
David Brazdil439d3492018-12-07 11:49:55 +000041ALL_FLAGS = FLAGS_API_LIST + [ FLAG_CORE_PLATFORM_API ]
42
43FLAGS_API_LIST_SET = set(FLAGS_API_LIST)
44ALL_FLAGS_SET = set(ALL_FLAGS)
David Brazdil89bf0f22018-10-30 18:21:24 +000045
46# Suffix used in command line args to express that only known and
47# otherwise unassigned entries should be assign the given flag.
48# For example, the P dark greylist is checked in as it was in P,
49# but signatures have changes since then. The flag instructs this
50# script to skip any entries which do not exist any more.
51FLAG_IGNORE_CONFLICTS_SUFFIX = "-ignore-conflicts"
52
53# Regex patterns of fields/methods used in serialization. These are
54# considered public API despite being hidden.
55SERIALIZATION_PATTERNS = [
56 r'readObject\(Ljava/io/ObjectInputStream;\)V',
57 r'readObjectNoData\(\)V',
58 r'readResolve\(\)Ljava/lang/Object;',
59 r'serialVersionUID:J',
60 r'serialPersistentFields:\[Ljava/io/ObjectStreamField;',
61 r'writeObject\(Ljava/io/ObjectOutputStream;\)V',
62 r'writeReplace\(\)Ljava/lang/Object;',
63]
64
65# Single regex used to match serialization API. It combines all the
66# SERIALIZATION_PATTERNS into a single regular expression.
67SERIALIZATION_REGEX = re.compile(r'.*->(' + '|'.join(SERIALIZATION_PATTERNS) + r')$')
68
69# Predicates to be used with filter_apis.
David Brazdil439d3492018-12-07 11:49:55 +000070HAS_NO_API_LIST_ASSIGNED = lambda api, flags: not FLAGS_API_LIST_SET.intersection(flags)
David Brazdil89bf0f22018-10-30 18:21:24 +000071IS_SERIALIZATION = lambda api, flags: SERIALIZATION_REGEX.match(api)
72
David Brazdil8503b902018-08-30 13:35:03 +010073def get_args():
74 """Parses command line arguments.
75
76 Returns:
77 Namespace: dictionary of parsed arguments
78 """
79 parser = argparse.ArgumentParser()
David Brazdil89bf0f22018-10-30 18:21:24 +000080 parser.add_argument('--output', required=True)
David Brazdil89bf0f22018-10-30 18:21:24 +000081 parser.add_argument('--csv', nargs='*', default=[], metavar='CSV_FILE',
82 help='CSV files to be merged into output')
83
David Brazdil439d3492018-12-07 11:49:55 +000084 for flag in ALL_FLAGS:
David Brazdil89bf0f22018-10-30 18:21:24 +000085 ignore_conflicts_flag = flag + FLAG_IGNORE_CONFLICTS_SUFFIX
86 parser.add_argument('--' + flag, dest=flag, nargs='*', default=[], metavar='TXT_FILE',
87 help='lists of entries with flag "' + flag + '"')
88 parser.add_argument('--' + ignore_conflicts_flag, dest=ignore_conflicts_flag, nargs='*',
89 default=[], metavar='TXT_FILE',
90 help='lists of entries with flag "' + flag +
91 '". skip entry if missing or flag conflict.')
92
David Brazdil8503b902018-08-30 13:35:03 +010093 return parser.parse_args()
94
95def read_lines(filename):
96 """Reads entire file and return it as a list of lines.
97
David Brazdilae88d4e2018-09-06 14:46:55 +010098 Lines which begin with a hash are ignored.
99
David Brazdil8503b902018-08-30 13:35:03 +0100100 Args:
101 filename (string): Path to the file to read from.
102
103 Returns:
David Brazdil89bf0f22018-10-30 18:21:24 +0000104 Lines of the file as a list of string.
David Brazdil8503b902018-08-30 13:35:03 +0100105 """
106 with open(filename, 'r') as f:
David Brazdil89bf0f22018-10-30 18:21:24 +0000107 lines = f.readlines();
108 lines = filter(lambda line: not line.startswith('#'), lines)
109 lines = map(lambda line: line.strip(), lines)
110 return set(lines)
David Brazdil8503b902018-08-30 13:35:03 +0100111
112def write_lines(filename, lines):
113 """Writes list of lines into a file, overwriting the file it it exists.
114
115 Args:
116 filename (string): Path to the file to be writting into.
117 lines (list): List of strings to write into the file.
118 """
David Brazdil89bf0f22018-10-30 18:21:24 +0000119 lines = map(lambda line: line + '\n', lines)
David Brazdil8503b902018-08-30 13:35:03 +0100120 with open(filename, 'w') as f:
121 f.writelines(lines)
122
David Brazdil89bf0f22018-10-30 18:21:24 +0000123class FlagsDict:
David Brazdil17d16e82018-12-13 17:00:09 +0000124 def __init__(self):
125 self._dict_keyset = set()
126 self._dict = defaultdict(set)
David Brazdil4a55eeb2018-09-11 11:09:01 +0100127
David Brazdil89bf0f22018-10-30 18:21:24 +0000128 def _check_entries_set(self, keys_subset, source):
129 assert isinstance(keys_subset, set)
130 assert keys_subset.issubset(self._dict_keyset), (
131 "Error processing: {}\n"
132 "The following entries were unexpected:\n"
133 "{}"
134 "Please visit go/hiddenapi for more information.").format(
135 source, "".join(map(lambda x: " " + str(x), keys_subset - self._dict_keyset)))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100136
David Brazdil89bf0f22018-10-30 18:21:24 +0000137 def _check_flags_set(self, flags_subset, source):
138 assert isinstance(flags_subset, set)
David Brazdil439d3492018-12-07 11:49:55 +0000139 assert flags_subset.issubset(ALL_FLAGS_SET), (
David Brazdil89bf0f22018-10-30 18:21:24 +0000140 "Error processing: {}\n"
141 "The following flags were not recognized: \n"
142 "{}\n"
143 "Please visit go/hiddenapi for more information.").format(
David Brazdil439d3492018-12-07 11:49:55 +0000144 source, "\n".join(flags_subset - ALL_FLAGS_SET))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100145
David Brazdil89bf0f22018-10-30 18:21:24 +0000146 def filter_apis(self, filter_fn):
147 """Returns APIs which match a given predicate.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100148
David Brazdil89bf0f22018-10-30 18:21:24 +0000149 This is a helper function which allows to filter on both signatures (keys) and
150 flags (values). The built-in filter() invokes the lambda only with dict's keys.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100151
David Brazdil89bf0f22018-10-30 18:21:24 +0000152 Args:
153 filter_fn : Function which takes two arguments (signature/flags) and returns a boolean.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100154
David Brazdil89bf0f22018-10-30 18:21:24 +0000155 Returns:
156 A set of APIs which match the predicate.
157 """
158 return set(filter(lambda x: filter_fn(x, self._dict[x]), self._dict_keyset))
David Brazdil4a55eeb2018-09-11 11:09:01 +0100159
David Brazdil89bf0f22018-10-30 18:21:24 +0000160 def get_valid_subset_of_unassigned_apis(self, api_subset):
161 """Sanitizes a key set input to only include keys which exist in the dictionary
David Brazdil439d3492018-12-07 11:49:55 +0000162 and have not been assigned any API list flags.
David Brazdil8503b902018-08-30 13:35:03 +0100163
David Brazdil89bf0f22018-10-30 18:21:24 +0000164 Args:
165 entries_subset (set/list): Key set to be sanitized.
David Brazdil8503b902018-08-30 13:35:03 +0100166
David Brazdil89bf0f22018-10-30 18:21:24 +0000167 Returns:
168 Sanitized key set.
169 """
170 assert isinstance(api_subset, set)
David Brazdil439d3492018-12-07 11:49:55 +0000171 return api_subset.intersection(self.filter_apis(HAS_NO_API_LIST_ASSIGNED))
David Brazdil8503b902018-08-30 13:35:03 +0100172
David Brazdil89bf0f22018-10-30 18:21:24 +0000173 def generate_csv(self):
174 """Constructs CSV entries from a dictionary.
David Brazdil8503b902018-08-30 13:35:03 +0100175
David Brazdil89bf0f22018-10-30 18:21:24 +0000176 Returns:
177 List of lines comprising a CSV file. See "parse_and_merge_csv" for format description.
178 """
179 return sorted(map(lambda api: ",".join([api] + sorted(self._dict[api])), self._dict))
David Brazdil8503b902018-08-30 13:35:03 +0100180
David Brazdil89bf0f22018-10-30 18:21:24 +0000181 def parse_and_merge_csv(self, csv_lines, source = "<unknown>"):
182 """Parses CSV entries and merges them into a given dictionary.
David Brazdil8503b902018-08-30 13:35:03 +0100183
David Brazdil89bf0f22018-10-30 18:21:24 +0000184 The expected CSV format is:
185 <api signature>,<flag1>,<flag2>,...,<flagN>
David Brazdil8503b902018-08-30 13:35:03 +0100186
David Brazdil89bf0f22018-10-30 18:21:24 +0000187 Args:
188 csv_lines (list of strings): Lines read from a CSV file.
189 source (string): Origin of `csv_lines`. Will be printed in error messages.
David Brazdil4a55eeb2018-09-11 11:09:01 +0100190
David Brazdil89bf0f22018-10-30 18:21:24 +0000191 Throws:
David Brazdil17d16e82018-12-13 17:00:09 +0000192 AssertionError if parsed flags are invalid.
David Brazdil89bf0f22018-10-30 18:21:24 +0000193 """
194 # Split CSV lines into arrays of values.
195 csv_values = [ line.split(',') for line in csv_lines ]
196
David Brazdil17d16e82018-12-13 17:00:09 +0000197 # Update the full set of API signatures.
198 self._dict_keyset.update([ csv[0] for csv in csv_values ])
David Brazdil89bf0f22018-10-30 18:21:24 +0000199
200 # Check that all flags are known.
201 csv_flags = set(reduce(lambda x, y: set(x).union(y), [ csv[1:] for csv in csv_values ], []))
202 self._check_flags_set(csv_flags, source)
203
204 # Iterate over all CSV lines, find entry in dict and append flags to it.
205 for csv in csv_values:
206 self._dict[csv[0]].update(csv[1:])
207
208 def assign_flag(self, flag, apis, source="<unknown>"):
209 """Assigns a flag to given subset of entries.
210
211 Args:
David Brazdil439d3492018-12-07 11:49:55 +0000212 flag (string): One of ALL_FLAGS.
David Brazdil89bf0f22018-10-30 18:21:24 +0000213 apis (set): Subset of APIs to recieve the flag.
214 source (string): Origin of `entries_subset`. Will be printed in error messages.
215
216 Throws:
217 AssertionError if parsed API signatures of flags are invalid.
218 """
219 # Check that all APIs exist in the dict.
220 self._check_entries_set(apis, source)
221
222 # Check that the flag is known.
223 self._check_flags_set(set([ flag ]), source)
224
225 # Iterate over the API subset, find each entry in dict and assign the flag to it.
226 for api in apis:
227 self._dict[api].add(flag)
David Brazdil4a55eeb2018-09-11 11:09:01 +0100228
David Brazdil8503b902018-08-30 13:35:03 +0100229def main(argv):
David Brazdil89bf0f22018-10-30 18:21:24 +0000230 # Parse arguments.
231 args = vars(get_args())
David Brazdil8503b902018-08-30 13:35:03 +0100232
David Brazdil17d16e82018-12-13 17:00:09 +0000233 # Initialize API->flags dictionary.
234 flags = FlagsDict()
235
236 # Merge input CSV files into the dictionary.
237 # Do this first because CSV files produced by parsing API stubs will
238 # contain the full set of APIs. Subsequent additions from text files
239 # will be able to detect invalid entries, and/or filter all as-yet
240 # unassigned entries.
241 for filename in args["csv"]:
242 flags.parse_and_merge_csv(read_lines(filename), filename)
David Brazdil8503b902018-08-30 13:35:03 +0100243
David Brazdil89bf0f22018-10-30 18:21:24 +0000244 # Combine inputs which do not require any particular order.
245 # (1) Assign serialization API to whitelist.
246 flags.assign_flag(FLAG_WHITELIST, flags.filter_apis(IS_SERIALIZATION))
David Brazdil8503b902018-08-30 13:35:03 +0100247
David Brazdil17d16e82018-12-13 17:00:09 +0000248 # (2) Merge text files with a known flag into the dictionary.
David Brazdil439d3492018-12-07 11:49:55 +0000249 for flag in ALL_FLAGS:
David Brazdil89bf0f22018-10-30 18:21:24 +0000250 for filename in args[flag]:
251 flags.assign_flag(flag, read_lines(filename), filename)
David Brazdil8503b902018-08-30 13:35:03 +0100252
David Brazdil89bf0f22018-10-30 18:21:24 +0000253 # Merge text files where conflicts should be ignored.
254 # This will only assign the given flag if:
255 # (a) the entry exists, and
256 # (b) it has not been assigned any other flag.
257 # Because of (b), this must run after all strict assignments have been performed.
David Brazdil439d3492018-12-07 11:49:55 +0000258 for flag in ALL_FLAGS:
David Brazdil89bf0f22018-10-30 18:21:24 +0000259 for filename in args[flag + FLAG_IGNORE_CONFLICTS_SUFFIX]:
260 valid_entries = flags.get_valid_subset_of_unassigned_apis(read_lines(filename))
261 flags.assign_flag(flag, valid_entries, filename)
David Brazdil8503b902018-08-30 13:35:03 +0100262
David Brazdil89bf0f22018-10-30 18:21:24 +0000263 # Assign all remaining entries to the blacklist.
David Brazdil439d3492018-12-07 11:49:55 +0000264 flags.assign_flag(FLAG_BLACKLIST, flags.filter_apis(HAS_NO_API_LIST_ASSIGNED))
David Brazdil8503b902018-08-30 13:35:03 +0100265
David Brazdil89bf0f22018-10-30 18:21:24 +0000266 # Write output.
267 write_lines(args["output"], flags.generate_csv())
David Brazdil8503b902018-08-30 13:35:03 +0100268
269if __name__ == "__main__":
270 main(sys.argv)