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 87JAR_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'jar') 88JAVAP_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'javap') 89 90 91def find_api_calls(dump, api_classes, bad_calls): 92 # Given a dump of an implementation class, find calls through API classes. 93 # |dump| is the output of "javap -c" on the implementation class files. 94 # |api_classes| is the list of classes comprising the API. 95 # |bad_calls| is the list of calls through API classes. This list is built up 96 # by this function. 97 98 for i, line in enumerate(dump): 99 try: 100 if CLASS_RE.match(line): 101 caller_class = CLASS_RE.match(line).group(2) 102 if METHOD_RE.match(line): 103 caller_method = METHOD_RE.match(line).group(1) 104 if line.startswith(': invoke', 8) and not line.startswith('dynamic', 16): 105 callee = line.split(' // ')[1].split('Method ')[1].split('\n')[0] 106 callee_class = callee.split('.')[0] 107 assert callee_class 108 if callee_class in api_classes: 109 callee_method = callee.split('.')[1] 110 assert callee_method 111 # Ignore constructor calls for now as every implementation class 112 # that extends an API class will call them. 113 # TODO(pauljensen): Look into enforcing restricting constructor calls. 114 # https://crbug.com/674975 115 if callee_method.startswith('"<init>"'): 116 continue 117 # Ignore VersionSafe calls 118 if 'VersionSafeCallbacks' in caller_class: 119 continue 120 bad_call = '%s/%s -> %s/%s' % (caller_class, caller_method, 121 callee_class, callee_method) 122 if bad_call in ALLOWED_EXCEPTIONS: 123 continue 124 bad_calls += [bad_call] 125 except Exception: 126 sys.stderr.write(f'Failed on line {i+1}: {line}') 127 raise 128 129 130def check_api_calls(opts): 131 # Returns True if no calls through API classes in implementation. 132 133 temp_dir = tempfile.mkdtemp() 134 135 # Extract API class files from jar 136 jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf', 137 os.path.abspath(opts.api_jar)] 138 build_utils.CheckOutput(jar_cmd, cwd=temp_dir) 139 shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) 140 141 # Collect names of API classes 142 api_classes = [] 143 for dirpath, _, filenames in os.walk(temp_dir): 144 if not filenames: 145 continue 146 package = os.path.relpath(dirpath, temp_dir) 147 for filename in filenames: 148 if filename.endswith('.class'): 149 classname = filename[:-len('.class')] 150 api_classes += [os.path.normpath(os.path.join(package, classname))] 151 152 shutil.rmtree(temp_dir) 153 temp_dir = tempfile.mkdtemp() 154 155 # Extract impl class files from jars 156 for impl_jar in opts.impl_jar: 157 jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf', 158 os.path.abspath(impl_jar)] 159 build_utils.CheckOutput(jar_cmd, cwd=temp_dir) 160 shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) 161 162 # Process classes 163 bad_api_calls = [] 164 for dirpath, _, filenames in os.walk(temp_dir): 165 if not filenames: 166 continue 167 # Dump classes 168 dump_file = os.path.join(temp_dir, 'dump.txt') 169 javap_cmd = '%s -c %s > %s' % ( 170 JAVAP_PATH, 171 ' '.join(os.path.join(dirpath, f) for f in filenames).replace('$', 172 '\\$'), 173 dump_file) 174 if os.system(javap_cmd): 175 print('ERROR: javap failed on ' + ' '.join(filenames)) 176 return False 177 # Process class dump 178 with open(dump_file, 'r') as dump: 179 find_api_calls(dump, api_classes, bad_api_calls) 180 181 shutil.rmtree(temp_dir) 182 183 if bad_api_calls: 184 print('ERROR: Found the following calls from implementation classes ' 185 'through') 186 print(' API classes. These could fail if older API is used that') 187 print(' does not contain newer methods. Please call through a') 188 print(' wrapper class from VersionSafeCallbacks.') 189 print('\n'.join(bad_api_calls)) 190 return not bad_api_calls 191 192 193def check_api_version(opts): 194 if update_api.check_up_to_date(opts.api_jar): 195 return True 196 print('ERROR: API file out of date. Please run this command:') 197 print(' components/cronet/tools/update_api.py --api_jar %s' % 198 (os.path.abspath(opts.api_jar))) 199 return False 200 201 202def main(args): 203 parser = argparse.ArgumentParser( 204 description='Enforce Cronet API requirements.') 205 parser.add_argument('--api_jar', 206 help='Path to API jar (i.e. cronet_api.jar)', 207 required=True, 208 metavar='path/to/cronet_api.jar') 209 parser.add_argument('--impl_jar', 210 help='Path to implementation jar ' 211 '(i.e. cronet_impl_native_java.jar)', 212 required=True, 213 metavar='path/to/cronet_impl_native_java.jar', 214 action='append') 215 parser.add_argument('--stamp', help='Path to touch on success.') 216 opts = parser.parse_args(args) 217 218 ret = True 219 ret = check_api_calls(opts) and ret 220 ret = check_api_version(opts) and ret 221 if ret and opts.stamp: 222 build_utils.Touch(opts.stamp) 223 return ret 224 225 226if __name__ == '__main__': 227 sys.exit(0 if main(sys.argv[1:]) else -1) 228