1#!/usr/bin/env python 2# Copyright 2016 The Chromium Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""api_static_checks.py - Enforce Cronet API requirements.""" 7 8 9 10import argparse 11import os 12import re 13import shutil 14import sys 15import tempfile 16 17REPOSITORY_ROOT = os.path.abspath( 18 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) 19 20sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp')) 21from util import build_utils # pylint: disable=wrong-import-position 22 23sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'components')) 24from cronet.tools import update_api # pylint: disable=wrong-import-position 25 26 27# These regular expressions catch the beginning of lines that declare classes 28# and methods. The first group returned by a match is the class or method name. 29from cronet.tools.update_api import CLASS_RE # pylint: disable=wrong-import-position 30METHOD_RE = re.compile(r'.* ([^ ]*)\(.*\);') 31 32# Allowed exceptions. Adding anything to this list is dangerous and should be 33# avoided if possible. For now these exceptions are for APIs that existed in 34# the first version of Cronet and will be supported forever. 35# TODO(pauljensen): Remove these. 36ALLOWED_EXCEPTIONS = [ 37 'org.chromium.net.impl.CronetEngineBuilderImpl/build ->' 38 ' org/chromium/net/ExperimentalCronetEngine/getVersionString:' 39 '()Ljava/lang/String;', 40 'org.chromium.net.urlconnection.CronetFixedModeOutputStream$UploadDataProviderI' 41 'mpl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', 42 'org.chromium.net.urlconnection.CronetFixedModeOutputStream$UploadDataProviderI' 43 'mpl/rewind -> org/chromium/net/UploadDataSink/onRewindError:' 44 '(Ljava/lang/Exception;)V', 45 'org.chromium.net.urlconnection.CronetHttpURLConnection/disconnect ->' 46 ' org/chromium/net/UrlRequest/cancel:()V', 47 'org.chromium.net.urlconnection.CronetHttpURLConnection/disconnect ->' 48 ' org/chromium/net/UrlResponseInfo/getHttpStatusText:()Ljava/lang/String;', 49 'org.chromium.net.urlconnection.CronetHttpURLConnection/disconnect ->' 50 ' org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', 51 'org.chromium.net.urlconnection.CronetHttpURLConnection/getHeaderField ->' 52 ' org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', 53 'org.chromium.net.urlconnection.CronetHttpURLConnection/getErrorStream ->' 54 ' org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', 55 'org.chromium.net.urlconnection.CronetHttpURLConnection/setConnectTimeout ->' 56 ' org/chromium/net/UrlRequest/read:(Ljava/nio/ByteBuffer;)V', 57 'org.chromium.net.urlconnection.CronetHttpURLConnection$CronetUrlRequestCallbac' 58 'k/onRedirectReceived -> org/chromium/net/UrlRequest/followRedirect:()V', 59 'org.chromium.net.urlconnection.CronetHttpURLConnection$CronetUrlRequestCallbac' 60 'k/onRedirectReceived -> org/chromium/net/UrlRequest/cancel:()V', 61 'org.chromium.net.urlconnection.CronetChunkedOutputStream$UploadDataProviderImp' 62 'l/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', 63 'org.chromium.net.urlconnection.CronetChunkedOutputStream$UploadDataProviderImp' 64 'l/rewind -> org/chromium/net/UploadDataSink/onRewindError:' 65 '(Ljava/lang/Exception;)V', 66 'org.chromium.net.urlconnection.CronetBufferedOutputStream$UploadDataProviderIm' 67 'pl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', 68 'org.chromium.net.urlconnection.CronetBufferedOutputStream$UploadDataProviderIm' 69 'pl/rewind -> org/chromium/net/UploadDataSink/onRewindSucceeded:()V', 70 'org.chromium.net.urlconnection.CronetHttpURLStreamHandler/org.chromium.net.url' 71 'connection.CronetHttpURLStreamHandler -> org/chromium/net/ExperimentalCron' 72 'etEngine/openConnection:(Ljava/net/URL;)Ljava/net/URLConnection;', 73 'org.chromium.net.urlconnection.CronetHttpURLStreamHandler/org.chromium.net.url' 74 'connection.CronetHttpURLStreamHandler -> org/chromium/net/ExperimentalCron' 75 'etEngine/openConnection:(Ljava/net/URL;Ljava/net/Proxy;)Ljava/net/URLConne' 76 'ction;', 77 'org.chromium.net.impl.CronetEngineBase/newBidirectionalStreamBuilder -> org/ch' 78 'romium/net/ExperimentalCronetEngine/newBidirectionalStreamBuilder:(Ljava/l' 79 'ang/String;Lorg/chromium/net/BidirectionalStream$Callback;Ljava/util/concu' 80 'rrent/Executor;)Lorg/chromium/net/ExperimentalBidirectionalStream$' 81 'Builder;', 82 # getMessage() is an java.lang.Exception member, and so cannot be removed. 83 'org.chromium.net.impl.NetworkExceptionImpl/getMessage -> ' 84 'org/chromium/net/NetworkException/getMessage:()Ljava/lang/String;', 85] 86 87# Filename of file containing the interface API version number. 88INTERFACE_API_VERSION_FILENAME = os.path.abspath(os.path.join( 89 os.path.dirname(__file__), '..', 'android', 'interface_api_version.txt')) 90# Filename of file containing the implementation API version number. 91IMPLEMENTATION_API_VERSION_FILENAME = os.path.abspath(os.path.join( 92 os.path.dirname(__file__), '..', 'android', 93 'implementation_api_version.txt')) 94JAR_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'jar') 95JAVAP_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'javap') 96 97 98def find_api_calls(dump, api_classes, bad_calls): 99 # Given a dump of an implementation class, find calls through API classes. 100 # |dump| is the output of "javap -c" on the implementation class files. 101 # |api_classes| is the list of classes comprising the API. 102 # |bad_calls| is the list of calls through API classes. This list is built up 103 # by this function. 104 105 for i, line in enumerate(dump): 106 try: 107 if CLASS_RE.match(line): 108 caller_class = CLASS_RE.match(line).group(2) 109 if METHOD_RE.match(line): 110 caller_method = METHOD_RE.match(line).group(1) 111 if line.startswith(': invoke', 8) and not line.startswith('dynamic', 16): 112 callee = line.split(' // ')[1].split('Method ')[1].split('\n')[0] 113 callee_class = callee.split('.')[0] 114 assert callee_class 115 if callee_class in api_classes: 116 callee_method = callee.split('.')[1] 117 assert callee_method 118 # Ignore constructor calls for now as every implementation class 119 # that extends an API class will call them. 120 # TODO(pauljensen): Look into enforcing restricting constructor calls. 121 # https://crbug.com/674975 122 if callee_method.startswith('"<init>"'): 123 continue 124 # Ignore VersionSafe calls 125 if 'VersionSafeCallbacks' in caller_class: 126 continue 127 bad_call = '%s/%s -> %s/%s' % (caller_class, caller_method, 128 callee_class, callee_method) 129 if bad_call in ALLOWED_EXCEPTIONS: 130 continue 131 bad_calls += [bad_call] 132 except Exception: 133 sys.stderr.write(f'Failed on line {i+1}: {line}') 134 raise 135 136 137def check_api_calls(opts): 138 # Returns True if no calls through API classes in implementation. 139 140 temp_dir = tempfile.mkdtemp() 141 142 # Extract API class files from jar 143 jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf', 144 os.path.abspath(opts.api_jar)] 145 build_utils.CheckOutput(jar_cmd, cwd=temp_dir) 146 shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) 147 148 # Collect names of API classes 149 api_classes = [] 150 for dirpath, _, filenames in os.walk(temp_dir): 151 if not filenames: 152 continue 153 package = os.path.relpath(dirpath, temp_dir) 154 for filename in filenames: 155 if filename.endswith('.class'): 156 classname = filename[:-len('.class')] 157 api_classes += [os.path.normpath(os.path.join(package, classname))] 158 159 shutil.rmtree(temp_dir) 160 temp_dir = tempfile.mkdtemp() 161 162 # Extract impl class files from jars 163 for impl_jar in opts.impl_jar: 164 jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf', 165 os.path.abspath(impl_jar)] 166 build_utils.CheckOutput(jar_cmd, cwd=temp_dir) 167 shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) 168 169 # Process classes 170 bad_api_calls = [] 171 for dirpath, _, filenames in os.walk(temp_dir): 172 if not filenames: 173 continue 174 # Dump classes 175 dump_file = os.path.join(temp_dir, 'dump.txt') 176 javap_cmd = '%s -c %s > %s' % ( 177 JAVAP_PATH, 178 ' '.join(os.path.join(dirpath, f) for f in filenames).replace('$', 179 '\\$'), 180 dump_file) 181 if os.system(javap_cmd): 182 print('ERROR: javap failed on ' + ' '.join(filenames)) 183 return False 184 # Process class dump 185 with open(dump_file, 'r') as dump: 186 find_api_calls(dump, api_classes, bad_api_calls) 187 188 shutil.rmtree(temp_dir) 189 190 if bad_api_calls: 191 print('ERROR: Found the following calls from implementation classes ' 192 'through') 193 print(' API classes. These could fail if older API is used that') 194 print(' does not contain newer methods. Please call through a') 195 print(' wrapper class from VersionSafeCallbacks.') 196 print('\n'.join(bad_api_calls)) 197 return not bad_api_calls 198 199 200def check_api_version(opts): 201 if not update_api.check_up_to_date(opts.api_jar): 202 print('ERROR: API file out of date. Please run this command:') 203 print(' components/cronet/tools/update_api.py --api_jar %s' % ( 204 os.path.abspath(opts.api_jar))) 205 return False 206 interface_api_version = None 207 implementation_api_version = None 208 with open(INTERFACE_API_VERSION_FILENAME, 'r') \ 209 as interface_api_version_file: 210 interface_api_version = int(interface_api_version_file.read()) 211 with open(IMPLEMENTATION_API_VERSION_FILENAME, 'r') \ 212 as implementation_api_version_file: 213 implementation_api_version = int(implementation_api_version_file.read()) 214 if interface_api_version > implementation_api_version: 215 print('ERROR: Interface API version cannot be higher than the current ' 216 'implementation API version.') 217 return False 218 if implementation_api_version not in \ 219 (interface_api_version + 1, interface_api_version): 220 print('ERROR: Implementation API version can be preemptively bumped up ' 221 'at most once. Land the interface part of the API which is already ' 222 'being released before adding a new one.') 223 return False 224 return True 225 226 227def main(args): 228 parser = argparse.ArgumentParser( 229 description='Enforce Cronet API requirements.') 230 parser.add_argument('--api_jar', 231 help='Path to API jar (i.e. cronet_api.jar)', 232 required=True, 233 metavar='path/to/cronet_api.jar') 234 parser.add_argument('--impl_jar', 235 help='Path to implementation jar ' 236 '(i.e. cronet_impl_native_java.jar)', 237 required=True, 238 metavar='path/to/cronet_impl_native_java.jar', 239 action='append') 240 parser.add_argument('--stamp', help='Path to touch on success.') 241 opts = parser.parse_args(args) 242 243 ret = True 244 ret = check_api_calls(opts) and ret 245 ret = check_api_version(opts) and ret 246 if ret and opts.stamp: 247 build_utils.Touch(opts.stamp) 248 return ret 249 250 251if __name__ == '__main__': 252 sys.exit(0 if main(sys.argv[1:]) else -1) 253