• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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