blob: c129def4edde9d0da04dd8d39819bb4b571b904a [file] [log] [blame]
Yuto Takano39639672021-08-05 19:47:48 +01001#!/usr/bin/env python3
2#
3# Copyright The Mbed TLS Contributors
4# SPDX-License-Identifier: Apache-2.0
5#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15# See the License for the specific language governing permissions and
16# limitations under the License.
17
Darryl Greend5802922018-05-08 15:30:59 +010018"""
Yuto Takano39639672021-08-05 19:47:48 +010019This script confirms that the naming of all symbols and identifiers in Mbed TLS
Yuto Takano159255a2021-08-06 17:00:28 +010020are consistent with the house style and are also self-consistent. It only runs
21on Linux and macOS since it depends on nm.
22
23The script performs the following checks:
Yuto Takano81528c02021-08-06 16:22:06 +010024
25- All exported and available symbols in the library object files, are explicitly
Yuto Takano159255a2021-08-06 17:00:28 +010026 declared in the header files. This uses the nm command.
Yuto Takano81528c02021-08-06 16:22:06 +010027- All macros, constants, and identifiers (function names, struct names, etc)
28 follow the required pattern.
29- Typo checking: All words that begin with MBED exist as macros or constants.
Yuto Takanofc54dfb2021-08-07 17:18:28 +010030
31Returns 0 on success, 1 on test failure, and 2 if there is a script error or a
32subprocess error. Must be run from Mbed TLS root.
Darryl Greend5802922018-05-08 15:30:59 +010033"""
Yuto Takano39639672021-08-05 19:47:48 +010034
35import argparse
Yuto Takano977e07f2021-08-09 11:56:15 +010036import glob
Yuto Takano39639672021-08-05 19:47:48 +010037import textwrap
Darryl Greend5802922018-05-08 15:30:59 +010038import os
39import sys
40import traceback
41import re
42import shutil
43import subprocess
44import logging
45
Yuto Takano81528c02021-08-06 16:22:06 +010046# Naming patterns to check against. These are defined outside the NameCheck
47# class for ease of modification.
Yuto Takanobb7dca42021-08-05 19:57:58 +010048MACRO_PATTERN = r"^(MBEDTLS|PSA)_[0-9A-Z_]*[0-9A-Z]$"
Yuto Takano81528c02021-08-06 16:22:06 +010049CONSTANTS_PATTERN = MACRO_PATTERN
Yuto Takanoc1838932021-08-05 19:52:09 +010050IDENTIFIER_PATTERN = r"^(mbedtls|psa)_[0-9a-z_]*[0-9a-z]$"
Yuto Takano39639672021-08-05 19:47:48 +010051
Yuto Takanod93fa372021-08-06 23:05:55 +010052class Match(): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010053 """
54 A class representing a match, together with its found position.
55
56 Fields:
57 * filename: the file that the match was in.
58 * line: the full line containing the match.
Yuto Takanod93fa372021-08-06 23:05:55 +010059 * pos: a tuple of (line_no, start, end) positions on the file line where the
60 match is.
Yuto Takano81528c02021-08-06 16:22:06 +010061 * name: the match itself.
62 """
Yuto Takanod93fa372021-08-06 23:05:55 +010063 def __init__(self, filename, line, pos, name):
Yuto Takano39639672021-08-05 19:47:48 +010064 self.filename = filename
65 self.line = line
66 self.pos = pos
67 self.name = name
Yuto Takano39639672021-08-05 19:47:48 +010068
Yuto Takanoa4e75122021-08-06 17:23:28 +010069 def __str__(self):
Yuto Takano381fda82021-08-06 23:37:20 +010070 ln_str = str(self.pos[0])
71 gutter_len = max(4, len(ln_str))
72 gutter = (gutter_len - len(ln_str)) * " " + ln_str
73 underline = self.pos[1] * " " + (self.pos[2] - self.pos[1]) * "^"
74
Yuto Takanoa4e75122021-08-06 17:23:28 +010075 return (
Yuto Takano381fda82021-08-06 23:37:20 +010076 " {0} |\n".format(gutter_len * " ") +
77 " {0} | {1}".format(gutter, self.line) +
Yuto Takano55614b52021-08-07 01:00:18 +010078 " {0} | {1}\n".format(gutter_len * " ", underline)
Yuto Takanoa4e75122021-08-06 17:23:28 +010079 )
Yuto Takanod93fa372021-08-06 23:05:55 +010080
81class Problem(): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010082 """
83 A parent class representing a form of static analysis error.
84
85 Fields:
86 * textwrapper: a TextWrapper instance to format problems nicely.
87 """
Yuto Takano39639672021-08-05 19:47:48 +010088 def __init__(self):
89 self.textwrapper = textwrap.TextWrapper()
Yuto Takano81528c02021-08-06 16:22:06 +010090 self.textwrapper.width = 80
Yuto Takanoa4e75122021-08-06 17:23:28 +010091 self.textwrapper.initial_indent = " > "
Yuto Takano81528c02021-08-06 16:22:06 +010092 self.textwrapper.subsequent_indent = " "
Yuto Takano39639672021-08-05 19:47:48 +010093
Yuto Takanod93fa372021-08-06 23:05:55 +010094class SymbolNotInHeader(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010095 """
96 A problem that occurs when an exported/available symbol in the object file
97 is not explicitly declared in header files. Created with
98 NameCheck.check_symbols_declared_in_header()
99
100 Fields:
101 * symbol_name: the name of the symbol.
102 """
Yuto Takano55614b52021-08-07 01:00:18 +0100103 def __init__(self, symbol_name, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100104 self.symbol_name = symbol_name
Yuto Takano55614b52021-08-07 01:00:18 +0100105 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100106 Problem.__init__(self)
107
108 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100109 if self.quiet:
110 return "{0}".format(self.symbol_name)
111
Yuto Takano39639672021-08-05 19:47:48 +0100112 return self.textwrapper.fill(
113 "'{0}' was found as an available symbol in the output of nm, "
114 "however it was not declared in any header files."
115 .format(self.symbol_name))
116
Yuto Takanod93fa372021-08-06 23:05:55 +0100117class PatternMismatch(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +0100118 """
119 A problem that occurs when something doesn't match the expected pattern.
120 Created with NameCheck.check_match_pattern()
121
122 Fields:
123 * pattern: the expected regex pattern
124 * match: the Match object in question
125 """
Yuto Takano55614b52021-08-07 01:00:18 +0100126 def __init__(self, pattern, match, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100127 self.pattern = pattern
128 self.match = match
Yuto Takano55614b52021-08-07 01:00:18 +0100129 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100130 Problem.__init__(self)
Yuto Takano81528c02021-08-06 16:22:06 +0100131
Yuto Takano39639672021-08-05 19:47:48 +0100132 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100133 if self.quiet:
134 return ("{0}:{1}:{3}"
135 .format(
136 self.match.filename,
137 self.match.pos[0],
138 self.match.name))
139
Yuto Takano39639672021-08-05 19:47:48 +0100140 return self.textwrapper.fill(
Yuto Takanoa4e75122021-08-06 17:23:28 +0100141 "{0}:{1}: '{2}' does not match the required pattern '{3}'."
142 .format(
143 self.match.filename,
Yuto Takanod93fa372021-08-06 23:05:55 +0100144 self.match.pos[0],
Yuto Takanoa4e75122021-08-06 17:23:28 +0100145 self.match.name,
146 self.pattern)) + "\n" + str(self.match)
Yuto Takano39639672021-08-05 19:47:48 +0100147
Yuto Takanod93fa372021-08-06 23:05:55 +0100148class Typo(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +0100149 """
150 A problem that occurs when a word using MBED doesn't appear to be defined as
151 constants nor enum values. Created with NameCheck.check_for_typos()
152
153 Fields:
154 * match: the Match object of the MBED name in question.
155 """
Yuto Takano55614b52021-08-07 01:00:18 +0100156 def __init__(self, match, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100157 self.match = match
Yuto Takano55614b52021-08-07 01:00:18 +0100158 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100159 Problem.__init__(self)
Yuto Takano81528c02021-08-06 16:22:06 +0100160
Yuto Takano39639672021-08-05 19:47:48 +0100161 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100162 if self.quiet:
163 return ("{0}:{1}:{2}"
164 .format(
165 self.match.filename,
166 self.match.pos[0],
167 self.match.name))
168
Yuto Takano39639672021-08-05 19:47:48 +0100169 return self.textwrapper.fill(
Yuto Takanoa4e75122021-08-06 17:23:28 +0100170 "{0}:{1}: '{2}' looks like a typo. It was not found in any "
171 "macros or any enums. If this is not a typo, put "
172 "//no-check-names after it."
173 .format(
174 self.match.filename,
Yuto Takanod93fa372021-08-06 23:05:55 +0100175 self.match.pos[0],
Yuto Takanoa4e75122021-08-06 17:23:28 +0100176 self.match.name)) + "\n" + str(self.match)
Darryl Greend5802922018-05-08 15:30:59 +0100177
Yuto Takanod93fa372021-08-06 23:05:55 +0100178class NameCheck():
Yuto Takano81528c02021-08-06 16:22:06 +0100179 """
180 Representation of the core name checking operation performed by this script.
Yuto Takano977e07f2021-08-09 11:56:15 +0100181 Shares a common logger, and a shared return code.
Yuto Takano81528c02021-08-06 16:22:06 +0100182 """
Yuto Takano977e07f2021-08-09 11:56:15 +0100183 def __init__(self, verbose=False):
Darryl Greend5802922018-05-08 15:30:59 +0100184 self.log = None
Yuto Takanofc54dfb2021-08-07 17:18:28 +0100185 self.check_repo_path()
Darryl Greend5802922018-05-08 15:30:59 +0100186 self.return_code = 0
Yuto Takano977e07f2021-08-09 11:56:15 +0100187
188 self.setup_logger(verbose)
189
190 # Globally excluded filenames
Yuto Takano81528c02021-08-06 16:22:06 +0100191 self.excluded_files = ["bn_mul", "compat-2.x.h"]
Yuto Takano977e07f2021-08-09 11:56:15 +0100192
193 # Will contain the parse result after a comprehensive parse
Yuto Takanod93fa372021-08-06 23:05:55 +0100194 self.parse_result = {}
Darryl Greend5802922018-05-08 15:30:59 +0100195
196 def set_return_code(self, return_code):
197 if return_code > self.return_code:
Yuto Takano201f9e82021-08-06 16:36:54 +0100198 self.log.debug("Setting new return code to {}".format(return_code))
Darryl Greend5802922018-05-08 15:30:59 +0100199 self.return_code = return_code
200
Yuto Takanofc54dfb2021-08-07 17:18:28 +0100201 @staticmethod
202 def check_repo_path():
203 """
204 Check that the current working directory is the project root, and throw
205 an exception if not.
206 """
207 if not all(os.path.isdir(d) for d in ["include", "library", "tests"]):
208 raise Exception("This script must be run from Mbed TLS root")
209
Yuto Takano39639672021-08-05 19:47:48 +0100210 def setup_logger(self, verbose=False):
211 """
212 Set up a logger and set the change the default logging level from
Yuto Takano81528c02021-08-06 16:22:06 +0100213 WARNING to INFO. Loggers are better than print statements since their
Yuto Takano39639672021-08-05 19:47:48 +0100214 verbosity can be controlled.
215 """
Darryl Greend5802922018-05-08 15:30:59 +0100216 self.log = logging.getLogger()
Yuto Takano39639672021-08-05 19:47:48 +0100217 if verbose:
218 self.log.setLevel(logging.DEBUG)
219 else:
220 self.log.setLevel(logging.INFO)
Darryl Greend5802922018-05-08 15:30:59 +0100221 self.log.addHandler(logging.StreamHandler())
222
Yuto Takano977e07f2021-08-09 11:56:15 +0100223 def get_files(self, wildcard):
Yuto Takano81528c02021-08-06 16:22:06 +0100224 """
Yuto Takano977e07f2021-08-09 11:56:15 +0100225 Get all files that match a UNIX-style wildcard recursively. While the
226 script is designed only for use on UNIX/macOS (due to nm), this function
227 would work fine on Windows even with forward slashes in the wildcard.
Yuto Takano81528c02021-08-06 16:22:06 +0100228
229 Args:
Yuto Takano977e07f2021-08-09 11:56:15 +0100230 * wildcard: shell-style wildcards to match filepaths against.
Yuto Takano81528c02021-08-06 16:22:06 +0100231
232 Returns a List of relative filepaths.
233 """
Yuto Takano977e07f2021-08-09 11:56:15 +0100234 accumulator = []
235
236 for filepath in glob.iglob(wildcard, recursive=True):
237 if os.path.basename(filepath) not in self.excluded_files:
238 accumulator.append(filepath)
239 return accumulator
Darryl Greend5802922018-05-08 15:30:59 +0100240
Yuto Takano81528c02021-08-06 16:22:06 +0100241 def parse_names_in_source(self):
242 """
Yuto Takano977e07f2021-08-09 11:56:15 +0100243 Comprehensive function to call each parsing function and retrieve
244 various elements of the code, together with their source location.
245 Puts the parsed values in the internal variable self.parse_result, so
246 they can be used from perform_checks().
Yuto Takano81528c02021-08-06 16:22:06 +0100247 """
248 self.log.info("Parsing source code...")
Yuto Takanod24e0372021-08-06 16:42:33 +0100249 self.log.debug(
250 "The following files are excluded from the search: {}"
251 .format(str(self.excluded_files))
252 )
Yuto Takano81528c02021-08-06 16:22:06 +0100253
Yuto Takano977e07f2021-08-09 11:56:15 +0100254 m_headers = self.get_files("include/mbedtls/*.h")
255 p_headers = self.get_files("include/psa/*.h")
Yuto Takano81528c02021-08-06 16:22:06 +0100256 t_headers = ["3rdparty/everest/include/everest/everest.h",
257 "3rdparty/everest/include/everest/x25519.h"]
Yuto Takano977e07f2021-08-09 11:56:15 +0100258 d_headers = self.get_files("tests/include/test/drivers/*.h")
259 l_headers = self.get_files("library/*.h")
260 libraries = self.get_files("library/*.c") + [
Yuto Takano81528c02021-08-06 16:22:06 +0100261 "3rdparty/everest/library/everest.c",
262 "3rdparty/everest/library/x25519.c"]
263
264 all_macros = self.parse_macros(
265 m_headers + p_headers + t_headers + l_headers + d_headers)
266 enum_consts = self.parse_enum_consts(
267 m_headers + l_headers + t_headers)
268 identifiers = self.parse_identifiers(
269 m_headers + p_headers + t_headers + l_headers)
Yuto Takanod93fa372021-08-06 23:05:55 +0100270 mbed_words = self.parse_mbed_words(
Yuto Takano81528c02021-08-06 16:22:06 +0100271 m_headers + p_headers + t_headers + l_headers + libraries)
272 symbols = self.parse_symbols()
273
274 # Remove identifier macros like mbedtls_printf or mbedtls_calloc
275 identifiers_justname = [x.name for x in identifiers]
276 actual_macros = []
277 for macro in all_macros:
278 if macro.name not in identifiers_justname:
279 actual_macros.append(macro)
280
281 self.log.debug("Found:")
282 self.log.debug(" {} Macros".format(len(all_macros)))
283 self.log.debug(" {} Non-identifier Macros".format(len(actual_macros)))
284 self.log.debug(" {} Enum Constants".format(len(enum_consts)))
285 self.log.debug(" {} Identifiers".format(len(identifiers)))
286 self.log.debug(" {} Exported Symbols".format(len(symbols)))
287 self.log.info("Analysing...")
288
289 self.parse_result = {
290 "macros": actual_macros,
291 "enum_consts": enum_consts,
292 "identifiers": identifiers,
293 "symbols": symbols,
Yuto Takanod93fa372021-08-06 23:05:55 +0100294 "mbed_words": mbed_words
Yuto Takano81528c02021-08-06 16:22:06 +0100295 }
296
Yuto Takano39639672021-08-05 19:47:48 +0100297 def parse_macros(self, header_files):
298 """
299 Parse all macros defined by #define preprocessor directives.
300
301 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100302 * header_files: A List of filepaths to look through.
303
304 Returns a List of Match objects for the found macros.
Yuto Takano39639672021-08-05 19:47:48 +0100305 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100306 macro_regex = re.compile(r"# *define +(?P<macro>\w+)")
307 exclusions = (
Yuto Takano39639672021-08-05 19:47:48 +0100308 "asm", "inline", "EMIT", "_CRT_SECURE_NO_DEPRECATE", "MULADDC_"
309 )
310
Yuto Takano201f9e82021-08-06 16:36:54 +0100311 self.log.debug("Looking for macros in {} files".format(len(header_files)))
Yuto Takanod93fa372021-08-06 23:05:55 +0100312
313 macros = []
314
Yuto Takano39639672021-08-05 19:47:48 +0100315 for header_file in header_files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100316 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100317 for line_no, line in enumerate(header):
Yuto Takanod93fa372021-08-06 23:05:55 +0100318 for macro in macro_regex.finditer(line):
319 if not macro.group("macro").startswith(exclusions):
Yuto Takano81528c02021-08-06 16:22:06 +0100320 macros.append(Match(
321 header_file,
322 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100323 (line_no, macro.start(), macro.end()),
Yuto Takano81528c02021-08-06 16:22:06 +0100324 macro.group("macro")))
Darryl Greend5802922018-05-08 15:30:59 +0100325
Yuto Takano39639672021-08-05 19:47:48 +0100326 return macros
Darryl Greend5802922018-05-08 15:30:59 +0100327
Yuto Takanod93fa372021-08-06 23:05:55 +0100328 def parse_mbed_words(self, files):
Yuto Takano39639672021-08-05 19:47:48 +0100329 """
Yuto Takanob47b5042021-08-07 00:42:54 +0100330 Parse all words in the file that begin with MBED, in and out of macros,
331 comments, anything.
Yuto Takano39639672021-08-05 19:47:48 +0100332
333 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100334 * files: a List of filepaths to look through.
335
336 Returns a List of Match objects for words beginning with MBED.
Yuto Takano39639672021-08-05 19:47:48 +0100337 """
Yuto Takanob47b5042021-08-07 00:42:54 +0100338 # Typos of TLS are common, hence the broader check below than MBEDTLS.
Yuto Takanod93fa372021-08-06 23:05:55 +0100339 mbed_regex = re.compile(r"\bMBED.+?_[A-Z0-9_]*")
340 exclusions = re.compile(r"// *no-check-names|#error")
341
Yuto Takano201f9e82021-08-06 16:36:54 +0100342 self.log.debug("Looking for MBED names in {} files".format(len(files)))
Yuto Takanod93fa372021-08-06 23:05:55 +0100343
344 mbed_words = []
345
Yuto Takanobb7dca42021-08-05 19:57:58 +0100346 for filename in files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100347 with open(filename, "r", encoding="utf-8") as fp:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100348 for line_no, line in enumerate(fp):
Yuto Takanod93fa372021-08-06 23:05:55 +0100349 if exclusions.search(line):
Yuto Takanoc62b4082021-08-05 20:17:07 +0100350 continue
Yuto Takano81528c02021-08-06 16:22:06 +0100351
Yuto Takanod93fa372021-08-06 23:05:55 +0100352 for name in mbed_regex.finditer(line):
353 mbed_words.append(Match(
Yuto Takano39639672021-08-05 19:47:48 +0100354 filename,
355 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100356 (line_no, name.start(), name.end()),
Yuto Takano39639672021-08-05 19:47:48 +0100357 name.group(0)
358 ))
359
Yuto Takanod93fa372021-08-06 23:05:55 +0100360 return mbed_words
Yuto Takano39639672021-08-05 19:47:48 +0100361
362 def parse_enum_consts(self, header_files):
363 """
364 Parse all enum value constants that are declared.
365
366 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100367 * header_files: A List of filepaths to look through.
Yuto Takano39639672021-08-05 19:47:48 +0100368
Yuto Takano81528c02021-08-06 16:22:06 +0100369 Returns a List of Match objects for the findings.
Yuto Takano39639672021-08-05 19:47:48 +0100370 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100371 self.log.debug("Looking for enum consts in {} files".format(len(header_files)))
Yuto Takano39639672021-08-05 19:47:48 +0100372
373 enum_consts = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100374
Yuto Takano39639672021-08-05 19:47:48 +0100375 for header_file in header_files:
376 # Emulate a finite state machine to parse enum declarations.
Yuto Takano81528c02021-08-06 16:22:06 +0100377 # 0 = not in enum
378 # 1 = inside enum
379 # 2 = almost inside enum
Darryl Greend5802922018-05-08 15:30:59 +0100380 state = 0
Yuto Takanoa083d152021-08-07 00:25:59 +0100381 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100382 for line_no, line in enumerate(header):
Yuto Takano13ecd992021-08-06 16:56:52 +0100383 # Match typedefs and brackets only when they are at the
384 # beginning of the line -- if they are indented, they might
385 # be sub-structures within structs, etc.
Yuto Takanod93fa372021-08-06 23:05:55 +0100386 if state == 0 and re.match(r"^(typedef +)?enum +{", line):
Darryl Greend5802922018-05-08 15:30:59 +0100387 state = 1
Yuto Takanod93fa372021-08-06 23:05:55 +0100388 elif state == 0 and re.match(r"^(typedef +)?enum", line):
Darryl Greend5802922018-05-08 15:30:59 +0100389 state = 2
Yuto Takanod93fa372021-08-06 23:05:55 +0100390 elif state == 2 and re.match(r"^{", line):
Darryl Greend5802922018-05-08 15:30:59 +0100391 state = 1
Yuto Takanod93fa372021-08-06 23:05:55 +0100392 elif state == 1 and re.match(r"^}", line):
Darryl Greend5802922018-05-08 15:30:59 +0100393 state = 0
Yuto Takanod93fa372021-08-06 23:05:55 +0100394 elif state == 1 and not re.match(r" *#", line):
Yuto Takano13ecd992021-08-06 16:56:52 +0100395 enum_const = re.match(r" *(?P<enum_const>\w+)", line)
Darryl Greend5802922018-05-08 15:30:59 +0100396 if enum_const:
Yuto Takano39639672021-08-05 19:47:48 +0100397 enum_consts.append(Match(
398 header_file,
399 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100400 (line_no, enum_const.start(), enum_const.end()),
Yuto Takano39639672021-08-05 19:47:48 +0100401 enum_const.group("enum_const")))
Yuto Takano81528c02021-08-06 16:22:06 +0100402
Yuto Takano39639672021-08-05 19:47:48 +0100403 return enum_consts
Darryl Greend5802922018-05-08 15:30:59 +0100404
Yuto Takano39639672021-08-05 19:47:48 +0100405 def parse_identifiers(self, header_files):
406 """
407 Parse all lines of a header where a function identifier is declared,
Yuto Takano81528c02021-08-06 16:22:06 +0100408 based on some huersitics. Highly dependent on formatting style.
Darryl Greend5802922018-05-08 15:30:59 +0100409
Yuto Takano39639672021-08-05 19:47:48 +0100410 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100411 * header_files: A List of filepaths to look through.
412
413 Returns a List of Match objects with identifiers.
Yuto Takano39639672021-08-05 19:47:48 +0100414 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100415 identifier_regex = re.compile(
416 # Match " something(a" or " *something(a". Functions.
417 # Assumptions:
418 # - function definition from return type to one of its arguments is
419 # all on one line (enforced by the previous_line concat below)
420 # - function definition line only contains alphanumeric, asterisk,
421 # underscore, and open bracket
422 r".* \**(\w+) *\( *\w|"
423 # Match "(*something)(". Flexible with spaces.
424 r".*\( *\* *(\w+) *\) *\(|"
425 # Match names of named data structures.
426 r"(?:typedef +)?(?:struct|union|enum) +(\w+)(?: *{)?$|"
427 # Match names of typedef instances, after closing bracket.
428 r"}? *(\w+)[;[].*")
429 exclusion_lines = re.compile(r"^("
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100430 r"extern +\"C\"|"
431 r"(typedef +)?(struct|union|enum)( *{)?$|"
432 r"} *;?$|"
433 r"$|"
434 r"//|"
435 r"#"
436 r")")
Yuto Takanod93fa372021-08-06 23:05:55 +0100437
438 self.log.debug("Looking for identifiers in {} files".format(len(header_files)))
Darryl Greend5802922018-05-08 15:30:59 +0100439
Yuto Takano39639672021-08-05 19:47:48 +0100440 identifiers = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100441
Yuto Takano39639672021-08-05 19:47:48 +0100442 for header_file in header_files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100443 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano39639672021-08-05 19:47:48 +0100444 in_block_comment = False
Yuto Takanod93fa372021-08-06 23:05:55 +0100445 previous_line = ""
Darryl Greend5802922018-05-08 15:30:59 +0100446
Yuto Takano8f457cf2021-08-06 17:54:58 +0100447 for line_no, line in enumerate(header):
Yuto Takano81528c02021-08-06 16:22:06 +0100448 # Skip parsing this line if a block comment ends on it,
449 # but don't skip if it has just started -- there is a chance
450 # it ends on the same line.
Yuto Takano39639672021-08-05 19:47:48 +0100451 if re.search(r"/\*", line):
Yuto Takano81528c02021-08-06 16:22:06 +0100452 in_block_comment = not in_block_comment
453 if re.search(r"\*/", line):
454 in_block_comment = not in_block_comment
Yuto Takano39639672021-08-05 19:47:48 +0100455 continue
456
Yuto Takano81528c02021-08-06 16:22:06 +0100457 if in_block_comment:
Yuto Takanod93fa372021-08-06 23:05:55 +0100458 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100459 continue
460
Yuto Takanod93fa372021-08-06 23:05:55 +0100461 if exclusion_lines.match(line):
462 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100463 continue
464
Yuto Takanocfc9e4a2021-08-06 20:02:32 +0100465 # If the line contains only space-separated alphanumeric
466 # characters (or underscore, asterisk, or, open bracket),
467 # and nothing else, high chance it's a declaration that
468 # continues on the next line
469 if re.match(r"^([\w\*\(]+\s+)+$", line):
Yuto Takanod93fa372021-08-06 23:05:55 +0100470 previous_line += line
Yuto Takano81528c02021-08-06 16:22:06 +0100471 continue
472
473 # If previous line seemed to start an unfinished declaration
Yuto Takanocfc9e4a2021-08-06 20:02:32 +0100474 # (as above), concat and treat them as one.
475 if previous_line:
476 line = previous_line.strip() + " " + line.strip()
Yuto Takanod93fa372021-08-06 23:05:55 +0100477 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100478
479 # Skip parsing if line has a space in front = hueristic to
480 # skip function argument lines (highly subject to formatting
481 # changes)
482 if line[0] == " ":
Yuto Takano39639672021-08-05 19:47:48 +0100483 continue
Yuto Takano6f38ab32021-08-05 21:07:14 +0100484
Yuto Takanod93fa372021-08-06 23:05:55 +0100485 identifier = identifier_regex.search(line)
Yuto Takano39639672021-08-05 19:47:48 +0100486
487 if identifier:
Yuto Takano81528c02021-08-06 16:22:06 +0100488 # Find the group that matched, and append it
Yuto Takano39639672021-08-05 19:47:48 +0100489 for group in identifier.groups():
490 if group:
491 identifiers.append(Match(
492 header_file,
493 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100494 (line_no, identifier.start(), identifier.end()),
Yuto Takano81528c02021-08-06 16:22:06 +0100495 group))
Yuto Takano39639672021-08-05 19:47:48 +0100496
497 return identifiers
498
499 def parse_symbols(self):
500 """
501 Compile the Mbed TLS libraries, and parse the TLS, Crypto, and x509
502 object files using nm to retrieve the list of referenced symbols.
Yuto Takano81528c02021-08-06 16:22:06 +0100503 Exceptions thrown here are rethrown because they would be critical
504 errors that void several tests, and thus needs to halt the program. This
505 is explicitly done for clarity.
Yuto Takano39639672021-08-05 19:47:48 +0100506
Yuto Takano81528c02021-08-06 16:22:06 +0100507 Returns a List of unique symbols defined and used in the libraries.
508 """
509 self.log.info("Compiling...")
Yuto Takano39639672021-08-05 19:47:48 +0100510 symbols = []
511
512 # Back up the config and atomically compile with the full configratuion.
513 shutil.copy("include/mbedtls/mbedtls_config.h",
Yuto Takano81528c02021-08-06 16:22:06 +0100514 "include/mbedtls/mbedtls_config.h.bak")
Darryl Greend5802922018-05-08 15:30:59 +0100515 try:
Yuto Takano81528c02021-08-06 16:22:06 +0100516 # Use check=True in all subprocess calls so that failures are raised
517 # as exceptions and logged.
Yuto Takano39639672021-08-05 19:47:48 +0100518 subprocess.run(
Yuto Takano81528c02021-08-06 16:22:06 +0100519 ["python3", "scripts/config.py", "full"],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100520 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100521 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100522 )
523 my_environment = os.environ.copy()
524 my_environment["CFLAGS"] = "-fno-asynchronous-unwind-tables"
Yuto Takano39639672021-08-05 19:47:48 +0100525 subprocess.run(
Darryl Greend5802922018-05-08 15:30:59 +0100526 ["make", "clean", "lib"],
527 env=my_environment,
Yuto Takanobcc3d992021-08-06 23:14:58 +0100528 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100529 stdout=subprocess.PIPE,
Darryl Greend5802922018-05-08 15:30:59 +0100530 stderr=subprocess.STDOUT,
Yuto Takano39639672021-08-05 19:47:48 +0100531 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100532 )
Yuto Takano39639672021-08-05 19:47:48 +0100533
534 # Perform object file analysis using nm
535 symbols = self.parse_symbols_from_nm(
536 ["library/libmbedcrypto.a",
Yuto Takanod93fa372021-08-06 23:05:55 +0100537 "library/libmbedtls.a",
538 "library/libmbedx509.a"])
Yuto Takano39639672021-08-05 19:47:48 +0100539
540 subprocess.run(
Darryl Greend5802922018-05-08 15:30:59 +0100541 ["make", "clean"],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100542 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100543 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100544 )
545 except subprocess.CalledProcessError as error:
Yuto Takano25eeb7b2021-08-06 21:27:59 +0100546 self.log.debug(error.output)
Darryl Greend5802922018-05-08 15:30:59 +0100547 self.set_return_code(2)
Yuto Takano81528c02021-08-06 16:22:06 +0100548 raise error
Yuto Takano39639672021-08-05 19:47:48 +0100549 finally:
Yuto Takano6fececf2021-08-07 17:28:23 +0100550 # Put back the original config regardless of there being errors.
551 # Works also for keyboard interrupts.
Yuto Takano39639672021-08-05 19:47:48 +0100552 shutil.move("include/mbedtls/mbedtls_config.h.bak",
553 "include/mbedtls/mbedtls_config.h")
554
555 return symbols
556
557 def parse_symbols_from_nm(self, object_files):
558 """
559 Run nm to retrieve the list of referenced symbols in each object file.
560 Does not return the position data since it is of no use.
561
Yuto Takano81528c02021-08-06 16:22:06 +0100562 Args:
563 * object_files: a List of compiled object files to search through.
564
565 Returns a List of unique symbols defined and used in any of the object
566 files.
Yuto Takano39639672021-08-05 19:47:48 +0100567 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100568 nm_undefined_regex = re.compile(r"^\S+: +U |^$|^\S+:$")
569 nm_valid_regex = re.compile(r"^\S+( [0-9A-Fa-f]+)* . _*(?P<symbol>\w+)")
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100570 exclusions = ("FStar", "Hacl")
Yuto Takano39639672021-08-05 19:47:48 +0100571
572 symbols = []
573
Yuto Takano81528c02021-08-06 16:22:06 +0100574 # Gather all outputs of nm
Yuto Takano39639672021-08-05 19:47:48 +0100575 nm_output = ""
576 for lib in object_files:
577 nm_output += subprocess.run(
578 ["nm", "-og", lib],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100579 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100580 stdout=subprocess.PIPE,
581 stderr=subprocess.STDOUT,
582 check=True
583 ).stdout
Yuto Takano81528c02021-08-06 16:22:06 +0100584
Yuto Takano39639672021-08-05 19:47:48 +0100585 for line in nm_output.splitlines():
Yuto Takanod93fa372021-08-06 23:05:55 +0100586 if not nm_undefined_regex.match(line):
587 symbol = nm_valid_regex.match(line)
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100588 if (symbol and not symbol.group("symbol").startswith(exclusions)):
Yuto Takanoe77f6992021-08-05 20:22:59 +0100589 symbols.append(symbol.group("symbol"))
Yuto Takano39639672021-08-05 19:47:48 +0100590 else:
591 self.log.error(line)
Yuto Takano81528c02021-08-06 16:22:06 +0100592
Yuto Takano39639672021-08-05 19:47:48 +0100593 return symbols
594
Yuto Takano55614b52021-08-07 01:00:18 +0100595 def perform_checks(self, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100596 """
597 Perform each check in order, output its PASS/FAIL status. Maintain an
598 overall test status, and output that at the end.
Yuto Takano977e07f2021-08-09 11:56:15 +0100599 Assumes parse_names_in_source() was called before this.
Yuto Takano81528c02021-08-06 16:22:06 +0100600
601 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100602 * quiet: whether to hide detailed problem explanation.
Yuto Takano39639672021-08-05 19:47:48 +0100603 """
Yuto Takano81528c02021-08-06 16:22:06 +0100604 self.log.info("=============")
Yuto Takano39639672021-08-05 19:47:48 +0100605 problems = 0
606
Yuto Takano55614b52021-08-07 01:00:18 +0100607 problems += self.check_symbols_declared_in_header(quiet)
Yuto Takano39639672021-08-05 19:47:48 +0100608
Yuto Takanod93fa372021-08-06 23:05:55 +0100609 pattern_checks = [("macros", MACRO_PATTERN),
610 ("enum_consts", CONSTANTS_PATTERN),
611 ("identifiers", IDENTIFIER_PATTERN)]
Yuto Takano39639672021-08-05 19:47:48 +0100612 for group, check_pattern in pattern_checks:
Yuto Takano55614b52021-08-07 01:00:18 +0100613 problems += self.check_match_pattern(quiet, group, check_pattern)
Yuto Takano39639672021-08-05 19:47:48 +0100614
Yuto Takano55614b52021-08-07 01:00:18 +0100615 problems += self.check_for_typos(quiet)
Yuto Takano39639672021-08-05 19:47:48 +0100616
617 self.log.info("=============")
618 if problems > 0:
619 self.log.info("FAIL: {0} problem(s) to fix".format(str(problems)))
Yuto Takano55614b52021-08-07 01:00:18 +0100620 if quiet:
621 self.log.info("Remove --quiet to see explanations.")
Yuto Takanofc54dfb2021-08-07 17:18:28 +0100622 else:
623 self.log.info("Use --quiet for minimal output.")
Yuto Takano39639672021-08-05 19:47:48 +0100624 else:
625 self.log.info("PASS")
Darryl Greend5802922018-05-08 15:30:59 +0100626
Yuto Takano55614b52021-08-07 01:00:18 +0100627 def check_symbols_declared_in_header(self, quiet):
Yuto Takano39639672021-08-05 19:47:48 +0100628 """
629 Perform a check that all detected symbols in the library object files
630 are properly declared in headers.
Yuto Takano977e07f2021-08-09 11:56:15 +0100631 Assumes parse_names_in_source() was called before this.
Darryl Greend5802922018-05-08 15:30:59 +0100632
Yuto Takano81528c02021-08-06 16:22:06 +0100633 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100634 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100635
636 Returns the number of problems that need fixing.
Yuto Takano39639672021-08-05 19:47:48 +0100637 """
638 problems = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100639
Yuto Takano39639672021-08-05 19:47:48 +0100640 for symbol in self.parse_result["symbols"]:
641 found_symbol_declared = False
642 for identifier_match in self.parse_result["identifiers"]:
643 if symbol == identifier_match.name:
644 found_symbol_declared = True
645 break
Yuto Takano81528c02021-08-06 16:22:06 +0100646
Yuto Takano39639672021-08-05 19:47:48 +0100647 if not found_symbol_declared:
Yuto Takano55614b52021-08-07 01:00:18 +0100648 problems.append(SymbolNotInHeader(symbol, quiet=quiet))
Yuto Takano39639672021-08-05 19:47:48 +0100649
Yuto Takano55614b52021-08-07 01:00:18 +0100650 self.output_check_result("All symbols in header", problems)
Yuto Takano39639672021-08-05 19:47:48 +0100651 return len(problems)
652
Yuto Takano81528c02021-08-06 16:22:06 +0100653
Yuto Takano55614b52021-08-07 01:00:18 +0100654 def check_match_pattern(self, quiet, group_to_check, check_pattern):
Yuto Takano81528c02021-08-06 16:22:06 +0100655 """
656 Perform a check that all items of a group conform to a regex pattern.
Yuto Takano977e07f2021-08-09 11:56:15 +0100657 Assumes parse_names_in_source() was called before this.
Yuto Takano81528c02021-08-06 16:22:06 +0100658
659 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100660 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100661 * group_to_check: string key to index into self.parse_result.
662 * check_pattern: the regex to check against.
663
664 Returns the number of problems that need fixing.
665 """
Yuto Takano39639672021-08-05 19:47:48 +0100666 problems = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100667
Yuto Takano39639672021-08-05 19:47:48 +0100668 for item_match in self.parse_result[group_to_check]:
669 if not re.match(check_pattern, item_match.name):
670 problems.append(PatternMismatch(check_pattern, item_match))
Yuto Takano201f9e82021-08-06 16:36:54 +0100671 # Double underscore is a reserved identifier, never to be used
Yuto Takanoc763cc32021-08-05 20:06:34 +0100672 if re.match(r".*__.*", item_match.name):
Yuto Takano55614b52021-08-07 01:00:18 +0100673 problems.append(PatternMismatch(
674 "double underscore",
675 item_match,
676 quiet=quiet))
Yuto Takano81528c02021-08-06 16:22:06 +0100677
678 self.output_check_result(
679 "Naming patterns of {}".format(group_to_check),
Yuto Takano55614b52021-08-07 01:00:18 +0100680 problems)
Yuto Takano39639672021-08-05 19:47:48 +0100681 return len(problems)
Darryl Greend5802922018-05-08 15:30:59 +0100682
Yuto Takano55614b52021-08-07 01:00:18 +0100683 def check_for_typos(self, quiet):
Yuto Takano81528c02021-08-06 16:22:06 +0100684 """
685 Perform a check that all words in the soure code beginning with MBED are
686 either defined as macros, or as enum constants.
Yuto Takano977e07f2021-08-09 11:56:15 +0100687 Assumes parse_names_in_source() was called before this.
Yuto Takano81528c02021-08-06 16:22:06 +0100688
689 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100690 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100691
692 Returns the number of problems that need fixing.
693 """
Yuto Takano39639672021-08-05 19:47:48 +0100694 problems = []
Yuto Takano39639672021-08-05 19:47:48 +0100695
Yuto Takanod93fa372021-08-06 23:05:55 +0100696 # Set comprehension, equivalent to a list comprehension inside set()
697 all_caps_names = {
698 match.name
699 for match
700 in self.parse_result["macros"] + self.parse_result["enum_consts"]}
701 typo_exclusion = re.compile(r"XXX|__|_$|^MBEDTLS_.*CONFIG_FILE$")
Yuto Takano39639672021-08-05 19:47:48 +0100702
Yuto Takanod93fa372021-08-06 23:05:55 +0100703 for name_match in self.parse_result["mbed_words"]:
Yuto Takano81528c02021-08-06 16:22:06 +0100704 found = name_match.name in all_caps_names
705
706 # Since MBEDTLS_PSA_ACCEL_XXX defines are defined by the
707 # PSA driver, they will not exist as macros. However, they
708 # should still be checked for typos using the equivalent
709 # BUILTINs that exist.
710 if "MBEDTLS_PSA_ACCEL_" in name_match.name:
711 found = name_match.name.replace(
712 "MBEDTLS_PSA_ACCEL_",
713 "MBEDTLS_PSA_BUILTIN_") in all_caps_names
714
Yuto Takanod93fa372021-08-06 23:05:55 +0100715 if not found and not typo_exclusion.search(name_match.name):
Yuto Takano55614b52021-08-07 01:00:18 +0100716 problems.append(Typo(name_match, quiet=quiet))
Yuto Takano39639672021-08-05 19:47:48 +0100717
Yuto Takano55614b52021-08-07 01:00:18 +0100718 self.output_check_result("Likely typos", problems)
Yuto Takano81528c02021-08-06 16:22:06 +0100719 return len(problems)
720
Yuto Takano55614b52021-08-07 01:00:18 +0100721 def output_check_result(self, name, problems):
Yuto Takano81528c02021-08-06 16:22:06 +0100722 """
723 Write out the PASS/FAIL status of a performed check depending on whether
724 there were problems.
Yuto Takano81528c02021-08-06 16:22:06 +0100725 """
Yuto Takano39639672021-08-05 19:47:48 +0100726 if problems:
Darryl Greend5802922018-05-08 15:30:59 +0100727 self.set_return_code(1)
Yuto Takano55614b52021-08-07 01:00:18 +0100728 self.log.info("{}: FAIL\n".format(name))
729 for problem in problems:
730 self.log.warning(str(problem))
Darryl Greend5802922018-05-08 15:30:59 +0100731 else:
Yuto Takano81528c02021-08-06 16:22:06 +0100732 self.log.info("{}: PASS".format(name))
Darryl Greend5802922018-05-08 15:30:59 +0100733
Yuto Takano39639672021-08-05 19:47:48 +0100734def main():
735 """
Yuto Takano81528c02021-08-06 16:22:06 +0100736 Perform argument parsing, and create an instance of NameCheck to begin the
737 core operation.
Yuto Takano39639672021-08-05 19:47:48 +0100738 """
Yuto Takano977e07f2021-08-09 11:56:15 +0100739 argparser = argparse.ArgumentParser(
Yuto Takano39639672021-08-05 19:47:48 +0100740 formatter_class=argparse.RawDescriptionHelpFormatter,
741 description=(
742 "This script confirms that the naming of all symbols and identifiers "
743 "in Mbed TLS are consistent with the house style and are also "
744 "self-consistent.\n\n"
745 "Expected to be run from the MbedTLS root directory."))
Darryl Greend5802922018-05-08 15:30:59 +0100746
Yuto Takano977e07f2021-08-09 11:56:15 +0100747 argparser.add_argument("-v", "--verbose",
Yuto Takano39639672021-08-05 19:47:48 +0100748 action="store_true",
Yuto Takano81528c02021-08-06 16:22:06 +0100749 help="show parse results")
750
Yuto Takano977e07f2021-08-09 11:56:15 +0100751 argparser.add_argument("-q", "--quiet",
Yuto Takano81528c02021-08-06 16:22:06 +0100752 action="store_true",
Yuto Takano55614b52021-08-07 01:00:18 +0100753 help="hide unnecessary text, explanations, and highlighs")
Yuto Takano81528c02021-08-06 16:22:06 +0100754
Yuto Takano977e07f2021-08-09 11:56:15 +0100755 args = argparser.parse_args()
Darryl Greend5802922018-05-08 15:30:59 +0100756
Darryl Greend5802922018-05-08 15:30:59 +0100757 try:
Yuto Takano977e07f2021-08-09 11:56:15 +0100758 name_check = NameCheck(verbose=args.verbose)
Yuto Takano39639672021-08-05 19:47:48 +0100759 name_check.parse_names_in_source()
Yuto Takano55614b52021-08-07 01:00:18 +0100760 name_check.perform_checks(quiet=args.quiet)
Yuto Takano81528c02021-08-06 16:22:06 +0100761 sys.exit(name_check.return_code)
Yuto Takanod93fa372021-08-06 23:05:55 +0100762 except Exception: # pylint: disable=broad-except
Darryl Greend5802922018-05-08 15:30:59 +0100763 traceback.print_exc()
764 sys.exit(2)
765
Darryl Greend5802922018-05-08 15:30:59 +0100766if __name__ == "__main__":
Yuto Takano39639672021-08-05 19:47:48 +0100767 main()