File: api_static_checks.py

package info (click to toggle)
chromium 139.0.7258.127-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 6,122,156 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (215 lines) | stat: -rwxr-xr-x 12,727 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#!/usr/bin/env python
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""api_static_checks.py - Enforce Cronet API requirements."""


import argparse
import os
import re
import shutil
import sys
import tempfile

REPOSITORY_ROOT = os.path.abspath(
    os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir))

sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'build/android/gyp'))
from util import build_utils  # pylint: disable=wrong-import-position

sys.path.insert(0, os.path.join(REPOSITORY_ROOT, 'components'))
from cronet.tools import update_api  # pylint: disable=wrong-import-position


# These regular expressions catch the beginning of lines that declare classes
# and methods.  The first group returned by a match is the class or method name.
from cronet.tools.update_api import CLASS_RE  # pylint: disable=wrong-import-position

METHOD_RE = re.compile(r".* ([^ ]*)\(.*\)( .+)?;")

# Allowed exceptions.  Adding anything to this list is dangerous and should be
# avoided if possible.  For now these exceptions are for APIs that existed in
# the first version of Cronet and will be supported forever.
# TODO(pauljensen): Remove these.
ALLOWED_EXCEPTIONS = [
    'org.chromium.net.urlconnection.CronetHttpURLConnection/disconnect -> org/chromium/net/UrlRequest/cancel:()V',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/getResponseMessage -> org/chromium/net/UrlResponseInfo/getHttpStatusText:()Ljava/lang/String;',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/getResponseCode -> org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/getInputStream -> org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I',
    '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;',
    '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;',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/UploadDataProvider/getLength:()J',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/addHeader:(Ljava/lang/String;Ljava/lang/String;)Lorg/chromium/net/ExperimentalUrlRequest$Builder;',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/disableCache:()Lorg/chromium/net/ExperimentalUrlRequest$Builder;',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/setHttpMethod:(Ljava/lang/String;)Lorg/chromium/net/ExperimentalUrlRequest$Builder;',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/setTrafficStatsTag:(I)Lorg/chromium/net/ExperimentalUrlRequest$Builder;',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/setTrafficStatsUid:(I)Lorg/chromium/net/ExperimentalUrlRequest$Builder;',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/ExperimentalUrlRequest$Builder/build:()Lorg/chromium/net/ExperimentalUrlRequest;',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/startRequest -> org/chromium/net/UrlRequest/start:()V',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/getErrorStream -> org/chromium/net/UrlResponseInfo/getHttpStatusCode:()I',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/getMoreData -> org/chromium/net/UrlRequest/read:(Ljava/nio/ByteBuffer;)V',
    'org.chromium.net.urlconnection.CronetHttpURLConnection/getAllHeadersAsList -> org/chromium/net/UrlResponseInfo/getAllHeadersAsList:()Ljava/util/List;',
    'org.chromium.net.urlconnection.CronetChunkedOutputStream$UploadDataProviderImpl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V',
    'org.chromium.net.urlconnection.CronetChunkedOutputStream$UploadDataProviderImpl/rewind -> org/chromium/net/UploadDataSink/onRewindError:(Ljava/lang/Exception;)V',
    'org.chromium.net.urlconnection.CronetFixedModeOutputStream$UploadDataProviderImpl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V',
    'org.chromium.net.urlconnection.CronetFixedModeOutputStream$UploadDataProviderImpl/rewind -> org/chromium/net/UploadDataSink/onRewindError:(Ljava/lang/Exception;)V',
    'org.chromium.net.urlconnection.CronetHttpURLConnection$CronetUrlRequestCallback/onRedirectReceived -> org/chromium/net/UrlRequest/followRedirect:()V',
    'org.chromium.net.urlconnection.CronetHttpURLConnection$CronetUrlRequestCallback/onRedirectReceived -> org/chromium/net/UrlRequest/cancel:()V',
    'org.chromium.net.urlconnection.CronetHttpURLStreamHandler/openConnection -> org/chromium/net/ExperimentalCronetEngine/openConnection:(Ljava/net/URL;)Ljava/net/URLConnection;',
    'org.chromium.net.urlconnection.CronetHttpURLStreamHandler/openConnection -> org/chromium/net/ExperimentalCronetEngine/openConnection:(Ljava/net/URL;Ljava/net/Proxy;)Ljava/net/URLConnection;',
    'org.chromium.net.urlconnection.CronetBufferedOutputStream$UploadDataProviderImpl/read -> org/chromium/net/UploadDataSink/onReadSucceeded:(Z)V',
    'org.chromium.net.urlconnection.CronetBufferedOutputStream$UploadDataProviderImpl/rewind -> org/chromium/net/UploadDataSink/onRewindSucceeded:()V',
    '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;',
    'org.chromium.net.impl.NetworkExceptionImpl/getMessage -> org/chromium/net/NetworkException/getMessage:()Ljava/lang/String;',
    'org.chromium.net.impl.VersionSafeProxyOptions/org.chromium.net.impl.VersionSafeProxyOptions -> org/chromium/net/ProxyOptions/getProxyList:()Ljava/util/List;',
    'org.chromium.net.impl.VersionSafeProxyOptions/createProxyCallbackList -> org/chromium/net/ProxyOptions/getProxyList:()Ljava/util/List;',
    'org.chromium.net.impl.VersionSafeProxyOptions/createProxyCallbackList -> org/chromium/net/Proxy/getCallback:()Lorg/chromium/net/Proxy$Callback;',
    'org.chromium.net.impl.VersionSafeProxyOptions/createProxyOptionsProto -> org/chromium/net/ProxyOptions/getProxyList:()Ljava/util/List;',
    'org.chromium.net.impl.VersionSafeProxyOptions/createProxyOptionsProto -> org/chromium/net/Proxy/getHost:()Ljava/lang/String;',
    'org.chromium.net.impl.VersionSafeProxyOptions/createProxyOptionsProto -> org/chromium/net/Proxy/getPort:()I',
    'org.chromium.net.impl.VersionSafeProxyOptions/createProxyOptionsProto -> org/chromium/net/Proxy/getScheme:()I',
    'org.chromium.net.impl.VersionSafeProxyCallback/onBeforeTunnelRequest -> org/chromium/net/Proxy$Callback/onBeforeTunnelRequest:()Ljava/util/List;',
    'org.chromium.net.impl.VersionSafeProxyCallback/onTunnelHeadersReceived -> org/chromium/net/Proxy$Callback/onTunnelHeadersReceived:(Ljava/util/List;I)Z',
]

JAR_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'jar')
JAVAP_PATH = os.path.join(build_utils.JAVA_HOME, 'bin', 'javap')


def find_api_calls(dump, api_classes, bad_calls):
  # Given a dump of an implementation class, find calls through API classes.
  # |dump| is the output of "javap -c" on the implementation class files.
  # |api_classes| is the list of classes comprising the API.
  # |bad_calls| is the list of calls through API classes.  This list is built up
  #             by this function.

  for i, line in enumerate(dump):
    try:
      if CLASS_RE.match(line):
        caller_class = CLASS_RE.match(line).group(2)
      if METHOD_RE.match(line):
        caller_method = METHOD_RE.match(line).group(1)
      if line.startswith(': invoke', 8) and not line.startswith('dynamic', 16):
        callee = line.split(' // ')[1].split('Method ')[1].split('\n')[0]
        callee_class = callee.split('.')[0]
        assert callee_class
        if callee_class in api_classes:
          callee_method = callee.split('.')[1]
          assert callee_method
          # Ignore constructor calls for now as every implementation class
          # that extends an API class will call them.
          # TODO(pauljensen): Look into enforcing restricting constructor calls.
          # https://crbug.com/674975
          if callee_method.startswith('"<init>"'):
            continue
          # Ignore VersionSafe calls
          if 'VersionSafeCallbacks' in caller_class:
            continue
          bad_call = '%s/%s -> %s/%s' % (caller_class, caller_method,
                                         callee_class, callee_method)
          if bad_call in ALLOWED_EXCEPTIONS:
            continue
          bad_calls += [bad_call]
    except Exception:
      sys.stderr.write(f'Failed on line {i+1}: {line}')
      raise


def check_api_calls(opts):
  # Returns True if no calls through API classes in implementation.

  temp_dir = tempfile.mkdtemp()

  # Extract API class files from jar
  jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf',
             os.path.abspath(opts.api_jar)]
  build_utils.CheckOutput(jar_cmd, cwd=temp_dir)
  shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True)

  # Collect names of API classes
  api_classes = []
  for dirpath, _, filenames in os.walk(temp_dir):
    if not filenames:
      continue
    package = os.path.relpath(dirpath, temp_dir)
    for filename in filenames:
      if filename.endswith('.class'):
        classname = filename[:-len('.class')]
        api_classes += [os.path.normpath(os.path.join(package, classname))]

  shutil.rmtree(temp_dir)
  temp_dir = tempfile.mkdtemp()

  # Extract impl class files from jars
  for impl_jar in opts.impl_jar:
    jar_cmd = [os.path.relpath(JAR_PATH, temp_dir), 'xf',
               os.path.abspath(impl_jar)]
    build_utils.CheckOutput(jar_cmd, cwd=temp_dir)
  shutil.rmtree(os.path.join(temp_dir, 'META-INF'), ignore_errors=True)

  # Process classes
  bad_api_calls = []
  for dirpath, _, filenames in os.walk(temp_dir):
    if not filenames:
      continue
    # Dump classes
    dump_file = os.path.join(temp_dir, 'dump.txt')
    javap_cmd = '%s -private -c %s > %s' % (JAVAP_PATH, ' '.join(
        os.path.join(dirpath, f)
        for f in filenames).replace('$', '\\$'), dump_file)
    if os.system(javap_cmd):
      print('ERROR: javap failed on ' + ' '.join(filenames))
      return False
    # Process class dump
    with open(dump_file, 'r') as dump:
      find_api_calls(dump, api_classes, bad_api_calls)

  shutil.rmtree(temp_dir)

  if bad_api_calls:
    print('ERROR: Found the following calls from implementation classes '
          'through')
    print('       API classes.  These could fail if older API is used that')
    print('       does not contain newer methods.  Please call through a')
    print('       wrapper class from VersionSafeCallbacks.')
    print('\n'.join(bad_api_calls))
  return not bad_api_calls


def check_api_version(opts):
  if update_api.check_up_to_date(opts.api_jar):
    return True
  print('ERROR: API file out of date.  Please run this command:')
  print('       components/cronet/tools/update_api.py --api_jar %s' %
        (os.path.abspath(opts.api_jar)))
  return False


def main(args):
  parser = argparse.ArgumentParser(
      description='Enforce Cronet API requirements.')
  parser.add_argument('--api_jar',
                      help='Path to API jar (i.e. cronet_api.jar)',
                      required=True,
                      metavar='path/to/cronet_api.jar')
  parser.add_argument('--impl_jar',
                      help='Path to implementation jar '
                          '(i.e. cronet_impl_native_java.jar)',
                      required=True,
                      metavar='path/to/cronet_impl_native_java.jar',
                      action='append')
  parser.add_argument('--stamp', help='Path to touch on success.')
  opts = parser.parse_args(args)

  ret = True
  ret = check_api_calls(opts) and ret
  ret = check_api_version(opts) and ret
  if ret and opts.stamp:
    build_utils.Touch(opts.stamp)
  return ret


if __name__ == '__main__':
  sys.exit(0 if main(sys.argv[1:]) else -1)