xref: /aosp_15_r20/external/cronet/components/cronet/tools/api_static_checks.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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