• 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
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