| Zelalem | 219df41 | 2020-05-17 19:21:20 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python3 | 
|  | 2 | # | 
| Leonardo Sandoval | 579c737 | 2020-10-23 15:23:32 -0500 | [diff] [blame] | 3 | # Copyright (c) 2019-2020 Arm Limited. All rights reserved. | 
| Zelalem | 219df41 | 2020-05-17 19:21:20 -0500 | [diff] [blame] | 4 | # | 
|  | 5 | # SPDX-License-Identifier: BSD-3-Clause | 
|  | 6 | # | 
|  | 7 |  | 
|  | 8 | import argparse | 
|  | 9 | import os | 
|  | 10 | import re | 
|  | 11 | import sys | 
|  | 12 | import utils | 
|  | 13 |  | 
|  | 14 | # File extensions to check | 
|  | 15 | VALID_FILE_EXTENSIONS = ('.c', '.S', '.h') | 
|  | 16 |  | 
|  | 17 | # Paths inside the tree to ignore. Hidden folders and files are always ignored. | 
|  | 18 | # They mustn't end in '/'. | 
|  | 19 | IGNORED_FOLDERS = ( | 
|  | 20 | "tools", | 
|  | 21 | "docs" | 
|  | 22 | ) | 
|  | 23 |  | 
|  | 24 | # List of ignored files in folders that aren't ignored | 
|  | 25 | IGNORED_FILES = () | 
|  | 26 |  | 
|  | 27 | # Regular expression for searching the Banned APIs. This is taken from the | 
|  | 28 | # Coding guideline in TF-A repo | 
|  | 29 | BANNED_APIS = ["strcpy", "wcscpy", "strncpy", "strcat", "wcscat", "strncat", | 
|  | 30 | "sprintf", "vsprintf", "strtok", "atoi", "atol", "atoll", | 
|  | 31 | "itoa", "ltoa", "lltoa"] | 
| Madhukar Pappireddy | b365fc9 | 2020-09-18 15:56:10 -0500 | [diff] [blame] | 32 | BANNED_PATTERN = re.compile('\(|'.join(BANNED_APIS)) | 
| Zelalem | 219df41 | 2020-05-17 19:21:20 -0500 | [diff] [blame] | 33 |  | 
|  | 34 | COMMENTS_PATTERN = re.compile(r"//|/\*|\*/") | 
|  | 35 |  | 
|  | 36 |  | 
|  | 37 | def filter_comments(f): | 
|  | 38 | ''' | 
|  | 39 | filter_comments(f) -> iterator for line number, filtered line | 
|  | 40 |  | 
|  | 41 | Given an iterable of lines (such as a file), return another iterable of | 
|  | 42 | lines, with the comments filtered out and removed. | 
|  | 43 | ''' | 
|  | 44 |  | 
|  | 45 | in_comment = False | 
|  | 46 | for line_num, line in enumerate(f): | 
|  | 47 | line = line.rstrip('\n') | 
|  | 48 |  | 
|  | 49 | temp = "" | 
|  | 50 | breaker = len(line) if in_comment else 0 | 
|  | 51 | for match in COMMENTS_PATTERN.finditer(line): | 
|  | 52 | content = match.group(0) | 
|  | 53 | start, end = match.span() | 
|  | 54 |  | 
|  | 55 | if in_comment: | 
|  | 56 | if content == "*/": | 
|  | 57 | in_comment = False | 
|  | 58 | breaker = end | 
|  | 59 | else: | 
|  | 60 | if content == "/*": | 
|  | 61 | in_comment = True | 
|  | 62 | temp += line[breaker:start] | 
|  | 63 | breaker = len(line) | 
|  | 64 | elif content == "//": | 
|  | 65 | temp += line[breaker:start] | 
|  | 66 | breaker = len(line) | 
|  | 67 | break | 
|  | 68 |  | 
|  | 69 | temp += line[breaker:] | 
|  | 70 | if temp: | 
|  | 71 | yield line_num + 1, temp | 
|  | 72 |  | 
|  | 73 |  | 
|  | 74 | def file_check_banned_api(path, encoding='utf-8'): | 
|  | 75 | ''' | 
|  | 76 | Reads all lines from a file in path and checks for any banned APIs. | 
|  | 77 | The combined number of errors and uses of banned APIs is returned. If the | 
|  | 78 | result is equal to 0, the file is clean and contains no banned APIs. | 
|  | 79 | ''' | 
|  | 80 |  | 
|  | 81 | count = 0 | 
|  | 82 |  | 
|  | 83 | try: | 
|  | 84 | f = open(path, encoding=encoding) | 
|  | 85 | except FileNotFoundError: | 
|  | 86 | print("ERROR: could not open " + path) | 
|  | 87 | utils.print_exception_info() | 
|  | 88 | return True | 
|  | 89 |  | 
|  | 90 | try: | 
|  | 91 | for line_num, line in filter_comments(f): | 
|  | 92 | match = BANNED_PATTERN.search(line) | 
|  | 93 | if match: | 
|  | 94 | location = "line {} of file {}".format(line_num, path) | 
|  | 95 | print("BANNED API: in " + location) | 
|  | 96 |  | 
|  | 97 | # NOTE: this preview of the error is not perfect if comments | 
|  | 98 | # have been removed - however, it does good enough most of the | 
|  | 99 | # time. | 
|  | 100 | start, end = match.span() | 
|  | 101 | print(">>> {}".format(line)) | 
|  | 102 | print("    {}^{}".format(start * " ", (end - start - 1) * "~")) | 
|  | 103 |  | 
|  | 104 | count += 1 | 
|  | 105 | except: | 
|  | 106 | print("ERROR: unexpected exception while parsing " + path) | 
|  | 107 | utils.print_exception_info() | 
|  | 108 | count += 1 | 
|  | 109 |  | 
|  | 110 | f.close() | 
|  | 111 |  | 
|  | 112 | return count | 
|  | 113 |  | 
|  | 114 |  | 
|  | 115 | def get_tree_files(): | 
|  | 116 | ''' | 
|  | 117 | Get all files in the git repository | 
|  | 118 | ''' | 
|  | 119 |  | 
|  | 120 | # Get patches of the affected commits with one line of context. | 
|  | 121 | (rc, stdout, stderr) = utils.shell_command(['git', 'ls-files']) | 
|  | 122 | if rc != 0: | 
|  | 123 | return False | 
|  | 124 |  | 
|  | 125 | lines = stdout.splitlines() | 
|  | 126 | return lines | 
|  | 127 |  | 
|  | 128 |  | 
|  | 129 | def get_patch_files(base_commit, end_commit): | 
|  | 130 | ''' | 
|  | 131 | Get all files that have changed in a given patch | 
|  | 132 | ''' | 
|  | 133 |  | 
|  | 134 | # Get patches of the affected commits with one line of context. | 
|  | 135 | (rc, stdout, stderr) = utils.shell_command([ | 
|  | 136 | 'git', 'diff-tree', '--diff-filter=ACMRT', '-r', '--name-only', | 
|  | 137 | base_commit, end_commit]) | 
|  | 138 |  | 
|  | 139 | if rc != 0: | 
|  | 140 | return False | 
|  | 141 |  | 
|  | 142 | paths = stdout.splitlines() | 
|  | 143 | return paths | 
|  | 144 |  | 
|  | 145 |  | 
|  | 146 | def parse_cmd_line(): | 
|  | 147 | parser = argparse.ArgumentParser( | 
|  | 148 | description="Check Banned APIs", | 
|  | 149 | epilog=""" | 
|  | 150 | For each source file in the tree, checks whether Banned APIs as | 
|  | 151 | described in the list are used or not. | 
|  | 152 | """ | 
|  | 153 | ) | 
|  | 154 |  | 
|  | 155 | parser.add_argument("--tree", "-t", | 
|  | 156 | help=""" | 
|  | 157 | Path to the source tree to check (default: %(default)s) | 
|  | 158 | """, | 
|  | 159 | default=os.curdir) | 
|  | 160 | parser.add_argument("--patch", "-p", | 
|  | 161 | help=""" | 
|  | 162 | Patch mode. Instead of checking all files in | 
|  | 163 | the source tree, the script will consider only files | 
|  | 164 | that are modified by the latest patch(es). | 
|  | 165 | """, | 
|  | 166 | action="store_true") | 
|  | 167 | parser.add_argument("--from-ref", | 
|  | 168 | help=""" | 
|  | 169 | Base commit in patch mode (default: %(default)s) | 
|  | 170 | """, | 
|  | 171 | default="master") | 
|  | 172 | parser.add_argument("--to-ref", | 
|  | 173 | help=""" | 
|  | 174 | Final commit in patch mode (default: %(default)s) | 
|  | 175 | """, | 
|  | 176 | default="HEAD") | 
|  | 177 | parser.add_argument("--verbose", "-v", | 
|  | 178 | help="Print verbose output", | 
|  | 179 | action="store_true") | 
|  | 180 | args = parser.parse_args() | 
|  | 181 | return args | 
|  | 182 |  | 
|  | 183 |  | 
|  | 184 | if __name__ == "__main__": | 
|  | 185 | args = parse_cmd_line() | 
|  | 186 |  | 
|  | 187 | os.chdir(args.tree) | 
|  | 188 |  | 
|  | 189 | if args.patch: | 
|  | 190 | print("Checking files modified between patches " + args.from_ref + | 
|  | 191 | " and " + args.to_ref + "...\n") | 
|  | 192 | files = get_patch_files(args.from_ref, args.to_ref) | 
|  | 193 | else: | 
|  | 194 | print("Checking all files git repo " + os.path.abspath(args.tree) + | 
|  | 195 | "...\n") | 
|  | 196 | files = get_tree_files() | 
|  | 197 |  | 
|  | 198 | total_errors = 0 | 
|  | 199 | for filename in files: | 
|  | 200 | ignored = utils.file_is_ignored(filename, VALID_FILE_EXTENSIONS, | 
|  | 201 | IGNORED_FILES, IGNORED_FOLDERS) | 
|  | 202 | if ignored: | 
|  | 203 | if args.verbose: | 
|  | 204 | print("INFO: Skipping ignored file " + filename) | 
|  | 205 | continue | 
|  | 206 |  | 
|  | 207 | if args.verbose: | 
|  | 208 | print("INFO: Checking " + filename) | 
|  | 209 |  | 
|  | 210 | total_errors += file_check_banned_api(filename) | 
|  | 211 |  | 
|  | 212 | print(str(total_errors) + " errors found") | 
|  | 213 |  | 
|  | 214 | if total_errors == 0: | 
|  | 215 | sys.exit(0) | 
|  | 216 | else: | 
|  | 217 | sys.exit(1) |