blob: 509b4353afec5d2268bd04f1d1112f45ec9aa984 [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.
Darryl Greend5802922018-05-08 15:30:59 +010030"""
Yuto Takano39639672021-08-05 19:47:48 +010031
32import argparse
33import textwrap
Darryl Greend5802922018-05-08 15:30:59 +010034import os
35import sys
36import traceback
37import re
38import shutil
39import subprocess
40import logging
41
Yuto Takano81528c02021-08-06 16:22:06 +010042# Naming patterns to check against. These are defined outside the NameCheck
43# class for ease of modification.
Yuto Takanobb7dca42021-08-05 19:57:58 +010044MACRO_PATTERN = r"^(MBEDTLS|PSA)_[0-9A-Z_]*[0-9A-Z]$"
Yuto Takano81528c02021-08-06 16:22:06 +010045CONSTANTS_PATTERN = MACRO_PATTERN
Yuto Takanoc1838932021-08-05 19:52:09 +010046IDENTIFIER_PATTERN = r"^(mbedtls|psa)_[0-9a-z_]*[0-9a-z]$"
Yuto Takano39639672021-08-05 19:47:48 +010047
Yuto Takanod93fa372021-08-06 23:05:55 +010048class Match(): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010049 """
50 A class representing a match, together with its found position.
51
52 Fields:
53 * filename: the file that the match was in.
54 * line: the full line containing the match.
Yuto Takanod93fa372021-08-06 23:05:55 +010055 * pos: a tuple of (line_no, start, end) positions on the file line where the
56 match is.
Yuto Takano81528c02021-08-06 16:22:06 +010057 * name: the match itself.
58 """
Yuto Takanod93fa372021-08-06 23:05:55 +010059 def __init__(self, filename, line, pos, name):
Yuto Takano39639672021-08-05 19:47:48 +010060 self.filename = filename
61 self.line = line
62 self.pos = pos
63 self.name = name
Yuto Takano39639672021-08-05 19:47:48 +010064
Yuto Takanoa4e75122021-08-06 17:23:28 +010065 def __str__(self):
Yuto Takano381fda82021-08-06 23:37:20 +010066 ln_str = str(self.pos[0])
67 gutter_len = max(4, len(ln_str))
68 gutter = (gutter_len - len(ln_str)) * " " + ln_str
69 underline = self.pos[1] * " " + (self.pos[2] - self.pos[1]) * "^"
70
Yuto Takanoa4e75122021-08-06 17:23:28 +010071 return (
Yuto Takano381fda82021-08-06 23:37:20 +010072 " {0} |\n".format(gutter_len * " ") +
73 " {0} | {1}".format(gutter, self.line) +
Yuto Takano55614b52021-08-07 01:00:18 +010074 " {0} | {1}\n".format(gutter_len * " ", underline)
Yuto Takanoa4e75122021-08-06 17:23:28 +010075 )
Yuto Takanod93fa372021-08-06 23:05:55 +010076
77class Problem(): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010078 """
79 A parent class representing a form of static analysis error.
80
81 Fields:
82 * textwrapper: a TextWrapper instance to format problems nicely.
83 """
Yuto Takano39639672021-08-05 19:47:48 +010084 def __init__(self):
85 self.textwrapper = textwrap.TextWrapper()
Yuto Takano81528c02021-08-06 16:22:06 +010086 self.textwrapper.width = 80
Yuto Takanoa4e75122021-08-06 17:23:28 +010087 self.textwrapper.initial_indent = " > "
Yuto Takano81528c02021-08-06 16:22:06 +010088 self.textwrapper.subsequent_indent = " "
Yuto Takano39639672021-08-05 19:47:48 +010089
Yuto Takanod93fa372021-08-06 23:05:55 +010090class SymbolNotInHeader(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010091 """
92 A problem that occurs when an exported/available symbol in the object file
93 is not explicitly declared in header files. Created with
94 NameCheck.check_symbols_declared_in_header()
95
96 Fields:
97 * symbol_name: the name of the symbol.
98 """
Yuto Takano55614b52021-08-07 01:00:18 +010099 def __init__(self, symbol_name, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100100 self.symbol_name = symbol_name
Yuto Takano55614b52021-08-07 01:00:18 +0100101 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100102 Problem.__init__(self)
103
104 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100105 if self.quiet:
106 return "{0}".format(self.symbol_name)
107
Yuto Takano39639672021-08-05 19:47:48 +0100108 return self.textwrapper.fill(
109 "'{0}' was found as an available symbol in the output of nm, "
110 "however it was not declared in any header files."
111 .format(self.symbol_name))
112
Yuto Takanod93fa372021-08-06 23:05:55 +0100113class PatternMismatch(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +0100114 """
115 A problem that occurs when something doesn't match the expected pattern.
116 Created with NameCheck.check_match_pattern()
117
118 Fields:
119 * pattern: the expected regex pattern
120 * match: the Match object in question
121 """
Yuto Takano55614b52021-08-07 01:00:18 +0100122 def __init__(self, pattern, match, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100123 self.pattern = pattern
124 self.match = match
Yuto Takano55614b52021-08-07 01:00:18 +0100125 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100126 Problem.__init__(self)
Yuto Takano81528c02021-08-06 16:22:06 +0100127
Yuto Takano39639672021-08-05 19:47:48 +0100128 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100129 if self.quiet:
130 return ("{0}:{1}:{3}"
131 .format(
132 self.match.filename,
133 self.match.pos[0],
134 self.match.name))
135
Yuto Takano39639672021-08-05 19:47:48 +0100136 return self.textwrapper.fill(
Yuto Takanoa4e75122021-08-06 17:23:28 +0100137 "{0}:{1}: '{2}' does not match the required pattern '{3}'."
138 .format(
139 self.match.filename,
Yuto Takanod93fa372021-08-06 23:05:55 +0100140 self.match.pos[0],
Yuto Takanoa4e75122021-08-06 17:23:28 +0100141 self.match.name,
142 self.pattern)) + "\n" + str(self.match)
Yuto Takano39639672021-08-05 19:47:48 +0100143
Yuto Takanod93fa372021-08-06 23:05:55 +0100144class Typo(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +0100145 """
146 A problem that occurs when a word using MBED doesn't appear to be defined as
147 constants nor enum values. Created with NameCheck.check_for_typos()
148
149 Fields:
150 * match: the Match object of the MBED name in question.
151 """
Yuto Takano55614b52021-08-07 01:00:18 +0100152 def __init__(self, match, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100153 self.match = match
Yuto Takano55614b52021-08-07 01:00:18 +0100154 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100155 Problem.__init__(self)
Yuto Takano81528c02021-08-06 16:22:06 +0100156
Yuto Takano39639672021-08-05 19:47:48 +0100157 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100158 if self.quiet:
159 return ("{0}:{1}:{2}"
160 .format(
161 self.match.filename,
162 self.match.pos[0],
163 self.match.name))
164
Yuto Takano39639672021-08-05 19:47:48 +0100165 return self.textwrapper.fill(
Yuto Takanoa4e75122021-08-06 17:23:28 +0100166 "{0}:{1}: '{2}' looks like a typo. It was not found in any "
167 "macros or any enums. If this is not a typo, put "
168 "//no-check-names after it."
169 .format(
170 self.match.filename,
Yuto Takanod93fa372021-08-06 23:05:55 +0100171 self.match.pos[0],
Yuto Takanoa4e75122021-08-06 17:23:28 +0100172 self.match.name)) + "\n" + str(self.match)
Darryl Greend5802922018-05-08 15:30:59 +0100173
Yuto Takanod93fa372021-08-06 23:05:55 +0100174class NameCheck():
Yuto Takano81528c02021-08-06 16:22:06 +0100175 """
176 Representation of the core name checking operation performed by this script.
177 Shares a common logger, common excluded filenames, and a shared return_code.
178 """
Darryl Greend5802922018-05-08 15:30:59 +0100179 def __init__(self):
180 self.log = None
Darryl Greend5802922018-05-08 15:30:59 +0100181 self.return_code = 0
Yuto Takano81528c02021-08-06 16:22:06 +0100182 self.excluded_files = ["bn_mul", "compat-2.x.h"]
Yuto Takanod93fa372021-08-06 23:05:55 +0100183 self.parse_result = {}
Darryl Greend5802922018-05-08 15:30:59 +0100184
185 def set_return_code(self, return_code):
186 if return_code > self.return_code:
Yuto Takano201f9e82021-08-06 16:36:54 +0100187 self.log.debug("Setting new return code to {}".format(return_code))
Darryl Greend5802922018-05-08 15:30:59 +0100188 self.return_code = return_code
189
Yuto Takano39639672021-08-05 19:47:48 +0100190 def setup_logger(self, verbose=False):
191 """
192 Set up a logger and set the change the default logging level from
Yuto Takano81528c02021-08-06 16:22:06 +0100193 WARNING to INFO. Loggers are better than print statements since their
Yuto Takano39639672021-08-05 19:47:48 +0100194 verbosity can be controlled.
195 """
Darryl Greend5802922018-05-08 15:30:59 +0100196 self.log = logging.getLogger()
Yuto Takano39639672021-08-05 19:47:48 +0100197 if verbose:
198 self.log.setLevel(logging.DEBUG)
199 else:
200 self.log.setLevel(logging.INFO)
Darryl Greend5802922018-05-08 15:30:59 +0100201 self.log.addHandler(logging.StreamHandler())
202
Yuto Takano157444c2021-08-05 20:10:45 +0100203 def get_files(self, extension, directory):
Yuto Takano81528c02021-08-06 16:22:06 +0100204 """
205 Get all files that end with .extension in the specified directory
206 recursively.
207
208 Args:
209 * extension: the file extension to search for, without the dot
210 * directory: the directory to recursively search for
211
212 Returns a List of relative filepaths.
213 """
Darryl Greend5802922018-05-08 15:30:59 +0100214 filenames = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100215 for root, _, files in sorted(os.walk(directory)):
Darryl Greend5802922018-05-08 15:30:59 +0100216 for filename in sorted(files):
217 if (filename not in self.excluded_files and
Yuto Takano157444c2021-08-05 20:10:45 +0100218 filename.endswith("." + extension)):
Darryl Greend5802922018-05-08 15:30:59 +0100219 filenames.append(os.path.join(root, filename))
220 return filenames
221
Yuto Takano81528c02021-08-06 16:22:06 +0100222 def parse_names_in_source(self):
223 """
224 Calls each parsing function to retrieve various elements of the code,
225 together with their source location. Puts the parsed values in the
226 internal variable self.parse_result.
227 """
228 self.log.info("Parsing source code...")
Yuto Takanod24e0372021-08-06 16:42:33 +0100229 self.log.debug(
230 "The following files are excluded from the search: {}"
231 .format(str(self.excluded_files))
232 )
Yuto Takano81528c02021-08-06 16:22:06 +0100233
234 m_headers = self.get_files("h", os.path.join("include", "mbedtls"))
235 p_headers = self.get_files("h", os.path.join("include", "psa"))
236 t_headers = ["3rdparty/everest/include/everest/everest.h",
237 "3rdparty/everest/include/everest/x25519.h"]
238 d_headers = self.get_files("h", os.path.join("tests", "include", "test", "drivers"))
239 l_headers = self.get_files("h", "library")
240 libraries = self.get_files("c", "library") + [
241 "3rdparty/everest/library/everest.c",
242 "3rdparty/everest/library/x25519.c"]
243
244 all_macros = self.parse_macros(
245 m_headers + p_headers + t_headers + l_headers + d_headers)
246 enum_consts = self.parse_enum_consts(
247 m_headers + l_headers + t_headers)
248 identifiers = self.parse_identifiers(
249 m_headers + p_headers + t_headers + l_headers)
Yuto Takanod93fa372021-08-06 23:05:55 +0100250 mbed_words = self.parse_mbed_words(
Yuto Takano81528c02021-08-06 16:22:06 +0100251 m_headers + p_headers + t_headers + l_headers + libraries)
252 symbols = self.parse_symbols()
253
254 # Remove identifier macros like mbedtls_printf or mbedtls_calloc
255 identifiers_justname = [x.name for x in identifiers]
256 actual_macros = []
257 for macro in all_macros:
258 if macro.name not in identifiers_justname:
259 actual_macros.append(macro)
260
261 self.log.debug("Found:")
262 self.log.debug(" {} Macros".format(len(all_macros)))
263 self.log.debug(" {} Non-identifier Macros".format(len(actual_macros)))
264 self.log.debug(" {} Enum Constants".format(len(enum_consts)))
265 self.log.debug(" {} Identifiers".format(len(identifiers)))
266 self.log.debug(" {} Exported Symbols".format(len(symbols)))
267 self.log.info("Analysing...")
268
269 self.parse_result = {
270 "macros": actual_macros,
271 "enum_consts": enum_consts,
272 "identifiers": identifiers,
273 "symbols": symbols,
Yuto Takanod93fa372021-08-06 23:05:55 +0100274 "mbed_words": mbed_words
Yuto Takano81528c02021-08-06 16:22:06 +0100275 }
276
Yuto Takano39639672021-08-05 19:47:48 +0100277 def parse_macros(self, header_files):
278 """
279 Parse all macros defined by #define preprocessor directives.
280
281 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100282 * header_files: A List of filepaths to look through.
283
284 Returns a List of Match objects for the found macros.
Yuto Takano39639672021-08-05 19:47:48 +0100285 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100286 macro_regex = re.compile(r"# *define +(?P<macro>\w+)")
287 exclusions = (
Yuto Takano39639672021-08-05 19:47:48 +0100288 "asm", "inline", "EMIT", "_CRT_SECURE_NO_DEPRECATE", "MULADDC_"
289 )
290
Yuto Takano201f9e82021-08-06 16:36:54 +0100291 self.log.debug("Looking for macros in {} files".format(len(header_files)))
Yuto Takanod93fa372021-08-06 23:05:55 +0100292
293 macros = []
294
Yuto Takano39639672021-08-05 19:47:48 +0100295 for header_file in header_files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100296 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100297 for line_no, line in enumerate(header):
Yuto Takanod93fa372021-08-06 23:05:55 +0100298 for macro in macro_regex.finditer(line):
299 if not macro.group("macro").startswith(exclusions):
Yuto Takano81528c02021-08-06 16:22:06 +0100300 macros.append(Match(
301 header_file,
302 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100303 (line_no, macro.start(), macro.end()),
Yuto Takano81528c02021-08-06 16:22:06 +0100304 macro.group("macro")))
Darryl Greend5802922018-05-08 15:30:59 +0100305
Yuto Takano39639672021-08-05 19:47:48 +0100306 return macros
Darryl Greend5802922018-05-08 15:30:59 +0100307
Yuto Takanod93fa372021-08-06 23:05:55 +0100308 def parse_mbed_words(self, files):
Yuto Takano39639672021-08-05 19:47:48 +0100309 """
Yuto Takanob47b5042021-08-07 00:42:54 +0100310 Parse all words in the file that begin with MBED, in and out of macros,
311 comments, anything.
Yuto Takano39639672021-08-05 19:47:48 +0100312
313 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100314 * files: a List of filepaths to look through.
315
316 Returns a List of Match objects for words beginning with MBED.
Yuto Takano39639672021-08-05 19:47:48 +0100317 """
Yuto Takanob47b5042021-08-07 00:42:54 +0100318 # Typos of TLS are common, hence the broader check below than MBEDTLS.
Yuto Takanod93fa372021-08-06 23:05:55 +0100319 mbed_regex = re.compile(r"\bMBED.+?_[A-Z0-9_]*")
320 exclusions = re.compile(r"// *no-check-names|#error")
321
Yuto Takano201f9e82021-08-06 16:36:54 +0100322 self.log.debug("Looking for MBED names in {} files".format(len(files)))
Yuto Takanod93fa372021-08-06 23:05:55 +0100323
324 mbed_words = []
325
Yuto Takanobb7dca42021-08-05 19:57:58 +0100326 for filename in files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100327 with open(filename, "r", encoding="utf-8") as fp:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100328 for line_no, line in enumerate(fp):
Yuto Takanod93fa372021-08-06 23:05:55 +0100329 if exclusions.search(line):
Yuto Takanoc62b4082021-08-05 20:17:07 +0100330 continue
Yuto Takano81528c02021-08-06 16:22:06 +0100331
Yuto Takanod93fa372021-08-06 23:05:55 +0100332 for name in mbed_regex.finditer(line):
333 mbed_words.append(Match(
Yuto Takano39639672021-08-05 19:47:48 +0100334 filename,
335 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100336 (line_no, name.start(), name.end()),
Yuto Takano39639672021-08-05 19:47:48 +0100337 name.group(0)
338 ))
339
Yuto Takanod93fa372021-08-06 23:05:55 +0100340 return mbed_words
Yuto Takano39639672021-08-05 19:47:48 +0100341
342 def parse_enum_consts(self, header_files):
343 """
344 Parse all enum value constants that are declared.
345
346 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100347 * header_files: A List of filepaths to look through.
Yuto Takano39639672021-08-05 19:47:48 +0100348
Yuto Takano81528c02021-08-06 16:22:06 +0100349 Returns a List of Match objects for the findings.
Yuto Takano39639672021-08-05 19:47:48 +0100350 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100351 self.log.debug("Looking for enum consts in {} files".format(len(header_files)))
Yuto Takano39639672021-08-05 19:47:48 +0100352
353 enum_consts = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100354
Yuto Takano39639672021-08-05 19:47:48 +0100355 for header_file in header_files:
356 # Emulate a finite state machine to parse enum declarations.
Yuto Takano81528c02021-08-06 16:22:06 +0100357 # 0 = not in enum
358 # 1 = inside enum
359 # 2 = almost inside enum
Darryl Greend5802922018-05-08 15:30:59 +0100360 state = 0
Yuto Takanoa083d152021-08-07 00:25:59 +0100361 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100362 for line_no, line in enumerate(header):
Yuto Takano13ecd992021-08-06 16:56:52 +0100363 # Match typedefs and brackets only when they are at the
364 # beginning of the line -- if they are indented, they might
365 # be sub-structures within structs, etc.
Yuto Takanod93fa372021-08-06 23:05:55 +0100366 if state == 0 and re.match(r"^(typedef +)?enum +{", line):
Darryl Greend5802922018-05-08 15:30:59 +0100367 state = 1
Yuto Takanod93fa372021-08-06 23:05:55 +0100368 elif state == 0 and re.match(r"^(typedef +)?enum", line):
Darryl Greend5802922018-05-08 15:30:59 +0100369 state = 2
Yuto Takanod93fa372021-08-06 23:05:55 +0100370 elif state == 2 and re.match(r"^{", line):
Darryl Greend5802922018-05-08 15:30:59 +0100371 state = 1
Yuto Takanod93fa372021-08-06 23:05:55 +0100372 elif state == 1 and re.match(r"^}", line):
Darryl Greend5802922018-05-08 15:30:59 +0100373 state = 0
Yuto Takanod93fa372021-08-06 23:05:55 +0100374 elif state == 1 and not re.match(r" *#", line):
Yuto Takano13ecd992021-08-06 16:56:52 +0100375 enum_const = re.match(r" *(?P<enum_const>\w+)", line)
Darryl Greend5802922018-05-08 15:30:59 +0100376 if enum_const:
Yuto Takano39639672021-08-05 19:47:48 +0100377 enum_consts.append(Match(
378 header_file,
379 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100380 (line_no, enum_const.start(), enum_const.end()),
Yuto Takano39639672021-08-05 19:47:48 +0100381 enum_const.group("enum_const")))
Yuto Takano81528c02021-08-06 16:22:06 +0100382
Yuto Takano39639672021-08-05 19:47:48 +0100383 return enum_consts
Darryl Greend5802922018-05-08 15:30:59 +0100384
Yuto Takano39639672021-08-05 19:47:48 +0100385 def parse_identifiers(self, header_files):
386 """
387 Parse all lines of a header where a function identifier is declared,
Yuto Takano81528c02021-08-06 16:22:06 +0100388 based on some huersitics. Highly dependent on formatting style.
Darryl Greend5802922018-05-08 15:30:59 +0100389
Yuto Takano39639672021-08-05 19:47:48 +0100390 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100391 * header_files: A List of filepaths to look through.
392
393 Returns a List of Match objects with identifiers.
Yuto Takano39639672021-08-05 19:47:48 +0100394 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100395 identifier_regex = re.compile(
396 # Match " something(a" or " *something(a". Functions.
397 # Assumptions:
398 # - function definition from return type to one of its arguments is
399 # all on one line (enforced by the previous_line concat below)
400 # - function definition line only contains alphanumeric, asterisk,
401 # underscore, and open bracket
402 r".* \**(\w+) *\( *\w|"
403 # Match "(*something)(". Flexible with spaces.
404 r".*\( *\* *(\w+) *\) *\(|"
405 # Match names of named data structures.
406 r"(?:typedef +)?(?:struct|union|enum) +(\w+)(?: *{)?$|"
407 # Match names of typedef instances, after closing bracket.
408 r"}? *(\w+)[;[].*")
409 exclusion_lines = re.compile(r"^("
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100410 r"extern +\"C\"|"
411 r"(typedef +)?(struct|union|enum)( *{)?$|"
412 r"} *;?$|"
413 r"$|"
414 r"//|"
415 r"#"
416 r")")
Yuto Takanod93fa372021-08-06 23:05:55 +0100417
418 self.log.debug("Looking for identifiers in {} files".format(len(header_files)))
Darryl Greend5802922018-05-08 15:30:59 +0100419
Yuto Takano39639672021-08-05 19:47:48 +0100420 identifiers = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100421
Yuto Takano39639672021-08-05 19:47:48 +0100422 for header_file in header_files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100423 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano39639672021-08-05 19:47:48 +0100424 in_block_comment = False
Yuto Takanod93fa372021-08-06 23:05:55 +0100425 previous_line = ""
Darryl Greend5802922018-05-08 15:30:59 +0100426
Yuto Takano8f457cf2021-08-06 17:54:58 +0100427 for line_no, line in enumerate(header):
Yuto Takano81528c02021-08-06 16:22:06 +0100428 # Skip parsing this line if a block comment ends on it,
429 # but don't skip if it has just started -- there is a chance
430 # it ends on the same line.
Yuto Takano39639672021-08-05 19:47:48 +0100431 if re.search(r"/\*", line):
Yuto Takano81528c02021-08-06 16:22:06 +0100432 in_block_comment = not in_block_comment
433 if re.search(r"\*/", line):
434 in_block_comment = not in_block_comment
Yuto Takano39639672021-08-05 19:47:48 +0100435 continue
436
Yuto Takano81528c02021-08-06 16:22:06 +0100437 if in_block_comment:
Yuto Takanod93fa372021-08-06 23:05:55 +0100438 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100439 continue
440
Yuto Takanod93fa372021-08-06 23:05:55 +0100441 if exclusion_lines.match(line):
442 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100443 continue
444
Yuto Takanocfc9e4a2021-08-06 20:02:32 +0100445 # If the line contains only space-separated alphanumeric
446 # characters (or underscore, asterisk, or, open bracket),
447 # and nothing else, high chance it's a declaration that
448 # continues on the next line
449 if re.match(r"^([\w\*\(]+\s+)+$", line):
Yuto Takanod93fa372021-08-06 23:05:55 +0100450 previous_line += line
Yuto Takano81528c02021-08-06 16:22:06 +0100451 continue
452
453 # If previous line seemed to start an unfinished declaration
Yuto Takanocfc9e4a2021-08-06 20:02:32 +0100454 # (as above), concat and treat them as one.
455 if previous_line:
456 line = previous_line.strip() + " " + line.strip()
Yuto Takanod93fa372021-08-06 23:05:55 +0100457 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100458
459 # Skip parsing if line has a space in front = hueristic to
460 # skip function argument lines (highly subject to formatting
461 # changes)
462 if line[0] == " ":
Yuto Takano39639672021-08-05 19:47:48 +0100463 continue
Yuto Takano6f38ab32021-08-05 21:07:14 +0100464
Yuto Takanod93fa372021-08-06 23:05:55 +0100465 identifier = identifier_regex.search(line)
Yuto Takano39639672021-08-05 19:47:48 +0100466
467 if identifier:
Yuto Takano81528c02021-08-06 16:22:06 +0100468 # Find the group that matched, and append it
Yuto Takano39639672021-08-05 19:47:48 +0100469 for group in identifier.groups():
470 if group:
471 identifiers.append(Match(
472 header_file,
473 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100474 (line_no, identifier.start(), identifier.end()),
Yuto Takano81528c02021-08-06 16:22:06 +0100475 group))
Yuto Takano39639672021-08-05 19:47:48 +0100476
477 return identifiers
478
479 def parse_symbols(self):
480 """
481 Compile the Mbed TLS libraries, and parse the TLS, Crypto, and x509
482 object files using nm to retrieve the list of referenced symbols.
Yuto Takano81528c02021-08-06 16:22:06 +0100483 Exceptions thrown here are rethrown because they would be critical
484 errors that void several tests, and thus needs to halt the program. This
485 is explicitly done for clarity.
Yuto Takano39639672021-08-05 19:47:48 +0100486
Yuto Takano81528c02021-08-06 16:22:06 +0100487 Returns a List of unique symbols defined and used in the libraries.
488 """
489 self.log.info("Compiling...")
Yuto Takano39639672021-08-05 19:47:48 +0100490 symbols = []
491
492 # Back up the config and atomically compile with the full configratuion.
493 shutil.copy("include/mbedtls/mbedtls_config.h",
Yuto Takano81528c02021-08-06 16:22:06 +0100494 "include/mbedtls/mbedtls_config.h.bak")
Darryl Greend5802922018-05-08 15:30:59 +0100495 try:
Yuto Takano81528c02021-08-06 16:22:06 +0100496 # Use check=True in all subprocess calls so that failures are raised
497 # as exceptions and logged.
Yuto Takano39639672021-08-05 19:47:48 +0100498 subprocess.run(
Yuto Takano81528c02021-08-06 16:22:06 +0100499 ["python3", "scripts/config.py", "full"],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100500 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100501 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100502 )
503 my_environment = os.environ.copy()
504 my_environment["CFLAGS"] = "-fno-asynchronous-unwind-tables"
Yuto Takano39639672021-08-05 19:47:48 +0100505 subprocess.run(
Darryl Greend5802922018-05-08 15:30:59 +0100506 ["make", "clean", "lib"],
507 env=my_environment,
Yuto Takanobcc3d992021-08-06 23:14:58 +0100508 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100509 stdout=subprocess.PIPE,
Darryl Greend5802922018-05-08 15:30:59 +0100510 stderr=subprocess.STDOUT,
Yuto Takano39639672021-08-05 19:47:48 +0100511 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100512 )
Yuto Takano39639672021-08-05 19:47:48 +0100513
514 # Perform object file analysis using nm
515 symbols = self.parse_symbols_from_nm(
516 ["library/libmbedcrypto.a",
Yuto Takanod93fa372021-08-06 23:05:55 +0100517 "library/libmbedtls.a",
518 "library/libmbedx509.a"])
Yuto Takano39639672021-08-05 19:47:48 +0100519
520 subprocess.run(
Darryl Greend5802922018-05-08 15:30:59 +0100521 ["make", "clean"],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100522 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100523 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100524 )
525 except subprocess.CalledProcessError as error:
Yuto Takano25eeb7b2021-08-06 21:27:59 +0100526 self.log.debug(error.output)
Darryl Greend5802922018-05-08 15:30:59 +0100527 self.set_return_code(2)
Yuto Takano81528c02021-08-06 16:22:06 +0100528 raise error
Yuto Takano39639672021-08-05 19:47:48 +0100529 finally:
530 shutil.move("include/mbedtls/mbedtls_config.h.bak",
531 "include/mbedtls/mbedtls_config.h")
532
533 return symbols
534
535 def parse_symbols_from_nm(self, object_files):
536 """
537 Run nm to retrieve the list of referenced symbols in each object file.
538 Does not return the position data since it is of no use.
539
Yuto Takano81528c02021-08-06 16:22:06 +0100540 Args:
541 * object_files: a List of compiled object files to search through.
542
543 Returns a List of unique symbols defined and used in any of the object
544 files.
Yuto Takano39639672021-08-05 19:47:48 +0100545 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100546 nm_undefined_regex = re.compile(r"^\S+: +U |^$|^\S+:$")
547 nm_valid_regex = re.compile(r"^\S+( [0-9A-Fa-f]+)* . _*(?P<symbol>\w+)")
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100548 exclusions = ("FStar", "Hacl")
Yuto Takano39639672021-08-05 19:47:48 +0100549
550 symbols = []
551
Yuto Takano81528c02021-08-06 16:22:06 +0100552 # Gather all outputs of nm
Yuto Takano39639672021-08-05 19:47:48 +0100553 nm_output = ""
554 for lib in object_files:
555 nm_output += subprocess.run(
556 ["nm", "-og", lib],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100557 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100558 stdout=subprocess.PIPE,
559 stderr=subprocess.STDOUT,
560 check=True
561 ).stdout
Yuto Takano81528c02021-08-06 16:22:06 +0100562
Yuto Takano39639672021-08-05 19:47:48 +0100563 for line in nm_output.splitlines():
Yuto Takanod93fa372021-08-06 23:05:55 +0100564 if not nm_undefined_regex.match(line):
565 symbol = nm_valid_regex.match(line)
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100566 if (symbol and not symbol.group("symbol").startswith(exclusions)):
Yuto Takanoe77f6992021-08-05 20:22:59 +0100567 symbols.append(symbol.group("symbol"))
Yuto Takano39639672021-08-05 19:47:48 +0100568 else:
569 self.log.error(line)
Yuto Takano81528c02021-08-06 16:22:06 +0100570
Yuto Takano39639672021-08-05 19:47:48 +0100571 return symbols
572
Yuto Takano55614b52021-08-07 01:00:18 +0100573 def perform_checks(self, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100574 """
575 Perform each check in order, output its PASS/FAIL status. Maintain an
576 overall test status, and output that at the end.
Yuto Takano81528c02021-08-06 16:22:06 +0100577
578 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100579 * quiet: whether to hide detailed problem explanation.
Yuto Takano39639672021-08-05 19:47:48 +0100580 """
Yuto Takano81528c02021-08-06 16:22:06 +0100581 self.log.info("=============")
Yuto Takano39639672021-08-05 19:47:48 +0100582 problems = 0
583
Yuto Takano55614b52021-08-07 01:00:18 +0100584 problems += self.check_symbols_declared_in_header(quiet)
Yuto Takano39639672021-08-05 19:47:48 +0100585
Yuto Takanod93fa372021-08-06 23:05:55 +0100586 pattern_checks = [("macros", MACRO_PATTERN),
587 ("enum_consts", CONSTANTS_PATTERN),
588 ("identifiers", IDENTIFIER_PATTERN)]
Yuto Takano39639672021-08-05 19:47:48 +0100589 for group, check_pattern in pattern_checks:
Yuto Takano55614b52021-08-07 01:00:18 +0100590 problems += self.check_match_pattern(quiet, group, check_pattern)
Yuto Takano39639672021-08-05 19:47:48 +0100591
Yuto Takano55614b52021-08-07 01:00:18 +0100592 problems += self.check_for_typos(quiet)
Yuto Takano39639672021-08-05 19:47:48 +0100593
594 self.log.info("=============")
595 if problems > 0:
596 self.log.info("FAIL: {0} problem(s) to fix".format(str(problems)))
Yuto Takano55614b52021-08-07 01:00:18 +0100597 if quiet:
598 self.log.info("Remove --quiet to see explanations.")
Yuto Takano39639672021-08-05 19:47:48 +0100599 else:
600 self.log.info("PASS")
Darryl Greend5802922018-05-08 15:30:59 +0100601
Yuto Takano55614b52021-08-07 01:00:18 +0100602 def check_symbols_declared_in_header(self, quiet):
Yuto Takano39639672021-08-05 19:47:48 +0100603 """
604 Perform a check that all detected symbols in the library object files
605 are properly declared in headers.
Darryl Greend5802922018-05-08 15:30:59 +0100606
Yuto Takano81528c02021-08-06 16:22:06 +0100607 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100608 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100609
610 Returns the number of problems that need fixing.
Yuto Takano39639672021-08-05 19:47:48 +0100611 """
612 problems = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100613
Yuto Takano39639672021-08-05 19:47:48 +0100614 for symbol in self.parse_result["symbols"]:
615 found_symbol_declared = False
616 for identifier_match in self.parse_result["identifiers"]:
617 if symbol == identifier_match.name:
618 found_symbol_declared = True
619 break
Yuto Takano81528c02021-08-06 16:22:06 +0100620
Yuto Takano39639672021-08-05 19:47:48 +0100621 if not found_symbol_declared:
Yuto Takano55614b52021-08-07 01:00:18 +0100622 problems.append(SymbolNotInHeader(symbol, quiet=quiet))
Yuto Takano39639672021-08-05 19:47:48 +0100623
Yuto Takano55614b52021-08-07 01:00:18 +0100624 self.output_check_result("All symbols in header", problems)
Yuto Takano39639672021-08-05 19:47:48 +0100625 return len(problems)
626
Yuto Takano81528c02021-08-06 16:22:06 +0100627
Yuto Takano55614b52021-08-07 01:00:18 +0100628 def check_match_pattern(self, quiet, group_to_check, check_pattern):
Yuto Takano81528c02021-08-06 16:22:06 +0100629 """
630 Perform a check that all items of a group conform to a regex pattern.
631
632 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100633 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100634 * group_to_check: string key to index into self.parse_result.
635 * check_pattern: the regex to check against.
636
637 Returns the number of problems that need fixing.
638 """
Yuto Takano39639672021-08-05 19:47:48 +0100639 problems = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100640
Yuto Takano39639672021-08-05 19:47:48 +0100641 for item_match in self.parse_result[group_to_check]:
642 if not re.match(check_pattern, item_match.name):
643 problems.append(PatternMismatch(check_pattern, item_match))
Yuto Takano201f9e82021-08-06 16:36:54 +0100644 # Double underscore is a reserved identifier, never to be used
Yuto Takanoc763cc32021-08-05 20:06:34 +0100645 if re.match(r".*__.*", item_match.name):
Yuto Takano55614b52021-08-07 01:00:18 +0100646 problems.append(PatternMismatch(
647 "double underscore",
648 item_match,
649 quiet=quiet))
Yuto Takano81528c02021-08-06 16:22:06 +0100650
651 self.output_check_result(
652 "Naming patterns of {}".format(group_to_check),
Yuto Takano55614b52021-08-07 01:00:18 +0100653 problems)
Yuto Takano39639672021-08-05 19:47:48 +0100654 return len(problems)
Darryl Greend5802922018-05-08 15:30:59 +0100655
Yuto Takano55614b52021-08-07 01:00:18 +0100656 def check_for_typos(self, quiet):
Yuto Takano81528c02021-08-06 16:22:06 +0100657 """
658 Perform a check that all words in the soure code beginning with MBED are
659 either defined as macros, or as enum constants.
660
661 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100662 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100663
664 Returns the number of problems that need fixing.
665 """
Yuto Takano39639672021-08-05 19:47:48 +0100666 problems = []
Yuto Takano39639672021-08-05 19:47:48 +0100667
Yuto Takanod93fa372021-08-06 23:05:55 +0100668 # Set comprehension, equivalent to a list comprehension inside set()
669 all_caps_names = {
670 match.name
671 for match
672 in self.parse_result["macros"] + self.parse_result["enum_consts"]}
673 typo_exclusion = re.compile(r"XXX|__|_$|^MBEDTLS_.*CONFIG_FILE$")
Yuto Takano39639672021-08-05 19:47:48 +0100674
Yuto Takanod93fa372021-08-06 23:05:55 +0100675 for name_match in self.parse_result["mbed_words"]:
Yuto Takano81528c02021-08-06 16:22:06 +0100676 found = name_match.name in all_caps_names
677
678 # Since MBEDTLS_PSA_ACCEL_XXX defines are defined by the
679 # PSA driver, they will not exist as macros. However, they
680 # should still be checked for typos using the equivalent
681 # BUILTINs that exist.
682 if "MBEDTLS_PSA_ACCEL_" in name_match.name:
683 found = name_match.name.replace(
684 "MBEDTLS_PSA_ACCEL_",
685 "MBEDTLS_PSA_BUILTIN_") in all_caps_names
686
Yuto Takanod93fa372021-08-06 23:05:55 +0100687 if not found and not typo_exclusion.search(name_match.name):
Yuto Takano55614b52021-08-07 01:00:18 +0100688 problems.append(Typo(name_match, quiet=quiet))
Yuto Takano39639672021-08-05 19:47:48 +0100689
Yuto Takano55614b52021-08-07 01:00:18 +0100690 self.output_check_result("Likely typos", problems)
Yuto Takano81528c02021-08-06 16:22:06 +0100691 return len(problems)
692
Yuto Takano55614b52021-08-07 01:00:18 +0100693 def output_check_result(self, name, problems):
Yuto Takano81528c02021-08-06 16:22:06 +0100694 """
695 Write out the PASS/FAIL status of a performed check depending on whether
696 there were problems.
Yuto Takano81528c02021-08-06 16:22:06 +0100697 """
Yuto Takano39639672021-08-05 19:47:48 +0100698 if problems:
Darryl Greend5802922018-05-08 15:30:59 +0100699 self.set_return_code(1)
Yuto Takano55614b52021-08-07 01:00:18 +0100700 self.log.info("{}: FAIL\n".format(name))
701 for problem in problems:
702 self.log.warning(str(problem))
Darryl Greend5802922018-05-08 15:30:59 +0100703 else:
Yuto Takano81528c02021-08-06 16:22:06 +0100704 self.log.info("{}: PASS".format(name))
Darryl Greend5802922018-05-08 15:30:59 +0100705
Yuto Takanod93fa372021-08-06 23:05:55 +0100706def check_repo_path():
707 """
708 Check that the current working directory is the project root, and throw
709 an exception if not.
710 """
711 if (not os.path.isdir("include") or
712 not os.path.isdir("tests") or
713 not os.path.isdir("library")):
714 raise Exception("This script must be run from Mbed TLS root")
715
Yuto Takano39639672021-08-05 19:47:48 +0100716def main():
717 """
Yuto Takano81528c02021-08-06 16:22:06 +0100718 Perform argument parsing, and create an instance of NameCheck to begin the
719 core operation.
Yuto Takano39639672021-08-05 19:47:48 +0100720 """
Yuto Takano39639672021-08-05 19:47:48 +0100721 parser = argparse.ArgumentParser(
722 formatter_class=argparse.RawDescriptionHelpFormatter,
723 description=(
724 "This script confirms that the naming of all symbols and identifiers "
725 "in Mbed TLS are consistent with the house style and are also "
726 "self-consistent.\n\n"
727 "Expected to be run from the MbedTLS root directory."))
Darryl Greend5802922018-05-08 15:30:59 +0100728
Yuto Takano39639672021-08-05 19:47:48 +0100729 parser.add_argument("-v", "--verbose",
730 action="store_true",
Yuto Takano81528c02021-08-06 16:22:06 +0100731 help="show parse results")
732
733 parser.add_argument("-q", "--quiet",
734 action="store_true",
Yuto Takano55614b52021-08-07 01:00:18 +0100735 help="hide unnecessary text, explanations, and highlighs")
Yuto Takano81528c02021-08-06 16:22:06 +0100736
Yuto Takano39639672021-08-05 19:47:48 +0100737 args = parser.parse_args()
Darryl Greend5802922018-05-08 15:30:59 +0100738
Darryl Greend5802922018-05-08 15:30:59 +0100739 try:
Yuto Takanod93fa372021-08-06 23:05:55 +0100740 check_repo_path()
Darryl Greend5802922018-05-08 15:30:59 +0100741 name_check = NameCheck()
Yuto Takano39639672021-08-05 19:47:48 +0100742 name_check.setup_logger(verbose=args.verbose)
743 name_check.parse_names_in_source()
Yuto Takano55614b52021-08-07 01:00:18 +0100744 name_check.perform_checks(quiet=args.quiet)
Yuto Takano81528c02021-08-06 16:22:06 +0100745 sys.exit(name_check.return_code)
Yuto Takanod93fa372021-08-06 23:05:55 +0100746 except Exception: # pylint: disable=broad-except
Darryl Greend5802922018-05-08 15:30:59 +0100747 traceback.print_exc()
748 sys.exit(2)
749
Darryl Greend5802922018-05-08 15:30:59 +0100750if __name__ == "__main__":
Yuto Takano39639672021-08-05 19:47:48 +0100751 main()