| #!/usr/bin/env python |
| # Copyright (c) 2017 Google Inc. |
| |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| """Ensures that all externally visible functions in the library have an appropriate name |
| |
| Appropriate function names are: |
| - names starting with spv, |
| - anything in a namespace, |
| - functions added by the protobuf compiler, |
| - and weak definitions of new and delete.""" |
| |
| import os.path |
| import re |
| import subprocess |
| import sys |
| |
| |
| PROG = 'check_symbol_exports' |
| |
| |
| def command_output(cmd, directory): |
| """Runs a command in a directory and returns its standard output stream. |
| |
| Captures the standard error stream. |
| |
| Raises a RuntimeError if the command fails to launch or otherwise fails. |
| """ |
| p = subprocess.Popen(cmd, |
| cwd=directory, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.PIPE, |
| universal_newlines=True) |
| (stdout, _) = p.communicate() |
| if p.returncode != 0: |
| raise RuntimeError('Failed to run %s in %s' % (cmd, directory)) |
| return stdout |
| |
| |
| def check_library(library): |
| """Scans the given library file for global exports. If all such |
| exports are namespaced or begin with spv (in either C or C++ styles) |
| then return 0. Otherwise emit a message and return 1.""" |
| |
| # The pattern for an externally visible symbol record |
| symbol_pattern = re.compile(r'^[0-aA-Fa-f]+ +([wg]) *F \.text.*[0-9A-Fa-f]+ +(.*)') |
| |
| # Ok patterns are as follows, assuming Itanium name mangling: |
| # spv[A-Z] : extern "C" symbol starting with spv |
| # _ZN : something in a namespace |
| # _ZSt : something in the standard namespace |
| # _ZZN : something in a local scope and namespace |
| # _Z[0-9]+spv[A-Z_] : C++ symbol starting with spv[A-Z_] |
| symbol_ok_pattern = re.compile(r'^(spv[A-Z]|_ZN|_ZSt|_ZZN|_Z[0-9]+spv[A-Z_])') |
| |
| # In addition, the following pattern allowlists global functions that are added |
| # by the protobuf compiler: |
| # - AddDescriptors_spvtoolsfuzz_2eproto() |
| # - InitDefaults_spvtoolsfuzz_2eproto() |
| symbol_allowlist_pattern = re.compile(r'_Z[0-9]+(InitDefaults|AddDescriptors)_spvtoolsfuzz_2eprotov') |
| |
| symbol_is_new_or_delete = re.compile(r'^(_Zna|_Znw|_Zdl|_Zda)') |
| # Compilaion for Arm has various thunks for constructors, destructors, vtables. |
| # They are weak. |
| symbol_is_thunk = re.compile(r'^_ZT') |
| |
| # This occurs in NDK builds. |
| symbol_is_hidden = re.compile(r'^\.hidden ') |
| |
| seen = set() |
| result = 0 |
| for line in command_output(['objdump', '-t', library], '.').split('\n'): |
| match = symbol_pattern.search(line) |
| if match: |
| linkage = match.group(1) |
| symbol = match.group(2) |
| if symbol not in seen: |
| seen.add(symbol) |
| #print("look at '{}'".format(symbol)) |
| if not (symbol_is_new_or_delete.match(symbol) and linkage == 'w'): |
| if not (symbol_is_thunk.match(symbol) and linkage == 'w'): |
| if not (symbol_allowlist_pattern.match(symbol) or |
| symbol_ok_pattern.match(symbol) or |
| symbol_is_hidden.match(symbol)): |
| print('{}: error: Unescaped exported symbol: {}'.format(PROG, symbol)) |
| result = 1 |
| return result |
| |
| |
| def main(): |
| import argparse |
| parser = argparse.ArgumentParser(description='Check global names exported from a library') |
| parser.add_argument('library', help='The static library to examine') |
| args = parser.parse_args() |
| |
| if not os.path.isfile(args.library): |
| print('{}: error: {} does not exist'.format(PROG, args.library)) |
| sys.exit(1) |
| |
| if os.name == 'posix': |
| status = check_library(args.library) |
| sys.exit(status) |
| else: |
| print('Passing test since not on Posix') |
| sys.exit(0) |
| |
| |
| if __name__ == '__main__': |
| main() |