blob: 78635699099c22a4730dea36a061c29f7dcce063 [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):
66 return (
67 " |\n" +
68 " | {}".format(self.line) +
Yuto Takanod93fa372021-08-06 23:05:55 +010069 " | " + self.pos[1] * " " + (self.pos[2] - self.pos[1]) * "^"
Yuto Takanoa4e75122021-08-06 17:23:28 +010070 )
Yuto Takanod93fa372021-08-06 23:05:55 +010071
72class Problem(): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010073 """
74 A parent class representing a form of static analysis error.
75
76 Fields:
77 * textwrapper: a TextWrapper instance to format problems nicely.
78 """
Yuto Takano39639672021-08-05 19:47:48 +010079 def __init__(self):
80 self.textwrapper = textwrap.TextWrapper()
Yuto Takano81528c02021-08-06 16:22:06 +010081 self.textwrapper.width = 80
Yuto Takanoa4e75122021-08-06 17:23:28 +010082 self.textwrapper.initial_indent = " > "
Yuto Takano81528c02021-08-06 16:22:06 +010083 self.textwrapper.subsequent_indent = " "
Yuto Takano39639672021-08-05 19:47:48 +010084
Yuto Takanod93fa372021-08-06 23:05:55 +010085class SymbolNotInHeader(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +010086 """
87 A problem that occurs when an exported/available symbol in the object file
88 is not explicitly declared in header files. Created with
89 NameCheck.check_symbols_declared_in_header()
90
91 Fields:
92 * symbol_name: the name of the symbol.
93 """
Yuto Takano39639672021-08-05 19:47:48 +010094 def __init__(self, symbol_name):
95 self.symbol_name = symbol_name
96 Problem.__init__(self)
97
98 def __str__(self):
99 return self.textwrapper.fill(
100 "'{0}' was found as an available symbol in the output of nm, "
101 "however it was not declared in any header files."
102 .format(self.symbol_name))
103
Yuto Takanod93fa372021-08-06 23:05:55 +0100104class PatternMismatch(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +0100105 """
106 A problem that occurs when something doesn't match the expected pattern.
107 Created with NameCheck.check_match_pattern()
108
109 Fields:
110 * pattern: the expected regex pattern
111 * match: the Match object in question
112 """
Yuto Takano39639672021-08-05 19:47:48 +0100113 def __init__(self, pattern, match):
114 self.pattern = pattern
115 self.match = match
116 Problem.__init__(self)
Yuto Takano81528c02021-08-06 16:22:06 +0100117
Yuto Takano39639672021-08-05 19:47:48 +0100118 def __str__(self):
119 return self.textwrapper.fill(
Yuto Takanoa4e75122021-08-06 17:23:28 +0100120 "{0}:{1}: '{2}' does not match the required pattern '{3}'."
121 .format(
122 self.match.filename,
Yuto Takanod93fa372021-08-06 23:05:55 +0100123 self.match.pos[0],
Yuto Takanoa4e75122021-08-06 17:23:28 +0100124 self.match.name,
125 self.pattern)) + "\n" + str(self.match)
Yuto Takano39639672021-08-05 19:47:48 +0100126
Yuto Takanod93fa372021-08-06 23:05:55 +0100127class Typo(Problem): # pylint: disable=too-few-public-methods
Yuto Takano81528c02021-08-06 16:22:06 +0100128 """
129 A problem that occurs when a word using MBED doesn't appear to be defined as
130 constants nor enum values. Created with NameCheck.check_for_typos()
131
132 Fields:
133 * match: the Match object of the MBED name in question.
134 """
Yuto Takano39639672021-08-05 19:47:48 +0100135 def __init__(self, match):
136 self.match = match
137 Problem.__init__(self)
Yuto Takano81528c02021-08-06 16:22:06 +0100138
Yuto Takano39639672021-08-05 19:47:48 +0100139 def __str__(self):
140 return self.textwrapper.fill(
Yuto Takanoa4e75122021-08-06 17:23:28 +0100141 "{0}:{1}: '{2}' looks like a typo. It was not found in any "
142 "macros or any enums. If this is not a typo, put "
143 "//no-check-names after it."
144 .format(
145 self.match.filename,
Yuto Takanod93fa372021-08-06 23:05:55 +0100146 self.match.pos[0],
Yuto Takanoa4e75122021-08-06 17:23:28 +0100147 self.match.name)) + "\n" + str(self.match)
Darryl Greend5802922018-05-08 15:30:59 +0100148
Yuto Takanod93fa372021-08-06 23:05:55 +0100149class NameCheck():
Yuto Takano81528c02021-08-06 16:22:06 +0100150 """
151 Representation of the core name checking operation performed by this script.
152 Shares a common logger, common excluded filenames, and a shared return_code.
153 """
Darryl Greend5802922018-05-08 15:30:59 +0100154 def __init__(self):
155 self.log = None
Darryl Greend5802922018-05-08 15:30:59 +0100156 self.return_code = 0
Yuto Takano81528c02021-08-06 16:22:06 +0100157 self.excluded_files = ["bn_mul", "compat-2.x.h"]
Yuto Takanod93fa372021-08-06 23:05:55 +0100158 self.parse_result = {}
Darryl Greend5802922018-05-08 15:30:59 +0100159
160 def set_return_code(self, return_code):
161 if return_code > self.return_code:
Yuto Takano201f9e82021-08-06 16:36:54 +0100162 self.log.debug("Setting new return code to {}".format(return_code))
Darryl Greend5802922018-05-08 15:30:59 +0100163 self.return_code = return_code
164
Yuto Takano39639672021-08-05 19:47:48 +0100165 def setup_logger(self, verbose=False):
166 """
167 Set up a logger and set the change the default logging level from
Yuto Takano81528c02021-08-06 16:22:06 +0100168 WARNING to INFO. Loggers are better than print statements since their
Yuto Takano39639672021-08-05 19:47:48 +0100169 verbosity can be controlled.
170 """
Darryl Greend5802922018-05-08 15:30:59 +0100171 self.log = logging.getLogger()
Yuto Takano39639672021-08-05 19:47:48 +0100172 if verbose:
173 self.log.setLevel(logging.DEBUG)
174 else:
175 self.log.setLevel(logging.INFO)
Darryl Greend5802922018-05-08 15:30:59 +0100176 self.log.addHandler(logging.StreamHandler())
177
Yuto Takano157444c2021-08-05 20:10:45 +0100178 def get_files(self, extension, directory):
Yuto Takano81528c02021-08-06 16:22:06 +0100179 """
180 Get all files that end with .extension in the specified directory
181 recursively.
182
183 Args:
184 * extension: the file extension to search for, without the dot
185 * directory: the directory to recursively search for
186
187 Returns a List of relative filepaths.
188 """
Darryl Greend5802922018-05-08 15:30:59 +0100189 filenames = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100190 for root, _, files in sorted(os.walk(directory)):
Darryl Greend5802922018-05-08 15:30:59 +0100191 for filename in sorted(files):
192 if (filename not in self.excluded_files and
Yuto Takano157444c2021-08-05 20:10:45 +0100193 filename.endswith("." + extension)):
Darryl Greend5802922018-05-08 15:30:59 +0100194 filenames.append(os.path.join(root, filename))
195 return filenames
196
Yuto Takano81528c02021-08-06 16:22:06 +0100197 def parse_names_in_source(self):
198 """
199 Calls each parsing function to retrieve various elements of the code,
200 together with their source location. Puts the parsed values in the
201 internal variable self.parse_result.
202 """
203 self.log.info("Parsing source code...")
Yuto Takanod24e0372021-08-06 16:42:33 +0100204 self.log.debug(
205 "The following files are excluded from the search: {}"
206 .format(str(self.excluded_files))
207 )
Yuto Takano81528c02021-08-06 16:22:06 +0100208
209 m_headers = self.get_files("h", os.path.join("include", "mbedtls"))
210 p_headers = self.get_files("h", os.path.join("include", "psa"))
211 t_headers = ["3rdparty/everest/include/everest/everest.h",
212 "3rdparty/everest/include/everest/x25519.h"]
213 d_headers = self.get_files("h", os.path.join("tests", "include", "test", "drivers"))
214 l_headers = self.get_files("h", "library")
215 libraries = self.get_files("c", "library") + [
216 "3rdparty/everest/library/everest.c",
217 "3rdparty/everest/library/x25519.c"]
218
219 all_macros = self.parse_macros(
220 m_headers + p_headers + t_headers + l_headers + d_headers)
221 enum_consts = self.parse_enum_consts(
222 m_headers + l_headers + t_headers)
223 identifiers = self.parse_identifiers(
224 m_headers + p_headers + t_headers + l_headers)
Yuto Takanod93fa372021-08-06 23:05:55 +0100225 mbed_words = self.parse_mbed_words(
Yuto Takano81528c02021-08-06 16:22:06 +0100226 m_headers + p_headers + t_headers + l_headers + libraries)
227 symbols = self.parse_symbols()
228
229 # Remove identifier macros like mbedtls_printf or mbedtls_calloc
230 identifiers_justname = [x.name for x in identifiers]
231 actual_macros = []
232 for macro in all_macros:
233 if macro.name not in identifiers_justname:
234 actual_macros.append(macro)
235
236 self.log.debug("Found:")
237 self.log.debug(" {} Macros".format(len(all_macros)))
238 self.log.debug(" {} Non-identifier Macros".format(len(actual_macros)))
239 self.log.debug(" {} Enum Constants".format(len(enum_consts)))
240 self.log.debug(" {} Identifiers".format(len(identifiers)))
241 self.log.debug(" {} Exported Symbols".format(len(symbols)))
242 self.log.info("Analysing...")
243
244 self.parse_result = {
245 "macros": actual_macros,
246 "enum_consts": enum_consts,
247 "identifiers": identifiers,
248 "symbols": symbols,
Yuto Takanod93fa372021-08-06 23:05:55 +0100249 "mbed_words": mbed_words
Yuto Takano81528c02021-08-06 16:22:06 +0100250 }
251
Yuto Takano39639672021-08-05 19:47:48 +0100252 def parse_macros(self, header_files):
253 """
254 Parse all macros defined by #define preprocessor directives.
255
256 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100257 * header_files: A List of filepaths to look through.
258
259 Returns a List of Match objects for the found macros.
Yuto Takano39639672021-08-05 19:47:48 +0100260 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100261 macro_regex = re.compile(r"# *define +(?P<macro>\w+)")
262 exclusions = (
Yuto Takano39639672021-08-05 19:47:48 +0100263 "asm", "inline", "EMIT", "_CRT_SECURE_NO_DEPRECATE", "MULADDC_"
264 )
265
Yuto Takano201f9e82021-08-06 16:36:54 +0100266 self.log.debug("Looking for macros in {} files".format(len(header_files)))
Yuto Takanod93fa372021-08-06 23:05:55 +0100267
268 macros = []
269
Yuto Takano39639672021-08-05 19:47:48 +0100270 for header_file in header_files:
Darryl Greend5802922018-05-08 15:30:59 +0100271 with open(header_file, "r") as header:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100272 for line_no, line in enumerate(header):
Yuto Takanod93fa372021-08-06 23:05:55 +0100273 for macro in macro_regex.finditer(line):
274 if not macro.group("macro").startswith(exclusions):
Yuto Takano81528c02021-08-06 16:22:06 +0100275 macros.append(Match(
276 header_file,
277 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100278 (line_no, macro.start(), macro.end()),
Yuto Takano81528c02021-08-06 16:22:06 +0100279 macro.group("macro")))
Darryl Greend5802922018-05-08 15:30:59 +0100280
Yuto Takano39639672021-08-05 19:47:48 +0100281 return macros
Darryl Greend5802922018-05-08 15:30:59 +0100282
Yuto Takanod93fa372021-08-06 23:05:55 +0100283 def parse_mbed_words(self, files):
Yuto Takano39639672021-08-05 19:47:48 +0100284 """
285 Parse all words in the file that begin with MBED. Includes macros.
Yuto Takano81528c02021-08-06 16:22:06 +0100286 There have been typos of TLS, hence the broader check than MBEDTLS.
Yuto Takano39639672021-08-05 19:47:48 +0100287
288 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100289 * files: a List of filepaths to look through.
290
291 Returns a List of Match objects for words beginning with MBED.
Yuto Takano39639672021-08-05 19:47:48 +0100292 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100293 mbed_regex = re.compile(r"\bMBED.+?_[A-Z0-9_]*")
294 exclusions = re.compile(r"// *no-check-names|#error")
295
Yuto Takano201f9e82021-08-06 16:36:54 +0100296 self.log.debug("Looking for MBED names in {} files".format(len(files)))
Yuto Takanod93fa372021-08-06 23:05:55 +0100297
298 mbed_words = []
299
Yuto Takanobb7dca42021-08-05 19:57:58 +0100300 for filename in files:
Yuto Takano39639672021-08-05 19:47:48 +0100301 with open(filename, "r") as fp:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100302 for line_no, line in enumerate(fp):
Yuto Takanod93fa372021-08-06 23:05:55 +0100303 if exclusions.search(line):
Yuto Takanoc62b4082021-08-05 20:17:07 +0100304 continue
Yuto Takano81528c02021-08-06 16:22:06 +0100305
Yuto Takanod93fa372021-08-06 23:05:55 +0100306 for name in mbed_regex.finditer(line):
307 mbed_words.append(Match(
Yuto Takano39639672021-08-05 19:47:48 +0100308 filename,
309 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100310 (line_no, name.start(), name.end()),
Yuto Takano39639672021-08-05 19:47:48 +0100311 name.group(0)
312 ))
313
Yuto Takanod93fa372021-08-06 23:05:55 +0100314 return mbed_words
Yuto Takano39639672021-08-05 19:47:48 +0100315
316 def parse_enum_consts(self, header_files):
317 """
318 Parse all enum value constants that are declared.
319
320 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100321 * header_files: A List of filepaths to look through.
Yuto Takano39639672021-08-05 19:47:48 +0100322
Yuto Takano81528c02021-08-06 16:22:06 +0100323 Returns a List of Match objects for the findings.
Yuto Takano39639672021-08-05 19:47:48 +0100324 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100325 self.log.debug("Looking for enum consts in {} files".format(len(header_files)))
Yuto Takano39639672021-08-05 19:47:48 +0100326
327 enum_consts = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100328
Yuto Takano39639672021-08-05 19:47:48 +0100329 for header_file in header_files:
330 # Emulate a finite state machine to parse enum declarations.
Yuto Takano81528c02021-08-06 16:22:06 +0100331 # 0 = not in enum
332 # 1 = inside enum
333 # 2 = almost inside enum
Darryl Greend5802922018-05-08 15:30:59 +0100334 state = 0
335 with open(header_file, "r") as header:
Yuto Takano8f457cf2021-08-06 17:54:58 +0100336 for line_no, line in enumerate(header):
Yuto Takano13ecd992021-08-06 16:56:52 +0100337 # Match typedefs and brackets only when they are at the
338 # beginning of the line -- if they are indented, they might
339 # be sub-structures within structs, etc.
Yuto Takanod93fa372021-08-06 23:05:55 +0100340 if state == 0 and re.match(r"^(typedef +)?enum +{", line):
Darryl Greend5802922018-05-08 15:30:59 +0100341 state = 1
Yuto Takanod93fa372021-08-06 23:05:55 +0100342 elif state == 0 and re.match(r"^(typedef +)?enum", line):
Darryl Greend5802922018-05-08 15:30:59 +0100343 state = 2
Yuto Takanod93fa372021-08-06 23:05:55 +0100344 elif state == 2 and re.match(r"^{", line):
Darryl Greend5802922018-05-08 15:30:59 +0100345 state = 1
Yuto Takanod93fa372021-08-06 23:05:55 +0100346 elif state == 1 and re.match(r"^}", line):
Darryl Greend5802922018-05-08 15:30:59 +0100347 state = 0
Yuto Takanod93fa372021-08-06 23:05:55 +0100348 elif state == 1 and not re.match(r" *#", line):
Yuto Takano13ecd992021-08-06 16:56:52 +0100349 enum_const = re.match(r" *(?P<enum_const>\w+)", line)
Darryl Greend5802922018-05-08 15:30:59 +0100350 if enum_const:
Yuto Takano39639672021-08-05 19:47:48 +0100351 enum_consts.append(Match(
352 header_file,
353 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100354 (line_no, enum_const.start(), enum_const.end()),
Yuto Takano39639672021-08-05 19:47:48 +0100355 enum_const.group("enum_const")))
Yuto Takano81528c02021-08-06 16:22:06 +0100356
Yuto Takano39639672021-08-05 19:47:48 +0100357 return enum_consts
Darryl Greend5802922018-05-08 15:30:59 +0100358
Yuto Takano39639672021-08-05 19:47:48 +0100359 def parse_identifiers(self, header_files):
360 """
361 Parse all lines of a header where a function identifier is declared,
Yuto Takano81528c02021-08-06 16:22:06 +0100362 based on some huersitics. Highly dependent on formatting style.
Darryl Greend5802922018-05-08 15:30:59 +0100363
Yuto Takano39639672021-08-05 19:47:48 +0100364 Args:
Yuto Takano81528c02021-08-06 16:22:06 +0100365 * header_files: A List of filepaths to look through.
366
367 Returns a List of Match objects with identifiers.
Yuto Takano39639672021-08-05 19:47:48 +0100368 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100369 identifier_regex = re.compile(
370 # Match " something(a" or " *something(a". Functions.
371 # Assumptions:
372 # - function definition from return type to one of its arguments is
373 # all on one line (enforced by the previous_line concat below)
374 # - function definition line only contains alphanumeric, asterisk,
375 # underscore, and open bracket
376 r".* \**(\w+) *\( *\w|"
377 # Match "(*something)(". Flexible with spaces.
378 r".*\( *\* *(\w+) *\) *\(|"
379 # Match names of named data structures.
380 r"(?:typedef +)?(?:struct|union|enum) +(\w+)(?: *{)?$|"
381 # Match names of typedef instances, after closing bracket.
382 r"}? *(\w+)[;[].*")
383 exclusion_lines = re.compile(r"^("
384 r"extern +\"C\"|"
385 r"(typedef +)?(struct|union|enum)( *{)?$|"
386 r"} *;?$|"
387 r"$|"
388 r"//|"
389 r"#"
390 r")")
391
392 self.log.debug("Looking for identifiers in {} files".format(len(header_files)))
Darryl Greend5802922018-05-08 15:30:59 +0100393
Yuto Takano39639672021-08-05 19:47:48 +0100394 identifiers = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100395
Yuto Takano39639672021-08-05 19:47:48 +0100396 for header_file in header_files:
Darryl Greend5802922018-05-08 15:30:59 +0100397 with open(header_file, "r") as header:
Yuto Takano39639672021-08-05 19:47:48 +0100398 in_block_comment = False
Yuto Takanod93fa372021-08-06 23:05:55 +0100399 previous_line = ""
Darryl Greend5802922018-05-08 15:30:59 +0100400
Yuto Takano8f457cf2021-08-06 17:54:58 +0100401 for line_no, line in enumerate(header):
Yuto Takano81528c02021-08-06 16:22:06 +0100402 # Skip parsing this line if a block comment ends on it,
403 # but don't skip if it has just started -- there is a chance
404 # it ends on the same line.
Yuto Takano39639672021-08-05 19:47:48 +0100405 if re.search(r"/\*", line):
Yuto Takano81528c02021-08-06 16:22:06 +0100406 in_block_comment = not in_block_comment
407 if re.search(r"\*/", line):
408 in_block_comment = not in_block_comment
Yuto Takano39639672021-08-05 19:47:48 +0100409 continue
410
Yuto Takano81528c02021-08-06 16:22:06 +0100411 if in_block_comment:
Yuto Takanod93fa372021-08-06 23:05:55 +0100412 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100413 continue
414
Yuto Takanod93fa372021-08-06 23:05:55 +0100415 if exclusion_lines.match(line):
416 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100417 continue
418
Yuto Takanocfc9e4a2021-08-06 20:02:32 +0100419 # If the line contains only space-separated alphanumeric
420 # characters (or underscore, asterisk, or, open bracket),
421 # and nothing else, high chance it's a declaration that
422 # continues on the next line
423 if re.match(r"^([\w\*\(]+\s+)+$", line):
Yuto Takanod93fa372021-08-06 23:05:55 +0100424 previous_line += line
Yuto Takano81528c02021-08-06 16:22:06 +0100425 continue
426
427 # If previous line seemed to start an unfinished declaration
Yuto Takanocfc9e4a2021-08-06 20:02:32 +0100428 # (as above), concat and treat them as one.
429 if previous_line:
430 line = previous_line.strip() + " " + line.strip()
Yuto Takanod93fa372021-08-06 23:05:55 +0100431 previous_line = ""
Yuto Takano81528c02021-08-06 16:22:06 +0100432
433 # Skip parsing if line has a space in front = hueristic to
434 # skip function argument lines (highly subject to formatting
435 # changes)
436 if line[0] == " ":
Yuto Takano39639672021-08-05 19:47:48 +0100437 continue
Yuto Takano6f38ab32021-08-05 21:07:14 +0100438
Yuto Takanod93fa372021-08-06 23:05:55 +0100439 identifier = identifier_regex.search(line)
Yuto Takano39639672021-08-05 19:47:48 +0100440
441 if identifier:
Yuto Takano81528c02021-08-06 16:22:06 +0100442 # Find the group that matched, and append it
Yuto Takano39639672021-08-05 19:47:48 +0100443 for group in identifier.groups():
444 if group:
445 identifiers.append(Match(
446 header_file,
447 line,
Yuto Takanod93fa372021-08-06 23:05:55 +0100448 (line_no, identifier.start(), identifier.end()),
Yuto Takano81528c02021-08-06 16:22:06 +0100449 group))
Yuto Takano39639672021-08-05 19:47:48 +0100450
451 return identifiers
452
453 def parse_symbols(self):
454 """
455 Compile the Mbed TLS libraries, and parse the TLS, Crypto, and x509
456 object files using nm to retrieve the list of referenced symbols.
Yuto Takano81528c02021-08-06 16:22:06 +0100457 Exceptions thrown here are rethrown because they would be critical
458 errors that void several tests, and thus needs to halt the program. This
459 is explicitly done for clarity.
Yuto Takano39639672021-08-05 19:47:48 +0100460
Yuto Takano81528c02021-08-06 16:22:06 +0100461 Returns a List of unique symbols defined and used in the libraries.
462 """
463 self.log.info("Compiling...")
Yuto Takano39639672021-08-05 19:47:48 +0100464 symbols = []
465
466 # Back up the config and atomically compile with the full configratuion.
467 shutil.copy("include/mbedtls/mbedtls_config.h",
Yuto Takano81528c02021-08-06 16:22:06 +0100468 "include/mbedtls/mbedtls_config.h.bak")
Darryl Greend5802922018-05-08 15:30:59 +0100469 try:
Yuto Takano81528c02021-08-06 16:22:06 +0100470 # Use check=True in all subprocess calls so that failures are raised
471 # as exceptions and logged.
Yuto Takano39639672021-08-05 19:47:48 +0100472 subprocess.run(
Yuto Takano81528c02021-08-06 16:22:06 +0100473 ["python3", "scripts/config.py", "full"],
Yuto Takano39639672021-08-05 19:47:48 +0100474 encoding=sys.stdout.encoding,
475 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100476 )
477 my_environment = os.environ.copy()
478 my_environment["CFLAGS"] = "-fno-asynchronous-unwind-tables"
Yuto Takano39639672021-08-05 19:47:48 +0100479 subprocess.run(
Darryl Greend5802922018-05-08 15:30:59 +0100480 ["make", "clean", "lib"],
481 env=my_environment,
Yuto Takano39639672021-08-05 19:47:48 +0100482 encoding=sys.stdout.encoding,
483 stdout=subprocess.PIPE,
Darryl Greend5802922018-05-08 15:30:59 +0100484 stderr=subprocess.STDOUT,
Yuto Takano39639672021-08-05 19:47:48 +0100485 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100486 )
Yuto Takano39639672021-08-05 19:47:48 +0100487
488 # Perform object file analysis using nm
489 symbols = self.parse_symbols_from_nm(
490 ["library/libmbedcrypto.a",
Yuto Takanod93fa372021-08-06 23:05:55 +0100491 "library/libmbedtls.a",
492 "library/libmbedx509.a"])
Yuto Takano39639672021-08-05 19:47:48 +0100493
494 subprocess.run(
Darryl Greend5802922018-05-08 15:30:59 +0100495 ["make", "clean"],
Yuto Takano39639672021-08-05 19:47:48 +0100496 encoding=sys.stdout.encoding,
497 check=True
Darryl Greend5802922018-05-08 15:30:59 +0100498 )
499 except subprocess.CalledProcessError as error:
Yuto Takano25eeb7b2021-08-06 21:27:59 +0100500 self.log.debug(error.output)
Darryl Greend5802922018-05-08 15:30:59 +0100501 self.set_return_code(2)
Yuto Takano81528c02021-08-06 16:22:06 +0100502 raise error
Yuto Takano39639672021-08-05 19:47:48 +0100503 finally:
504 shutil.move("include/mbedtls/mbedtls_config.h.bak",
505 "include/mbedtls/mbedtls_config.h")
506
507 return symbols
508
509 def parse_symbols_from_nm(self, object_files):
510 """
511 Run nm to retrieve the list of referenced symbols in each object file.
512 Does not return the position data since it is of no use.
513
Yuto Takano81528c02021-08-06 16:22:06 +0100514 Args:
515 * object_files: a List of compiled object files to search through.
516
517 Returns a List of unique symbols defined and used in any of the object
518 files.
Yuto Takano39639672021-08-05 19:47:48 +0100519 """
Yuto Takanod93fa372021-08-06 23:05:55 +0100520 nm_undefined_regex = re.compile(r"^\S+: +U |^$|^\S+:$")
521 nm_valid_regex = re.compile(r"^\S+( [0-9A-Fa-f]+)* . _*(?P<symbol>\w+)")
522 nm_exclusions = ("FStar", "Hacl")
Yuto Takano39639672021-08-05 19:47:48 +0100523
524 symbols = []
525
Yuto Takano81528c02021-08-06 16:22:06 +0100526 # Gather all outputs of nm
Yuto Takano39639672021-08-05 19:47:48 +0100527 nm_output = ""
528 for lib in object_files:
529 nm_output += subprocess.run(
530 ["nm", "-og", lib],
531 encoding=sys.stdout.encoding,
532 stdout=subprocess.PIPE,
533 stderr=subprocess.STDOUT,
534 check=True
535 ).stdout
Yuto Takano81528c02021-08-06 16:22:06 +0100536
Yuto Takano39639672021-08-05 19:47:48 +0100537 for line in nm_output.splitlines():
Yuto Takanod93fa372021-08-06 23:05:55 +0100538 if not nm_undefined_regex.match(line):
539 symbol = nm_valid_regex.match(line)
540 if (symbol and not symbol.group("symbol").startswith(
541 nm_exclusions)):
Yuto Takanoe77f6992021-08-05 20:22:59 +0100542 symbols.append(symbol.group("symbol"))
Yuto Takano39639672021-08-05 19:47:48 +0100543 else:
544 self.log.error(line)
Yuto Takano81528c02021-08-06 16:22:06 +0100545
Yuto Takano39639672021-08-05 19:47:48 +0100546 return symbols
547
Yuto Takano9e0e0e92021-08-06 22:01:37 +0100548 def perform_checks(self, show_problems=True):
Yuto Takano39639672021-08-05 19:47:48 +0100549 """
550 Perform each check in order, output its PASS/FAIL status. Maintain an
551 overall test status, and output that at the end.
Yuto Takano81528c02021-08-06 16:22:06 +0100552
553 Args:
554 * show_problems: whether to show the problematic examples.
Yuto Takano39639672021-08-05 19:47:48 +0100555 """
Yuto Takano81528c02021-08-06 16:22:06 +0100556 self.log.info("=============")
Yuto Takano39639672021-08-05 19:47:48 +0100557 problems = 0
558
Yuto Takano81528c02021-08-06 16:22:06 +0100559 problems += self.check_symbols_declared_in_header(show_problems)
Yuto Takano39639672021-08-05 19:47:48 +0100560
Yuto Takanod93fa372021-08-06 23:05:55 +0100561 pattern_checks = [("macros", MACRO_PATTERN),
562 ("enum_consts", CONSTANTS_PATTERN),
563 ("identifiers", IDENTIFIER_PATTERN)]
Yuto Takano39639672021-08-05 19:47:48 +0100564 for group, check_pattern in pattern_checks:
Yuto Takano81528c02021-08-06 16:22:06 +0100565 problems += self.check_match_pattern(
566 show_problems, group, check_pattern)
Yuto Takano39639672021-08-05 19:47:48 +0100567
Yuto Takano81528c02021-08-06 16:22:06 +0100568 problems += self.check_for_typos(show_problems)
Yuto Takano39639672021-08-05 19:47:48 +0100569
570 self.log.info("=============")
571 if problems > 0:
572 self.log.info("FAIL: {0} problem(s) to fix".format(str(problems)))
Yuto Takano81528c02021-08-06 16:22:06 +0100573 if not show_problems:
574 self.log.info("Remove --quiet to show the problems.")
Yuto Takano39639672021-08-05 19:47:48 +0100575 else:
576 self.log.info("PASS")
Darryl Greend5802922018-05-08 15:30:59 +0100577
Yuto Takano81528c02021-08-06 16:22:06 +0100578 def check_symbols_declared_in_header(self, show_problems):
Yuto Takano39639672021-08-05 19:47:48 +0100579 """
580 Perform a check that all detected symbols in the library object files
581 are properly declared in headers.
Darryl Greend5802922018-05-08 15:30:59 +0100582
Yuto Takano81528c02021-08-06 16:22:06 +0100583 Args:
584 * show_problems: whether to show the problematic examples.
585
586 Returns the number of problems that need fixing.
Yuto Takano39639672021-08-05 19:47:48 +0100587 """
588 problems = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100589
Yuto Takano39639672021-08-05 19:47:48 +0100590 for symbol in self.parse_result["symbols"]:
591 found_symbol_declared = False
592 for identifier_match in self.parse_result["identifiers"]:
593 if symbol == identifier_match.name:
594 found_symbol_declared = True
595 break
Yuto Takano81528c02021-08-06 16:22:06 +0100596
Yuto Takano39639672021-08-05 19:47:48 +0100597 if not found_symbol_declared:
598 problems.append(SymbolNotInHeader(symbol))
599
Yuto Takano81528c02021-08-06 16:22:06 +0100600 self.output_check_result("All symbols in header", problems, show_problems)
Yuto Takano39639672021-08-05 19:47:48 +0100601 return len(problems)
602
Yuto Takano81528c02021-08-06 16:22:06 +0100603
604 def check_match_pattern(self, show_problems, group_to_check, check_pattern):
605 """
606 Perform a check that all items of a group conform to a regex pattern.
607
608 Args:
609 * show_problems: whether to show the problematic examples.
610 * group_to_check: string key to index into self.parse_result.
611 * check_pattern: the regex to check against.
612
613 Returns the number of problems that need fixing.
614 """
Yuto Takano39639672021-08-05 19:47:48 +0100615 problems = []
Yuto Takanod93fa372021-08-06 23:05:55 +0100616
Yuto Takano39639672021-08-05 19:47:48 +0100617 for item_match in self.parse_result[group_to_check]:
618 if not re.match(check_pattern, item_match.name):
619 problems.append(PatternMismatch(check_pattern, item_match))
Yuto Takano201f9e82021-08-06 16:36:54 +0100620 # Double underscore is a reserved identifier, never to be used
Yuto Takanoc763cc32021-08-05 20:06:34 +0100621 if re.match(r".*__.*", item_match.name):
622 problems.append(PatternMismatch("double underscore", item_match))
Yuto Takano81528c02021-08-06 16:22:06 +0100623
624 self.output_check_result(
625 "Naming patterns of {}".format(group_to_check),
626 problems,
627 show_problems)
Yuto Takano39639672021-08-05 19:47:48 +0100628 return len(problems)
Darryl Greend5802922018-05-08 15:30:59 +0100629
Yuto Takano81528c02021-08-06 16:22:06 +0100630 def check_for_typos(self, show_problems):
631 """
632 Perform a check that all words in the soure code beginning with MBED are
633 either defined as macros, or as enum constants.
634
635 Args:
636 * show_problems: whether to show the problematic examples.
637
638 Returns the number of problems that need fixing.
639 """
Yuto Takano39639672021-08-05 19:47:48 +0100640 problems = []
Yuto Takano39639672021-08-05 19:47:48 +0100641
Yuto Takanod93fa372021-08-06 23:05:55 +0100642 # Set comprehension, equivalent to a list comprehension inside set()
643 all_caps_names = {
644 match.name
645 for match
646 in self.parse_result["macros"] + self.parse_result["enum_consts"]}
647 typo_exclusion = re.compile(r"XXX|__|_$|^MBEDTLS_.*CONFIG_FILE$")
Yuto Takano39639672021-08-05 19:47:48 +0100648
Yuto Takanod93fa372021-08-06 23:05:55 +0100649 for name_match in self.parse_result["mbed_words"]:
Yuto Takano81528c02021-08-06 16:22:06 +0100650 found = name_match.name in all_caps_names
651
652 # Since MBEDTLS_PSA_ACCEL_XXX defines are defined by the
653 # PSA driver, they will not exist as macros. However, they
654 # should still be checked for typos using the equivalent
655 # BUILTINs that exist.
656 if "MBEDTLS_PSA_ACCEL_" in name_match.name:
657 found = name_match.name.replace(
658 "MBEDTLS_PSA_ACCEL_",
659 "MBEDTLS_PSA_BUILTIN_") in all_caps_names
660
Yuto Takanod93fa372021-08-06 23:05:55 +0100661 if not found and not typo_exclusion.search(name_match.name):
Yuto Takano201f9e82021-08-06 16:36:54 +0100662 problems.append(Typo(name_match))
Yuto Takano39639672021-08-05 19:47:48 +0100663
Yuto Takano81528c02021-08-06 16:22:06 +0100664 self.output_check_result("Likely typos", problems, show_problems)
665 return len(problems)
666
667 def output_check_result(self, name, problems, show_problems):
668 """
669 Write out the PASS/FAIL status of a performed check depending on whether
670 there were problems.
671
672 Args:
673 * show_problems: whether to show the problematic examples.
674 """
Yuto Takano39639672021-08-05 19:47:48 +0100675 if problems:
Darryl Greend5802922018-05-08 15:30:59 +0100676 self.set_return_code(1)
Yuto Takano81528c02021-08-06 16:22:06 +0100677 self.log.info("{}: FAIL".format(name))
678 if show_problems:
679 self.log.info("")
680 for problem in problems:
Yuto Takanod93fa372021-08-06 23:05:55 +0100681 self.log.warning("{}\n".format(str(problem)))
Darryl Greend5802922018-05-08 15:30:59 +0100682 else:
Yuto Takano81528c02021-08-06 16:22:06 +0100683 self.log.info("{}: PASS".format(name))
Darryl Greend5802922018-05-08 15:30:59 +0100684
Yuto Takanod93fa372021-08-06 23:05:55 +0100685def check_repo_path():
686 """
687 Check that the current working directory is the project root, and throw
688 an exception if not.
689 """
690 if (not os.path.isdir("include") or
691 not os.path.isdir("tests") or
692 not os.path.isdir("library")):
693 raise Exception("This script must be run from Mbed TLS root")
694
Yuto Takano39639672021-08-05 19:47:48 +0100695def main():
696 """
Yuto Takano81528c02021-08-06 16:22:06 +0100697 Perform argument parsing, and create an instance of NameCheck to begin the
698 core operation.
Yuto Takano39639672021-08-05 19:47:48 +0100699 """
Yuto Takano39639672021-08-05 19:47:48 +0100700 parser = argparse.ArgumentParser(
701 formatter_class=argparse.RawDescriptionHelpFormatter,
702 description=(
703 "This script confirms that the naming of all symbols and identifiers "
704 "in Mbed TLS are consistent with the house style and are also "
705 "self-consistent.\n\n"
706 "Expected to be run from the MbedTLS root directory."))
Darryl Greend5802922018-05-08 15:30:59 +0100707
Yuto Takano39639672021-08-05 19:47:48 +0100708 parser.add_argument("-v", "--verbose",
709 action="store_true",
Yuto Takano81528c02021-08-06 16:22:06 +0100710 help="show parse results")
711
712 parser.add_argument("-q", "--quiet",
713 action="store_true",
714 help="hide unnecessary text and problematic examples")
715
Yuto Takano39639672021-08-05 19:47:48 +0100716 args = parser.parse_args()
Darryl Greend5802922018-05-08 15:30:59 +0100717
Darryl Greend5802922018-05-08 15:30:59 +0100718 try:
Yuto Takanod93fa372021-08-06 23:05:55 +0100719 check_repo_path()
Darryl Greend5802922018-05-08 15:30:59 +0100720 name_check = NameCheck()
Yuto Takano39639672021-08-05 19:47:48 +0100721 name_check.setup_logger(verbose=args.verbose)
722 name_check.parse_names_in_source()
Yuto Takano81528c02021-08-06 16:22:06 +0100723 name_check.perform_checks(show_problems=not args.quiet)
724 sys.exit(name_check.return_code)
Yuto Takanod93fa372021-08-06 23:05:55 +0100725 except Exception: # pylint: disable=broad-except
Darryl Greend5802922018-05-08 15:30:59 +0100726 traceback.print_exc()
727 sys.exit(2)
728
Darryl Greend5802922018-05-08 15:30:59 +0100729if __name__ == "__main__":
Yuto Takano39639672021-08-05 19:47:48 +0100730 main()