diff options
Diffstat (limited to 'tools/check_static.py')
-rwxr-xr-x | tools/check_static.py | 326 |
1 files changed, 326 insertions, 0 deletions
diff --git a/tools/check_static.py b/tools/check_static.py new file mode 100755 index 00000000..fbd1d11c --- /dev/null +++ b/tools/check_static.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +# Wireshark - Network traffic analyzer +# By Gerald Combs <gerald@wireshark.org> +# Copyright 1998 Gerald Combs +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import os +import re +import subprocess +import argparse +import signal + +# Look for dissector symbols that could/should be static. +# This will not run on Windows, unless/until we check the platform +# and use (I think) dumpbin.exe + +# Try to exit soon after Ctrl-C is pressed. +should_exit = False + +def signal_handler(sig, frame): + global should_exit + should_exit = True + print('You pressed Ctrl+C - exiting') + +signal.signal(signal.SIGINT, signal_handler) + +# Allow this as a default build folder name... +build_folder = os.getcwd() + '-build' + +# Record which symbols are referred to (by a set of files). +class CalledSymbols: + def __init__(self): + self.referred = set() + + def addCalls(self, file): + # Make sure that file is built. + last_dir = os.path.split(os.path.dirname(file))[-1] + if file.find('ui/cli') != -1: + # A tshark target-only file + object_file = os.path.join(build_folder, 'CMakeFiles', ('tshark' + '.dir'), file + '.o') + elif file.find('ui/qt') != -1: + object_file = os.path.join(build_folder, os.path.dirname(file), 'CMakeFiles', ('qtui' + '.dir'), os.path.basename(file) + '.o') + else: + if file.endswith('dissectors.c'): + object_file = os.path.join(build_folder, os.path.dirname(file), 'CMakeFiles', 'dissector-registration' + '.dir', os.path.basename(file) + '.o') + else: + object_file = os.path.join(build_folder, os.path.dirname(file), 'CMakeFiles', last_dir + '.dir', os.path.basename(file) + '.o') + if not os.path.exists(object_file): + #print('Warning -', object_file, 'does not exist') + return + command = ['nm', object_file] + for f in subprocess.check_output(command).splitlines(): + l = str(f)[2:-1] + # Lines might or might not have an address before letter and symbol. + p1 = re.compile(r'[0-9a-f]* ([a-zA-Z]) (.*)') + p2 = re.compile(r'[ ]* ([a-zA-Z]) (.*)') + + m = p1.match(l) + if not m: + m = p2.match(l) + if m: + letter = m.group(1) + function_name = m.group(2) + + # Only interested in undefined references to symbols. + if letter == 'U': + self.referred.add(function_name) + + + +# Record which symbols are defined in a single file. +class DefinedSymbols: + def __init__(self, file): + self.filename = file + self.global_dict = {} + self.header_file_contents = None + + # Make sure that file is built. + object_file = os.path.join(build_folder, 'epan', 'dissectors', 'CMakeFiles', 'dissectors.dir', os.path.basename(file) + '.o') + + if not os.path.exists(object_file): + #print('Warning -', object_file, 'does not exist') + return + + header_file= file.replace('.c', '.h') + try: + f = open(header_file, 'r') + self.header_file_contents = f.read() + except IOError: + pass + + + command = ['nm', object_file] + for f in subprocess.check_output(command).splitlines(): + # Line consists of whitespace, [address], letter, symbolName + l = str(f)[2:-1] + p = re.compile(r'[0-9a-f]* ([a-zA-Z]) (.*)') + m = p.match(l) + if m: + letter = m.group(1) + function_name = m.group(2) + # globally-defined symbols. Would be 't' or 'd' if already static. + if letter in 'TD': + self.add(function_name, l) + + def add(self, letter, function_name): + self.global_dict[letter] = function_name + + def mentionedInHeaders(self, symbol): + if self.header_file_contents: + if self.header_file_contents.find(symbol) != -1: + return True + # Also check some of the 'common' header files that don't match the dissector file name. + # TODO: could cache the contents of these files, but it's not that slow. + common_mismatched_headers = [ os.path.join('epan', 'dissectors', 'packet-ncp-int.h'), + os.path.join('epan', 'dissectors', 'packet-mq.h'), + os.path.join('epan', 'dissectors', 'packet-ip.h'), + os.path.join('epan', 'dissectors', 'packet-gsm_a_common.h'), + os.path.join('epan', 'dissectors', 'packet-epl.h'), + os.path.join('epan', 'dissectors', 'packet-bluetooth.h'), + os.path.join('epan', 'dissectors', 'packet-dcerpc.h'), + os.path.join('epan', 'ip_opts.h'), + os.path.join('epan', 'eap.h')] + for hf in common_mismatched_headers: + try: + f = open(hf) + contents = f.read() + if contents.find(symbol) != -1: + return True + except EnvironmentError: + pass + + return False + + def check(self, called_symbols): + global issues_found + for f in self.global_dict: + if not f in called_symbols: + mentioned_in_header = self.mentionedInHeaders(f) + fun = self.global_dict[f] + print(self.filename, '(' + fun + ')', 'is not referred to so could be static?', '(in header)' if mentioned_in_header else '') + issues_found += 1 + + + +# Helper functions. + +def isDissectorFile(filename): + p = re.compile(r'(packet|file)-.*\.c') + return p.match(filename) + +# Test for whether the given dissector file was automatically generated. +def isGeneratedFile(filename): + # Check file exists - e.g. may have been deleted in a recent commit. + if not os.path.exists(filename): + return False + + if not filename.endswith('.c'): + return False + + # Open file + f_read = open(os.path.join(filename), 'r') + lines_tested = 0 + for line in f_read: + # The comment to say that its generated is near the top, so give up once + # get a few lines down. + if lines_tested > 10: + f_read.close() + return False + if (line.find('Generated automatically') != -1 or + line.find('Autogenerated from') != -1 or + line.find('is autogenerated') != -1 or + line.find('automatically generated by Pidl') != -1 or + line.find('Created by: The Qt Meta Object Compiler') != -1 or + line.find('This file was generated') != -1 or + line.find('This filter was automatically generated') != -1): + + f_read.close() + return True + lines_tested = lines_tested + 1 + + # OK, looks like a hand-written file! + f_read.close() + return False + + +def findDissectorFilesInFolder(folder, include_generated): + # Look at files in sorted order, to give some idea of how far through is. + tmp_files = [] + + for f in sorted(os.listdir(folder)): + if should_exit: + return + if isDissectorFile(f): + if include_generated or not isGeneratedFile(os.path.join('epan', 'dissectors', f)): + filename = os.path.join(folder, f) + tmp_files.append(filename) + return tmp_files + +def findFilesInFolder(folder): + # Look at files in sorted order, to give some idea of how far through is. + tmp_files = [] + + for f in sorted(os.listdir(folder)): + if should_exit: + return + if f.endswith('.c') or f.endswith('.cpp'): + filename = os.path.join(folder, f) + tmp_files.append(filename) + return tmp_files + + +def is_dissector_file(filename): + p = re.compile(r'.*packet-.*\.c') + return p.match(filename) + + +issues_found = 0 + + + +################################################################# +# Main logic. + +# command-line args. Controls which dissector files should be checked. +# If no args given, will just scan epan/dissectors folder. +parser = argparse.ArgumentParser(description='Check calls in dissectors') +parser.add_argument('--build-folder', action='store', default='', + help='build folder', required=False) +parser.add_argument('--file', action='append', + help='specify individual dissector file to test') +parser.add_argument('--commits', action='store', + help='last N commits to check') +parser.add_argument('--open', action='store_true', + help='check open files') + +args = parser.parse_args() + + +# Get files from wherever command-line args indicate. +files = [] + +if args.build_folder: + build_folder = args.build_folder + +if args.file: + # Add specified file(s) + for f in args.file: + if not f.startswith('epan'): + f = os.path.join('epan', 'dissectors', f) + if not os.path.isfile(f): + print('Chosen file', f, 'does not exist.') + exit(1) + else: + files.append(f) +elif args.commits: + # Get files affected by specified number of commits. + command = ['git', 'diff', '--name-only', 'HEAD~' + args.commits] + files = [f.decode('utf-8') + for f in subprocess.check_output(command).splitlines()] + # Will examine dissector files only + files = list(filter(lambda f : is_dissector_file(f), files)) +elif args.open: + # Unstaged changes. + command = ['git', 'diff', '--name-only'] + files = [f.decode('utf-8') + for f in subprocess.check_output(command).splitlines()] + # Only interested in dissector files. + files = list(filter(lambda f : is_dissector_file(f), files)) + # Staged changes. + command = ['git', 'diff', '--staged', '--name-only'] + files_staged = [f.decode('utf-8') + for f in subprocess.check_output(command).splitlines()] + # Only interested in dissector files. + files_staged = list(filter(lambda f : is_dissector_file(f), files_staged)) + for f in files: + files.append(f) + for f in files_staged: + if not f in files: + files.append(f) +else: + # Find all dissector files from folder. + files = findDissectorFilesInFolder(os.path.join('epan', 'dissectors'), + include_generated=False) + + +# If scanning a subset of files, list them here. +print('Examining:') +if args.file or args.commits or args.open: + if files: + print(' '.join(files), '\n') + else: + print('No files to check.\n') +else: + print('All dissector modules\n') + + +if not os.path.isdir(build_folder): + print('Build directory not valid', build_folder, '- please set with --build-folder') + exit(1) + + +# Get the set of called functions and referred-to data. +called = CalledSymbols() +for d in findDissectorFilesInFolder(os.path.join('epan', 'dissectors'), include_generated=True): + called.addCalls(d) +called.addCalls(os.path.join('epan', 'dissectors', 'dissectors.c')) +# Also check calls from GUI code +for d in findFilesInFolder('ui'): + called.addCalls(d) +for d in findFilesInFolder(os.path.join('ui', 'qt')): + called.addCalls(d) +# These are from tshark.. +for d in findFilesInFolder(os.path.join('ui', 'cli')): + called.addCalls(d) + + +# Now check identified files. +for f in files: + if should_exit: + exit(1) + DefinedSymbols(f).check(called.referred) + +# Show summary. +print(issues_found, 'issues found') |