blob: 5fe85b7bd66d22f228bb3f760cb1aecaf8376c4f [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
36import textwrap
Darryl Greend5802922018-05-08 15:30:59 +010037import os
38import sys
39import traceback
40import re
41import shutil
42import subprocess
43import logging
44
Yuto Takano81528c02021-08-06 16:22:06 +010045# Naming patterns to check against. These are defined outside the NameCheck
46# class for ease of modification.
Yuto Takanobb7dca42021-08-05 19:57:58 +010047MACRO_PATTERN = r"^(MBEDTLS|PSA)_[0-9A-Z_]*[0-9A-Z]$"
Yuto Takano81528c02021-08-06 16:22:06 +010048CONSTANTS_PATTERN = MACRO_PATTERN
Yuto Takanoc1838932021-08-05 19:52:09 +010049IDENTIFIER_PATTERN = r"^(mbedtls|psa)_[0-9a-z_]*[0-9a-z]$"
Yuto Takano39639672021-08-05 19:47:48 +010050
Yuto Takanod93fa372021-08-06 23:05:55 +010051class Match(): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010052 """
53 A class representing a match, together with its found position.
54
55 Fields:
56 * filename: the file that the match was in.
57 * line: the full line containing the match.
Yuto Takanod93fa372021-08-06 23:05:55 +010058 * pos: a tuple of (line_no, start, end) positions on the file line where the
59 match is.
Yuto Takano81528c02021-08-06 16:22:06 +010060 * name: the match itself.
61 """
Yuto Takanod93fa372021-08-06 23:05:55 +010062 def __init__(self, filename, line, pos, name):
Yuto Takano39639672021-08-05 19:47:48 +010063 self.filename = filename
64 self.line = line
65 self.pos = pos
66 self.name = name
Yuto Takano39639672021-08-05 19:47:48 +010067
Yuto Takanoa4e75122021-08-06 17:23:28 +010068 def __str__(self):
Yuto Takano381fda82021-08-06 23:37:20 +010069 ln_str = str(self.pos[0])
70 gutter_len = max(4, len(ln_str))
71 gutter = (gutter_len - len(ln_str)) * " " + ln_str
72 underline = self.pos[1] * " " + (self.pos[2] - self.pos[1]) * "^"
73
Yuto Takanoa4e75122021-08-06 17:23:28 +010074 return (
Yuto Takano381fda82021-08-06 23:37:20 +010075 " {0} |\n".format(gutter_len * " ") +
76 " {0} | {1}".format(gutter, self.line) +
Yuto Takano55614b52021-08-07 01:00:18 +010077 " {0} | {1}\n".format(gutter_len * " ", underline)
Yuto Takanoa4e75122021-08-06 17:23:28 +010078 )
Yuto Takanod93fa372021-08-06 23:05:55 +010079
80class Problem(): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010081 """
82 A parent class representing a form of static analysis error.
83
84 Fields:
85 * textwrapper: a TextWrapper instance to format problems nicely.
86 """
Yuto Takano39639672021-08-05 19:47:48 +010087 def __init__(self):
88 self.textwrapper = textwrap.TextWrapper()
Yuto Takano81528c02021-08-06 16:22:06 +010089 self.textwrapper.width = 80
Yuto Takanoa4e75122021-08-06 17:23:28 +010090 self.textwrapper.initial_indent = " > "
Yuto Takano81528c02021-08-06 16:22:06 +010091 self.textwrapper.subsequent_indent = " "
Yuto Takano39639672021-08-05 19:47:48 +010092
Yuto Takanod93fa372021-08-06 23:05:55 +010093class SymbolNotInHeader(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010094 """
95 A problem that occurs when an exported/available symbol in the object file
96 is not explicitly declared in header files. Created with
97 NameCheck.check_symbols_declared_in_header()
98
99 Fields:
100 * symbol_name: the name of the symbol.
101 """
Yuto Takano55614b52021-08-07 01:00:18 +0100102 def __init__(self, symbol_name, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100103 self.symbol_name = symbol_name
Yuto Takano55614b52021-08-07 01:00:18 +0100104 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100105 Problem.__init__(self)
106
107 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100108 if self.quiet:
109 return "{0}".format(self.symbol_name)
110
Yuto Takano39639672021-08-05 19:47:48 +0100111 return self.textwrapper.fill(
112 "'{0}' was found as an available symbol in the output of nm, "
113 "however it was not declared in any header files."
114 .format(self.symbol_name))
115
Yuto Takanod93fa372021-08-06 23:05:55 +0100116class PatternMismatch(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +0100117 """
118 A problem that occurs when something doesn't match the expected pattern.
119 Created with NameCheck.check_match_pattern()
120
121 Fields:
122 * pattern: the expected regex pattern
123 * match: the Match object in question
124 """
Yuto Takano55614b52021-08-07 01:00:18 +0100125 def __init__(self, pattern, match, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100126 self.pattern = pattern
127 self.match = match
Yuto Takano55614b52021-08-07 01:00:18 +0100128 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100129 Problem.__init__(self)
Yuto Takano81528c02021-08-06 16:22:06 +0100130
Yuto Takano39639672021-08-05 19:47:48 +0100131 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100132 if self.quiet:
133 return ("{0}:{1}:{3}"
134 .format(
135 self.match.filename,
136 self.match.pos[0],
137 self.match.name))
138
Yuto Takano39639672021-08-05 19:47:48 +0100139 return self.textwrapper.fill(
Yuto Takanoa4e75122021-08-06 17:23:28 +0100140 "{0}:{1}: '{2}' does not match the required pattern '{3}'."
141 .format(
142 self.match.filename,
Yuto Takanod93fa372021-08-06 23:05:55 +0100143 self.match.pos[0],
Yuto Takanoa4e75122021-08-06 17:23:28 +0100144 self.match.name,
145 self.pattern)) + "\n" + str(self.match)
Yuto Takano39639672021-08-05 19:47:48 +0100146
Yuto Takanod93fa372021-08-06 23:05:55 +0100147class Typo(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +0100148 """
149 A problem that occurs when a word using MBED doesn't appear to be defined as
150 constants nor enum values. Created with NameCheck.check_for_typos()
151
152 Fields:
153 * match: the Match object of the MBED name in question.
154 """
Yuto Takano55614b52021-08-07 01:00:18 +0100155 def __init__(self, match, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100156 self.match = match
Yuto Takano55614b52021-08-07 01:00:18 +0100157 self.quiet = quiet
Yuto Takano39639672021-08-05 19:47:48 +0100158 Problem.__init__(self)
Yuto Takano81528c02021-08-06 16:22:06 +0100159
Yuto Takano39639672021-08-05 19:47:48 +0100160 def __str__(self):
Yuto Takano55614b52021-08-07 01:00:18 +0100161 if self.quiet:
162 return ("{0}:{1}:{2}"
163 .format(
164 self.match.filename,
165 self.match.pos[0],
166 self.match.name))
167
Yuto Takano39639672021-08-05 19:47:48 +0100168 return self.textwrapper.fill(
Yuto Takanoa4e75122021-08-06 17:23:28 +0100169 "{0}:{1}: '{2}' looks like a typo. It was not found in any "
170 "macros or any enums. If this is not a typo, put "
171 "//no-check-names after it."
172 .format(
173 self.match.filename,
Yuto Takanod93fa372021-08-06 23:05:55 +0100174 self.match.pos[0],
Yuto Takanoa4e75122021-08-06 17:23:28 +0100175 self.match.name)) + "\n" + str(self.match)
Darryl Greend5802922018-05-08 15:30:59 +0100176
Yuto Takanod93fa372021-08-06 23:05:55 +0100177class NameCheck():
Yuto Takano81528c02021-08-06 16:22:06 +0100178 """
179 Representation of the core name checking operation performed by this script.
180 Shares a common logger, common excluded filenames, and a shared return_code.
181 """
Darryl Greend5802922018-05-08 15:30:59 +0100182 def __init__(self):
183 self.log = None
Yuto Takanofc54dfb2021-08-07 17:18:28 +0100184 self.check_repo_path()
Darryl Greend5802922018-05-08 15:30:59 +0100185 self.return_code = 0
Yuto Takano81528c02021-08-06 16:22:06 +0100186 self.excluded_files = ["bn_mul", "compat-2.x.h"]
Yuto Takanod93fa372021-08-06 23:05:55 +0100187 self.parse_result = {}
Darryl Greend5802922018-05-08 15:30:59 +0100188
189 def set_return_code(self, return_code):
190 if return_code > self.return_code:
Yuto Takano201f9e82021-08-06 16:36:54 +0100191 self.log.debug("Setting new return code to {}".format(return_code))
Darryl Greend5802922018-05-08 15:30:59 +0100192 self.return_code = return_code
193
Yuto Takanofc54dfb2021-08-07 17:18:28 +0100194 @staticmethod
195 def check_repo_path():
196 """
197 Check that the current working directory is the project root, and throw
198 an exception if not.
199 """
200 if not all(os.path.isdir(d) for d in ["include", "library", "tests"]):
201 raise Exception("This script must be run from Mbed TLS root")
202
Yuto Takano39639672021-08-05 19:47:48 +0100203 def setup_logger(self, verbose=False):
204 """
205 Set up a logger and set the change the default logging level from
Yuto Takano81528c02021-08-06 16:22:06 +0100206 WARNING to INFO. Loggers are better than print statements since their
Yuto Takano39639672021-08-05 19:47:48 +0100207 verbosity can be controlled.
208 """
Darryl Greend5802922018-05-08 15:30:59 +0100209 self.log = logging.getLogger()
Yuto Takano39639672021-08-05 19:47:48 +0100210 if verbose:
211 self.log.setLevel(logging.DEBUG)
212 else:
213 self.log.setLevel(logging.INFO)
Darryl Greend5802922018-05-08 15:30:59 +0100214 self.log.addHandler(logging.StreamHandler())
215
Yuto Takano157444c2021-08-05 20:10:45 +0100216 def get_files(self, extension, directory):
Yuto Takano81528c02021-08-06 16:22:06 +0100217 """
218 Get all files that end with .extension in the specified directory
219 recursively.
220
221 Args:
222 * extension: the file extension to search for, without the dot
223 * directory: the directory to recursively search for
224
225 Returns a List of relative filepaths.
226 """
Darryl Greend5802922018-05-08 15:30:59 +0100227 filenames = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100228 for root, _, files in sorted(os.walk(directory)):
Darryl Greend5802922018-05-08 15:30:59 +0100229 for filename in sorted(files):
230 if (filename not in self.excluded_files and
Yuto Takano157444c2021-08-05 20:10:45 +0100231 filename.endswith("." + extension)):
Darryl Greend5802922018-05-08 15:30:59 +0100232 filenames.append(os.path.join(root, filename))
233 return filenames
234
Yuto Takano81528c02021-08-06 16:22:06 +0100235 def parse_names_in_source(self):
236 """
237 Calls each parsing function to retrieve various elements of the code,
238 together with their source location. Puts the parsed values in the
239 internal variable self.parse_result.
240 """
241 self.log.info("Parsing source code...")
Yuto Takanod24e0372021-08-06 16:42:33 +0100242 self.log.debug(
243 "The following files are excluded from the search: {}"
244 .format(str(self.excluded_files))
245 )
Yuto Takano81528c02021-08-06 16:22:06 +0100246
247 m_headers = self.get_files("h", os.path.join("include", "mbedtls"))
248 p_headers = self.get_files("h", os.path.join("include", "psa"))
249 t_headers = ["3rdparty/everest/include/everest/everest.h",
250 "3rdparty/everest/include/everest/x25519.h"]
251 d_headers = self.get_files("h", os.path.join("tests", "include", "test", "drivers"))
252 l_headers = self.get_files("h", "library")
253 libraries = self.get_files("c", "library") + [
254 "3rdparty/everest/library/everest.c",
255 "3rdparty/everest/library/x25519.c"]
256
257 all_macros = self.parse_macros(
258 m_headers + p_headers + t_headers + l_headers + d_headers)
259 enum_consts = self.parse_enum_consts(
260 m_headers + l_headers + t_headers)
261 identifiers = self.parse_identifiers(
262 m_headers + p_headers + t_headers + l_headers)
Yuto Takanod93fa372021-08-06 23:05:55 +0100263 mbed_words = self.parse_mbed_words(
Yuto Takano81528c02021-08-06 16:22:06 +0100264 m_headers + p_headers + t_headers + l_headers + libraries)
265 symbols = self.parse_symbols()
266
267 # Remove identifier macros like mbedtls_printf or mbedtls_calloc
268 identifiers_justname = [x.name for x in identifiers]
269 actual_macros = []
270 for macro in all_macros:
271 if macro.name not in identifiers_justname:
272 actual_macros.append(macro)
273
274 self.log.debug("Found:")
275 self.log.debug(" {} Macros".format(len(all_macros)))
276 self.log.debug(" {} Non-identifier Macros".format(len(actual_macros)))
277 self.log.debug(" {} Enum Constants".format(len(enum_consts)))
278 self.log.debug(" {} Identifiers".format(len(identifiers)))
279 self.log.debug(" {} Exported Symbols".format(len(symbols)))
280 self.log.info("Analysing...")
281
282 self.parse_result = {
283 "macros": actual_macros,
284 "enum_consts": enum_consts,
285 "identifiers": identifiers,
286 "symbols": symbols,
Yuto Takanod93fa372021-08-06 23:05:55 +0100287 "mbed_words": mbed_words
Yuto Takano81528c02021-08-06 16:22:06 +0100288 }
289
Yuto Takano39639672021-08-05 19:47:48 +0100290 def parse_macros(self, header_files):
291 """
292 Parse all macros defined by #define preprocessor directives.
293
294 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100295 * header_files: A List of filepaths to look through.
296
297 Returns a List of Match objects for the found macros.
Yuto Takano39639672021-08-05 19:47:48 +0100298 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100299 macro_regex = re.compile(r"# *define +(?P<macro>\w+)")
300 exclusions = (
Yuto Takano39639672021-08-05 19:47:48 +0100301 "asm", "inline", "EMIT", "_CRT_SECURE_NO_DEPRECATE", "MULADDC_"
302 )
303
Yuto Takano201f9e82021-08-06 16:36:54 +0100304 self.log.debug("Looking for macros in {} files".format(len(header_files)))
Yuto Takanod93fa372021-08-06 23:05:55 +0100305
306 macros = []
307
Yuto Takano39639672021-08-05 19:47:48 +0100308 for header_file in header_files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100309 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100310 for line_no, line in enumerate(header):
Yuto Takanod93fa372021-08-06 23:05:55 +0100311 for macro in macro_regex.finditer(line):
312 if not macro.group("macro").startswith(exclusions):
Yuto Takano81528c02021-08-06 16:22:06 +0100313 macros.append(Match(
314 header_file,
315 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100316 (line_no, macro.start(), macro.end()),
Yuto Takano81528c02021-08-06 16:22:06 +0100317 macro.group("macro")))
Darryl Greend5802922018-05-08 15:30:59 +0100318
Yuto Takano39639672021-08-05 19:47:48 +0100319 return macros
Darryl Greend5802922018-05-08 15:30:59 +0100320
Yuto Takanod93fa372021-08-06 23:05:55 +0100321 def parse_mbed_words(self, files):
Yuto Takano39639672021-08-05 19:47:48 +0100322 """
Yuto Takanob47b5042021-08-07 00:42:54 +0100323 Parse all words in the file that begin with MBED, in and out of macros,
324 comments, anything.
Yuto Takano39639672021-08-05 19:47:48 +0100325
326 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100327 * files: a List of filepaths to look through.
328
329 Returns a List of Match objects for words beginning with MBED.
Yuto Takano39639672021-08-05 19:47:48 +0100330 """
Yuto Takanob47b5042021-08-07 00:42:54 +0100331 # Typos of TLS are common, hence the broader check below than MBEDTLS.
Yuto Takanod93fa372021-08-06 23:05:55 +0100332 mbed_regex = re.compile(r"\bMBED.+?_[A-Z0-9_]*")
333 exclusions = re.compile(r"// *no-check-names|#error")
334
Yuto Takano201f9e82021-08-06 16:36:54 +0100335 self.log.debug("Looking for MBED names in {} files".format(len(files)))
Yuto Takanod93fa372021-08-06 23:05:55 +0100336
337 mbed_words = []
338
Yuto Takanobb7dca42021-08-05 19:57:58 +0100339 for filename in files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100340 with open(filename, "r", encoding="utf-8") as fp:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100341 for line_no, line in enumerate(fp):
Yuto Takanod93fa372021-08-06 23:05:55 +0100342 if exclusions.search(line):
Yuto Takanoc62b4082021-08-05 20:17:07 +0100343 continue
Yuto Takano81528c02021-08-06 16:22:06 +0100344
Yuto Takanod93fa372021-08-06 23:05:55 +0100345 for name in mbed_regex.finditer(line):
346 mbed_words.append(Match(
Yuto Takano39639672021-08-05 19:47:48 +0100347 filename,
348 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100349 (line_no, name.start(), name.end()),
Yuto Takano39639672021-08-05 19:47:48 +0100350 name.group(0)
351 ))
352
Yuto Takanod93fa372021-08-06 23:05:55 +0100353 return mbed_words
Yuto Takano39639672021-08-05 19:47:48 +0100354
355 def parse_enum_consts(self, header_files):
356 """
357 Parse all enum value constants that are declared.
358
359 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100360 * header_files: A List of filepaths to look through.
Yuto Takano39639672021-08-05 19:47:48 +0100361
Yuto Takano81528c02021-08-06 16:22:06 +0100362 Returns a List of Match objects for the findings.
Yuto Takano39639672021-08-05 19:47:48 +0100363 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100364 self.log.debug("Looking for enum consts in {} files".format(len(header_files)))
Yuto Takano39639672021-08-05 19:47:48 +0100365
366 enum_consts = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100367
Yuto Takano39639672021-08-05 19:47:48 +0100368 for header_file in header_files:
369 # Emulate a finite state machine to parse enum declarations.
Yuto Takano81528c02021-08-06 16:22:06 +0100370 # 0 = not in enum
371 # 1 = inside enum
372 # 2 = almost inside enum
Darryl Greend5802922018-05-08 15:30:59 +0100373 state = 0
Yuto Takanoa083d152021-08-07 00:25:59 +0100374 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100375 for line_no, line in enumerate(header):
Yuto Takano13ecd992021-08-06 16:56:52 +0100376 # Match typedefs and brackets only when they are at the
377 # beginning of the line -- if they are indented, they might
378 # be sub-structures within structs, etc.
Yuto Takanod93fa372021-08-06 23:05:55 +0100379 if state == 0 and re.match(r"^(typedef +)?enum +{", line):
Darryl Greend5802922018-05-08 15:30:59 +0100380 state = 1
Yuto Takanod93fa372021-08-06 23:05:55 +0100381 elif state == 0 and re.match(r"^(typedef +)?enum", line):
Darryl Greend5802922018-05-08 15:30:59 +0100382 state = 2
Yuto Takanod93fa372021-08-06 23:05:55 +0100383 elif state == 2 and re.match(r"^{", line):
Darryl Greend5802922018-05-08 15:30:59 +0100384 state = 1
Yuto Takanod93fa372021-08-06 23:05:55 +0100385 elif state == 1 and re.match(r"^}", line):
Darryl Greend5802922018-05-08 15:30:59 +0100386 state = 0
Yuto Takanod93fa372021-08-06 23:05:55 +0100387 elif state == 1 and not re.match(r" *#", line):
Yuto Takano13ecd992021-08-06 16:56:52 +0100388 enum_const = re.match(r" *(?P<enum_const>\w+)", line)
Darryl Greend5802922018-05-08 15:30:59 +0100389 if enum_const:
Yuto Takano39639672021-08-05 19:47:48 +0100390 enum_consts.append(Match(
391 header_file,
392 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100393 (line_no, enum_const.start(), enum_const.end()),
Yuto Takano39639672021-08-05 19:47:48 +0100394 enum_const.group("enum_const")))
Yuto Takano81528c02021-08-06 16:22:06 +0100395
Yuto Takano39639672021-08-05 19:47:48 +0100396 return enum_consts
Darryl Greend5802922018-05-08 15:30:59 +0100397
Yuto Takano39639672021-08-05 19:47:48 +0100398 def parse_identifiers(self, header_files):
399 """
400 Parse all lines of a header where a function identifier is declared,
Yuto Takano81528c02021-08-06 16:22:06 +0100401 based on some huersitics. Highly dependent on formatting style.
Darryl Greend5802922018-05-08 15:30:59 +0100402
Yuto Takano39639672021-08-05 19:47:48 +0100403 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100404 * header_files: A List of filepaths to look through.
405
406 Returns a List of Match objects with identifiers.
Yuto Takano39639672021-08-05 19:47:48 +0100407 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100408 identifier_regex = re.compile(
409 # Match " something(a" or " *something(a". Functions.
410 # Assumptions:
411 # - function definition from return type to one of its arguments is
412 # all on one line (enforced by the previous_line concat below)
413 # - function definition line only contains alphanumeric, asterisk,
414 # underscore, and open bracket
415 r".* \**(\w+) *\( *\w|"
416 # Match "(*something)(". Flexible with spaces.
417 r".*\( *\* *(\w+) *\) *\(|"
418 # Match names of named data structures.
419 r"(?:typedef +)?(?:struct|union|enum) +(\w+)(?: *{)?$|"
420 # Match names of typedef instances, after closing bracket.
421 r"}? *(\w+)[;[].*")
422 exclusion_lines = re.compile(r"^("
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100423 r"extern +\"C\"|"
424 r"(typedef +)?(struct|union|enum)( *{)?$|"
425 r"} *;?$|"
426 r"$|"
427 r"//|"
428 r"#"
429 r")")
Yuto Takanod93fa372021-08-06 23:05:55 +0100430
431 self.log.debug("Looking for identifiers in {} files".format(len(header_files)))
Darryl Greend5802922018-05-08 15:30:59 +0100432
Yuto Takano39639672021-08-05 19:47:48 +0100433 identifiers = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100434
Yuto Takano39639672021-08-05 19:47:48 +0100435 for header_file in header_files:
Yuto Takanoa083d152021-08-07 00:25:59 +0100436 with open(header_file, "r", encoding="utf-8") as header:
Yuto Takano39639672021-08-05 19:47:48 +0100437 in_block_comment = False
Yuto Takanod93fa372021-08-06 23:05:55 +0100438 previous_line = ""
Darryl Greend5802922018-05-08 15:30:59 +0100439
Yuto Takano8f457cf2021-08-06 17:54:58 +0100440 for line_no, line in enumerate(header):
Yuto Takano81528c02021-08-06 16:22:06 +0100441 # Skip parsing this line if a block comment ends on it,
442 # but don't skip if it has just started -- there is a chance
443 # it ends on the same line.
Yuto Takano39639672021-08-05 19:47:48 +0100444 if re.search(r"/\*", line):
Yuto Takano81528c02021-08-06 16:22:06 +0100445 in_block_comment = not in_block_comment
446 if re.search(r"\*/", line):
447 in_block_comment = not in_block_comment
Yuto Takano39639672021-08-05 19:47:48 +0100448 continue
449
Yuto Takano81528c02021-08-06 16:22:06 +0100450 if in_block_comment:
Yuto Takanod93fa372021-08-06 23:05:55 +0100451 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100452 continue
453
Yuto Takanod93fa372021-08-06 23:05:55 +0100454 if exclusion_lines.match(line):
455 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100456 continue
457
Yuto Takanocfc9e4a2021-08-06 20:02:32 +0100458 # If the line contains only space-separated alphanumeric
459 # characters (or underscore, asterisk, or, open bracket),
460 # and nothing else, high chance it's a declaration that
461 # continues on the next line
462 if re.match(r"^([\w\*\(]+\s+)+$", line):
Yuto Takanod93fa372021-08-06 23:05:55 +0100463 previous_line += line
Yuto Takano81528c02021-08-06 16:22:06 +0100464 continue
465
466 # If previous line seemed to start an unfinished declaration
Yuto Takanocfc9e4a2021-08-06 20:02:32 +0100467 # (as above), concat and treat them as one.
468 if previous_line:
469 line = previous_line.strip() + " " + line.strip()
Yuto Takanod93fa372021-08-06 23:05:55 +0100470 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100471
472 # Skip parsing if line has a space in front = hueristic to
473 # skip function argument lines (highly subject to formatting
474 # changes)
475 if line[0] == " ":
Yuto Takano39639672021-08-05 19:47:48 +0100476 continue
Yuto Takano6f38ab32021-08-05 21:07:14 +0100477
Yuto Takanod93fa372021-08-06 23:05:55 +0100478 identifier = identifier_regex.search(line)
Yuto Takano39639672021-08-05 19:47:48 +0100479
480 if identifier:
Yuto Takano81528c02021-08-06 16:22:06 +0100481 # Find the group that matched, and append it
Yuto Takano39639672021-08-05 19:47:48 +0100482 for group in identifier.groups():
483 if group:
484 identifiers.append(Match(
485 header_file,
486 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100487 (line_no, identifier.start(), identifier.end()),
Yuto Takano81528c02021-08-06 16:22:06 +0100488 group))
Yuto Takano39639672021-08-05 19:47:48 +0100489
490 return identifiers
491
492 def parse_symbols(self):
493 """
494 Compile the Mbed TLS libraries, and parse the TLS, Crypto, and x509
495 object files using nm to retrieve the list of referenced symbols.
Yuto Takano81528c02021-08-06 16:22:06 +0100496 Exceptions thrown here are rethrown because they would be critical
497 errors that void several tests, and thus needs to halt the program. This
498 is explicitly done for clarity.
Yuto Takano39639672021-08-05 19:47:48 +0100499
Yuto Takano81528c02021-08-06 16:22:06 +0100500 Returns a List of unique symbols defined and used in the libraries.
501 """
502 self.log.info("Compiling...")
Yuto Takano39639672021-08-05 19:47:48 +0100503 symbols = []
504
505 # Back up the config and atomically compile with the full configratuion.
506 shutil.copy("include/mbedtls/mbedtls_config.h",
Yuto Takano81528c02021-08-06 16:22:06 +0100507 "include/mbedtls/mbedtls_config.h.bak")
Darryl Greend5802922018-05-08 15:30:59 +0100508 try:
Yuto Takano81528c02021-08-06 16:22:06 +0100509 # Use check=True in all subprocess calls so that failures are raised
510 # as exceptions and logged.
Yuto Takano39639672021-08-05 19:47:48 +0100511 subprocess.run(
Yuto Takano81528c02021-08-06 16:22:06 +0100512 ["python3", "scripts/config.py", "full"],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100513 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100514 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100515 )
516 my_environment = os.environ.copy()
517 my_environment["CFLAGS"] = "-fno-asynchronous-unwind-tables"
Yuto Takano39639672021-08-05 19:47:48 +0100518 subprocess.run(
Darryl Greend5802922018-05-08 15:30:59 +0100519 ["make", "clean", "lib"],
520 env=my_environment,
Yuto Takanobcc3d992021-08-06 23:14:58 +0100521 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100522 stdout=subprocess.PIPE,
Darryl Greend5802922018-05-08 15:30:59 +0100523 stderr=subprocess.STDOUT,
Yuto Takano39639672021-08-05 19:47:48 +0100524 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100525 )
Yuto Takano39639672021-08-05 19:47:48 +0100526
527 # Perform object file analysis using nm
528 symbols = self.parse_symbols_from_nm(
529 ["library/libmbedcrypto.a",
Yuto Takanod93fa372021-08-06 23:05:55 +0100530 "library/libmbedtls.a",
531 "library/libmbedx509.a"])
Yuto Takano39639672021-08-05 19:47:48 +0100532
533 subprocess.run(
Darryl Greend5802922018-05-08 15:30:59 +0100534 ["make", "clean"],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100535 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100536 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100537 )
538 except subprocess.CalledProcessError as error:
Yuto Takano25eeb7b2021-08-06 21:27:59 +0100539 self.log.debug(error.output)
Darryl Greend5802922018-05-08 15:30:59 +0100540 self.set_return_code(2)
Yuto Takano81528c02021-08-06 16:22:06 +0100541 raise error
Yuto Takano39639672021-08-05 19:47:48 +0100542 finally:
Yuto Takano6fececf2021-08-07 17:28:23 +0100543 # Put back the original config regardless of there being errors.
544 # Works also for keyboard interrupts.
Yuto Takano39639672021-08-05 19:47:48 +0100545 shutil.move("include/mbedtls/mbedtls_config.h.bak",
546 "include/mbedtls/mbedtls_config.h")
547
548 return symbols
549
550 def parse_symbols_from_nm(self, object_files):
551 """
552 Run nm to retrieve the list of referenced symbols in each object file.
553 Does not return the position data since it is of no use.
554
Yuto Takano81528c02021-08-06 16:22:06 +0100555 Args:
556 * object_files: a List of compiled object files to search through.
557
558 Returns a List of unique symbols defined and used in any of the object
559 files.
Yuto Takano39639672021-08-05 19:47:48 +0100560 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100561 nm_undefined_regex = re.compile(r"^\S+: +U |^$|^\S+:$")
562 nm_valid_regex = re.compile(r"^\S+( [0-9A-Fa-f]+)* . _*(?P<symbol>\w+)")
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100563 exclusions = ("FStar", "Hacl")
Yuto Takano39639672021-08-05 19:47:48 +0100564
565 symbols = []
566
Yuto Takano81528c02021-08-06 16:22:06 +0100567 # Gather all outputs of nm
Yuto Takano39639672021-08-05 19:47:48 +0100568 nm_output = ""
569 for lib in object_files:
570 nm_output += subprocess.run(
571 ["nm", "-og", lib],
Yuto Takanobcc3d992021-08-06 23:14:58 +0100572 universal_newlines=True,
Yuto Takano39639672021-08-05 19:47:48 +0100573 stdout=subprocess.PIPE,
574 stderr=subprocess.STDOUT,
575 check=True
576 ).stdout
Yuto Takano81528c02021-08-06 16:22:06 +0100577
Yuto Takano39639672021-08-05 19:47:48 +0100578 for line in nm_output.splitlines():
Yuto Takanod93fa372021-08-06 23:05:55 +0100579 if not nm_undefined_regex.match(line):
580 symbol = nm_valid_regex.match(line)
Yuto Takano12a7ecd2021-08-07 00:40:29 +0100581 if (symbol and not symbol.group("symbol").startswith(exclusions)):
Yuto Takanoe77f6992021-08-05 20:22:59 +0100582 symbols.append(symbol.group("symbol"))
Yuto Takano39639672021-08-05 19:47:48 +0100583 else:
584 self.log.error(line)
Yuto Takano81528c02021-08-06 16:22:06 +0100585
Yuto Takano39639672021-08-05 19:47:48 +0100586 return symbols
587
Yuto Takano55614b52021-08-07 01:00:18 +0100588 def perform_checks(self, quiet=False):
Yuto Takano39639672021-08-05 19:47:48 +0100589 """
590 Perform each check in order, output its PASS/FAIL status. Maintain an
591 overall test status, and output that at the end.
Yuto Takano81528c02021-08-06 16:22:06 +0100592
593 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100594 * quiet: whether to hide detailed problem explanation.
Yuto Takano39639672021-08-05 19:47:48 +0100595 """
Yuto Takano81528c02021-08-06 16:22:06 +0100596 self.log.info("=============")
Yuto Takano39639672021-08-05 19:47:48 +0100597 problems = 0
598
Yuto Takano55614b52021-08-07 01:00:18 +0100599 problems += self.check_symbols_declared_in_header(quiet)
Yuto Takano39639672021-08-05 19:47:48 +0100600
Yuto Takanod93fa372021-08-06 23:05:55 +0100601 pattern_checks = [("macros", MACRO_PATTERN),
602 ("enum_consts", CONSTANTS_PATTERN),
603 ("identifiers", IDENTIFIER_PATTERN)]
Yuto Takano39639672021-08-05 19:47:48 +0100604 for group, check_pattern in pattern_checks:
Yuto Takano55614b52021-08-07 01:00:18 +0100605 problems += self.check_match_pattern(quiet, group, check_pattern)
Yuto Takano39639672021-08-05 19:47:48 +0100606
Yuto Takano55614b52021-08-07 01:00:18 +0100607 problems += self.check_for_typos(quiet)
Yuto Takano39639672021-08-05 19:47:48 +0100608
609 self.log.info("=============")
610 if problems > 0:
611 self.log.info("FAIL: {0} problem(s) to fix".format(str(problems)))
Yuto Takano55614b52021-08-07 01:00:18 +0100612 if quiet:
613 self.log.info("Remove --quiet to see explanations.")
Yuto Takanofc54dfb2021-08-07 17:18:28 +0100614 else:
615 self.log.info("Use --quiet for minimal output.")
Yuto Takano39639672021-08-05 19:47:48 +0100616 else:
617 self.log.info("PASS")
Darryl Greend5802922018-05-08 15:30:59 +0100618
Yuto Takano55614b52021-08-07 01:00:18 +0100619 def check_symbols_declared_in_header(self, quiet):
Yuto Takano39639672021-08-05 19:47:48 +0100620 """
621 Perform a check that all detected symbols in the library object files
622 are properly declared in headers.
Darryl Greend5802922018-05-08 15:30:59 +0100623
Yuto Takano81528c02021-08-06 16:22:06 +0100624 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100625 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100626
627 Returns the number of problems that need fixing.
Yuto Takano39639672021-08-05 19:47:48 +0100628 """
629 problems = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100630
Yuto Takano39639672021-08-05 19:47:48 +0100631 for symbol in self.parse_result["symbols"]:
632 found_symbol_declared = False
633 for identifier_match in self.parse_result["identifiers"]:
634 if symbol == identifier_match.name:
635 found_symbol_declared = True
636 break
Yuto Takano81528c02021-08-06 16:22:06 +0100637
Yuto Takano39639672021-08-05 19:47:48 +0100638 if not found_symbol_declared:
Yuto Takano55614b52021-08-07 01:00:18 +0100639 problems.append(SymbolNotInHeader(symbol, quiet=quiet))
Yuto Takano39639672021-08-05 19:47:48 +0100640
Yuto Takano55614b52021-08-07 01:00:18 +0100641 self.output_check_result("All symbols in header", problems)
Yuto Takano39639672021-08-05 19:47:48 +0100642 return len(problems)
643
Yuto Takano81528c02021-08-06 16:22:06 +0100644
Yuto Takano55614b52021-08-07 01:00:18 +0100645 def check_match_pattern(self, quiet, group_to_check, check_pattern):
Yuto Takano81528c02021-08-06 16:22:06 +0100646 """
647 Perform a check that all items of a group conform to a regex pattern.
648
649 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100650 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100651 * group_to_check: string key to index into self.parse_result.
652 * check_pattern: the regex to check against.
653
654 Returns the number of problems that need fixing.
655 """
Yuto Takano39639672021-08-05 19:47:48 +0100656 problems = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100657
Yuto Takano39639672021-08-05 19:47:48 +0100658 for item_match in self.parse_result[group_to_check]:
659 if not re.match(check_pattern, item_match.name):
660 problems.append(PatternMismatch(check_pattern, item_match))
Yuto Takano201f9e82021-08-06 16:36:54 +0100661 # Double underscore is a reserved identifier, never to be used
Yuto Takanoc763cc32021-08-05 20:06:34 +0100662 if re.match(r".*__.*", item_match.name):
Yuto Takano55614b52021-08-07 01:00:18 +0100663 problems.append(PatternMismatch(
664 "double underscore",
665 item_match,
666 quiet=quiet))
Yuto Takano81528c02021-08-06 16:22:06 +0100667
668 self.output_check_result(
669 "Naming patterns of {}".format(group_to_check),
Yuto Takano55614b52021-08-07 01:00:18 +0100670 problems)
Yuto Takano39639672021-08-05 19:47:48 +0100671 return len(problems)
Darryl Greend5802922018-05-08 15:30:59 +0100672
Yuto Takano55614b52021-08-07 01:00:18 +0100673 def check_for_typos(self, quiet):
Yuto Takano81528c02021-08-06 16:22:06 +0100674 """
675 Perform a check that all words in the soure code beginning with MBED are
676 either defined as macros, or as enum constants.
677
678 Args:
Yuto Takano55614b52021-08-07 01:00:18 +0100679 * quiet: whether to hide detailed problem explanation.
Yuto Takano81528c02021-08-06 16:22:06 +0100680
681 Returns the number of problems that need fixing.
682 """
Yuto Takano39639672021-08-05 19:47:48 +0100683 problems = []
Yuto Takano39639672021-08-05 19:47:48 +0100684
Yuto Takanod93fa372021-08-06 23:05:55 +0100685 # Set comprehension, equivalent to a list comprehension inside set()
686 all_caps_names = {
687 match.name
688 for match
689 in self.parse_result["macros"] + self.parse_result["enum_consts"]}
690 typo_exclusion = re.compile(r"XXX|__|_$|^MBEDTLS_.*CONFIG_FILE$")
Yuto Takano39639672021-08-05 19:47:48 +0100691
Yuto Takanod93fa372021-08-06 23:05:55 +0100692 for name_match in self.parse_result["mbed_words"]:
Yuto Takano81528c02021-08-06 16:22:06 +0100693 found = name_match.name in all_caps_names
694
695 # Since MBEDTLS_PSA_ACCEL_XXX defines are defined by the
696 # PSA driver, they will not exist as macros. However, they
697 # should still be checked for typos using the equivalent
698 # BUILTINs that exist.
699 if "MBEDTLS_PSA_ACCEL_" in name_match.name:
700 found = name_match.name.replace(
701 "MBEDTLS_PSA_ACCEL_",
702 "MBEDTLS_PSA_BUILTIN_") in all_caps_names
703
Yuto Takanod93fa372021-08-06 23:05:55 +0100704 if not found and not typo_exclusion.search(name_match.name):
Yuto Takano55614b52021-08-07 01:00:18 +0100705 problems.append(Typo(name_match, quiet=quiet))
Yuto Takano39639672021-08-05 19:47:48 +0100706
Yuto Takano55614b52021-08-07 01:00:18 +0100707 self.output_check_result("Likely typos", problems)
Yuto Takano81528c02021-08-06 16:22:06 +0100708 return len(problems)
709
Yuto Takano55614b52021-08-07 01:00:18 +0100710 def output_check_result(self, name, problems):
Yuto Takano81528c02021-08-06 16:22:06 +0100711 """
712 Write out the PASS/FAIL status of a performed check depending on whether
713 there were problems.
Yuto Takano81528c02021-08-06 16:22:06 +0100714 """
Yuto Takano39639672021-08-05 19:47:48 +0100715 if problems:
Darryl Greend5802922018-05-08 15:30:59 +0100716 self.set_return_code(1)
Yuto Takano55614b52021-08-07 01:00:18 +0100717 self.log.info("{}: FAIL\n".format(name))
718 for problem in problems:
719 self.log.warning(str(problem))
Darryl Greend5802922018-05-08 15:30:59 +0100720 else:
Yuto Takano81528c02021-08-06 16:22:06 +0100721 self.log.info("{}: PASS".format(name))
Darryl Greend5802922018-05-08 15:30:59 +0100722
Yuto Takano39639672021-08-05 19:47:48 +0100723def main():
724 """
Yuto Takano81528c02021-08-06 16:22:06 +0100725 Perform argument parsing, and create an instance of NameCheck to begin the
726 core operation.
Yuto Takano39639672021-08-05 19:47:48 +0100727 """
Yuto Takano39639672021-08-05 19:47:48 +0100728 parser = argparse.ArgumentParser(
729 formatter_class=argparse.RawDescriptionHelpFormatter,
730 description=(
731 "This script confirms that the naming of all symbols and identifiers "
732 "in Mbed TLS are consistent with the house style and are also "
733 "self-consistent.\n\n"
734 "Expected to be run from the MbedTLS root directory."))
Darryl Greend5802922018-05-08 15:30:59 +0100735
Yuto Takano39639672021-08-05 19:47:48 +0100736 parser.add_argument("-v", "--verbose",
737 action="store_true",
Yuto Takano81528c02021-08-06 16:22:06 +0100738 help="show parse results")
739
740 parser.add_argument("-q", "--quiet",
741 action="store_true",
Yuto Takano55614b52021-08-07 01:00:18 +0100742 help="hide unnecessary text, explanations, and highlighs")
Yuto Takano81528c02021-08-06 16:22:06 +0100743
Yuto Takano39639672021-08-05 19:47:48 +0100744 args = parser.parse_args()
Darryl Greend5802922018-05-08 15:30:59 +0100745
Darryl Greend5802922018-05-08 15:30:59 +0100746 try:
747 name_check = NameCheck()
Yuto Takano39639672021-08-05 19:47:48 +0100748 name_check.setup_logger(verbose=args.verbose)
749 name_check.parse_names_in_source()
Yuto Takano55614b52021-08-07 01:00:18 +0100750 name_check.perform_checks(quiet=args.quiet)
Yuto Takano81528c02021-08-06 16:22:06 +0100751 sys.exit(name_check.return_code)
Yuto Takanod93fa372021-08-06 23:05:55 +0100752 except Exception: # pylint: disable=broad-except
Darryl Greend5802922018-05-08 15:30:59 +0100753 traceback.print_exc()
754 sys.exit(2)
755
Darryl Greend5802922018-05-08 15:30:59 +0100756if __name__ == "__main__":
Yuto Takano39639672021-08-05 19:47:48 +0100757 main()