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 9import argparse 10import os 11import re 12import shutil 13import sys 14import tempfile 15 16REPOSITORY_ROOT = os.path.abspath( 17 os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) 18 19sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp')) 20from util import build_utils # pylint: disable=wrong-import-position 21 22sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'components')) 23from cronet.tools import update_api # pylint: disable=wrong-import-position 24 25 26# These regular expressions catch the beginning of lines that declare classes 27# and methods. The first group returned by a match is the class or method name. 28from cronet.tools.update_api import CLASS_RE # pylint: disable=wrong-import-position 29 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.urlconnection.CronetHttpURLConnection/disconnect -> org/chromium/net/UrlRequest/cancel:()V', 38 'org.chromium.net.urlconnection.CronetHttpURLConnection/getResponseMessage -> org/chromium/net/UrlResponseInfo/getHttpStatusText:()Ljava/lang/String;', 39 'org.chromium.net.urlconnection.CronetHttpURLConnection/getResponseCode -> org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', 40 'org.chromium.net.urlconnection.CronetHttpURLConnection/getInputStream -> org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', 41 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/CronetEngine/newUrlRequestBuilder:(Ljava/lang/String;Lorg/chromium/net/UrlRequest$Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest$Builder;', 42 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/setUploadDataProvider:(Lorg/chromium/net/UploadDataProvider;Ljava/util/concurrent/Executor;)Lorg/chromium/net/ExperimentalUrlRequest$Builder;', 43 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/UploadDataProvider/getLength:()J', 44 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/addHeader:(Ljava/lang/String;Ljava/lang/String;)Lorg/chromium/net/ExperimentalUrlRequest$Builder;', 45 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/disableCache:()Lorg/chromium/net/ExperimentalUrlRequest$Builder;', 46 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/setHttpMethod:(Ljava/lang/String;)Lorg/chromium/net/ExperimentalUrlRequest$Builder;', 47 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/setTrafficStatsTag:(I)Lorg/chromium/net/ExperimentalUrlRequest$Builder;', 48 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/setTrafficStatsUid:(I)Lorg/chromium/net/ExperimentalUrlRequest$Builder;', 49 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/build:()Lorg/chromium/net/ExperimentalUrlRequest;', 50 'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/UrlRequest/start:()V', 51 'org.chromium.net.urlconnection.CronetHttpURLConnection/getErrorStream -> org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I', 52 'org.chromium.net.urlconnection.CronetHttpURLConnection/getMoreData -> org/chromium/net/UrlRequest/read:(Ljava/nio/ByteBuffer;)V', 53 'org.chromium.net.urlconnection.CronetHttpURLConnection/getAllHeadersAsList -> org/chromium/net/UrlResponseInfo/getAllHeadersAsList:()Ljava/util/List;', 54 'org.chromium.net.urlconnection.CronetChunkedOutputStream$UploadDataProviderImpl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', 55 'org.chromium.net.urlconnection.CronetChunkedOutputStream$UploadDataProviderImpl/rewind -> org/chromium/net/UploadDataSink/onRewindError:(Ljava/lang/Exception;)V', 56 'org.chromium.net.urlconnection.CronetFixedModeOutputStream$UploadDataProviderImpl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', 57 'org.chromium.net.urlconnection.CronetFixedModeOutputStream$UploadDataProviderImpl/rewind -> org/chromium/net/UploadDataSink/onRewindError:(Ljava/lang/Exception;)V', 58 'org.chromium.net.urlconnection.CronetHttpURLConnection$CronetUrlRequestCallback/onRedirectReceived -> org/chromium/net/UrlRequest/followRedirect:()V', 59 'org.chromium.net.urlconnection.CronetHttpURLConnection$CronetUrlRequestCallback/onRedirectReceived -> org/chromium/net/UrlRequest/cancel:()V', 60 'org.chromium.net.urlconnection.CronetHttpURLStreamHandler/openConnection -> org/chromium/net/ExperimentalCronetEngine/openConnection:(Ljava/net/URL;)Ljava/net/URLConnection;', 61 'org.chromium.net.urlconnection.CronetHttpURLStreamHandler/openConnection -> org/chromium/net/ExperimentalCronetEngine/openConnection:(Ljava/net/URL;Ljava/net/Proxy;)Ljava/net/URLConnection;', 62 'org.chromium.net.urlconnection.CronetBufferedOutputStream$UploadDataProviderImpl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V', 63 'org.chromium.net.urlconnection.CronetBufferedOutputStream$UploadDataProviderImpl/rewind -> org/chromium/net/UploadDataSink/onRewindSucceeded:()V', 64 'org.chromium.net.impl.CronetEngineBase/newBidirectionalStreamBuilder -> org/chromium/net/ExperimentalCronetEngine/newBidirectionalStreamBuilder:(Ljava/lang/String;Lorg/chromium/net/BidirectionalStream$Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/ExperimentalBidirectionalStream$Builder;', 65 'org.chromium.net.impl.NetworkExceptionImpl/getMessage -> org/chromium/net/NetworkException/getMessage:()Ljava/lang/String;', 66] 67 68JAR_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'jar') 69JAVAP_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'javap') 70 71 72def find_api_calls(dump, api_classes, bad_calls): 73 # Given a dump of an implementation class, find calls through API classes. 74 # |dump| is the output of "javap -c" on the implementation class files. 75 # |api_classes| is the list of classes comprising the API. 76 # |bad_calls| is the list of calls through API classes. This list is built up 77 # by this function. 78 79 for i, line in enumerate(dump): 80 try: 81 if CLASS_RE.match(line): 82 caller_class = CLASS_RE.match(line).group(2) 83 if METHOD_RE.match(line): 84 caller_method = METHOD_RE.match(line).group(1) 85 if line.startswith(': invoke', 8) and not line.startswith('dynamic', 16): 86 callee = line.split(' // ')[1].split('Method ')[1].split('\n')[0] 87 callee_class = callee.split('.')[0] 88 assert callee_class 89 if callee_class in api_classes: 90 callee_method = callee.split('.')[1] 91 assert callee_method 92 # Ignore constructor calls for now as every implementation class 93 # that extends an API class will call them. 94 # TODO(pauljensen): Look into enforcing restricting constructor calls. 95 # https://crbug.com/674975 96 if callee_method.startswith('"<init>"'): 97 continue 98 # Ignore VersionSafe calls 99 if 'VersionSafeCallbacks' in caller_class: 100 continue 101 bad_call = '%s/%s -> %s/%s' % (caller_class, caller_method, 102 callee_class, callee_method) 103 if bad_call in ALLOWED_EXCEPTIONS: 104 continue 105 bad_calls += [bad_call] 106 except Exception: 107 sys.stderr.write(f'Failed on line {i+1}: {line}') 108 raise 109 110 111def check_api_calls(opts): 112 # Returns True if no calls through API classes in implementation. 113 114 temp_dir = tempfile.mkdtemp() 115 116 # Extract API class files from jar 117 jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf', 118 os.path.abspath(opts.api_jar)] 119 build_utils.CheckOutput(jar_cmd, cwd=temp_dir) 120 shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) 121 122 # Collect names of API classes 123 api_classes = [] 124 for dirpath, _, filenames in os.walk(temp_dir): 125 if not filenames: 126 continue 127 package = os.path.relpath(dirpath, temp_dir) 128 for filename in filenames: 129 if filename.endswith('.class'): 130 classname = filename[:-len('.class')] 131 api_classes += [os.path.normpath(os.path.join(package, classname))] 132 133 shutil.rmtree(temp_dir) 134 temp_dir = tempfile.mkdtemp() 135 136 # Extract impl class files from jars 137 for impl_jar in opts.impl_jar: 138 jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf', 139 os.path.abspath(impl_jar)] 140 build_utils.CheckOutput(jar_cmd, cwd=temp_dir) 141 shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True) 142 143 # Process classes 144 bad_api_calls = [] 145 for dirpath, _, filenames in os.walk(temp_dir): 146 if not filenames: 147 continue 148 # Dump classes 149 dump_file = os.path.join(temp_dir, 'dump.txt') 150 javap_cmd = '%s -private -c %s > %s' % (JAVAP_PATH, ' '.join( 151 os.path.join(dirpath, f) 152 for f in filenames).replace('$', '\\$'), dump_file) 153 if os.system(javap_cmd): 154 print('ERROR: javap failed on ' + ' '.join(filenames)) 155 return False 156 # Process class dump 157 with open(dump_file, 'r') as dump: 158 find_api_calls(dump, api_classes, bad_api_calls) 159 160 shutil.rmtree(temp_dir) 161 162 if bad_api_calls: 163 print('ERROR: Found the following calls from implementation classes ' 164 'through') 165 print(' API classes. These could fail if older API is used that') 166 print(' does not contain newer methods. Please call through a') 167 print(' wrapper class from VersionSafeCallbacks.') 168 print('\n'.join(bad_api_calls)) 169 return not bad_api_calls 170 171 172def check_api_version(opts): 173 if update_api.check_up_to_date(opts.api_jar): 174 return True 175 print('ERROR: API file out of date. Please run this command:') 176 print(' components/cronet/tools/update_api.py --api_jar %s' % 177 (os.path.abspath(opts.api_jar))) 178 return False 179 180 181def main(args): 182 parser = argparse.ArgumentParser( 183 description='Enforce Cronet API requirements.') 184 parser.add_argument('--api_jar', 185 help='Path to API jar (i.e. cronet_api.jar)', 186 required=True, 187 metavar='path/to/cronet_api.jar') 188 parser.add_argument('--impl_jar', 189 help='Path to implementation jar ' 190 '(i.e. cronet_impl_native_java.jar)', 191 required=True, 192 metavar='path/to/cronet_impl_native_java.jar', 193 action='append') 194 parser.add_argument('--stamp', help='Path to touch on success.') 195 opts = parser.parse_args(args) 196 197 ret = True 198 ret = check_api_calls(opts) and ret 199 ret = check_api_version(opts) and ret 200 if ret and opts.stamp: 201 build_utils.Touch(opts.stamp) 202 return ret 203 204 205if __name__ == '__main__': 206 sys.exit(0 if main(sys.argv[1:]) else -1) 207