blob: 024fb2210a647006ac956dcd1701b1c778f2a6f0 [file] [log] [blame]
Gilles Peskinee7c44552021-01-25 21:40:45 +01001"""Collect macro definitions from header files.
2"""
3
4# Copyright The Mbed TLS Contributors
5# SPDX-License-Identifier: Apache-2.0
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010019import itertools
Gilles Peskinee7c44552021-01-25 21:40:45 +010020import re
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +020021from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Set, Tuple, Union
22
23
24class ReadFileLineException(Exception):
25 def __init__(self, filename: str, line_number: Union[int, str]) -> None:
26 message = 'in {} at {}'.format(filename, line_number)
27 super(ReadFileLineException, self).__init__(message)
28 self.filename = filename
29 self.line_number = line_number
30
31
32class read_file_lines:
33 # Dear Pylint, conventionally, a context manager class name is lowercase.
34 # pylint: disable=invalid-name,too-few-public-methods
35 """Context manager to read a text file line by line.
36
37 ```
38 with read_file_lines(filename) as lines:
39 for line in lines:
40 process(line)
41 ```
42 is equivalent to
43 ```
44 with open(filename, 'r') as input_file:
45 for line in input_file:
46 process(line)
47 ```
48 except that if process(line) raises an exception, then the read_file_lines
49 snippet annotates the exception with the file name and line number.
50 """
51 def __init__(self, filename: str, binary: bool = False) -> None:
52 self.filename = filename
53 self.line_number = 'entry' #type: Union[int, str]
54 self.generator = None #type: Optional[Iterable[Tuple[int, str]]]
55 self.binary = binary
56 def __enter__(self) -> 'read_file_lines':
57 self.generator = enumerate(open(self.filename,
58 'rb' if self.binary else 'r'))
59 return self
60 def __iter__(self) -> Iterator[str]:
61 assert self.generator is not None
62 for line_number, content in self.generator:
63 self.line_number = line_number
64 yield content
65 self.line_number = 'exit'
66 def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
67 if exc_type is not None:
68 raise ReadFileLineException(self.filename, self.line_number) \
69 from exc_value
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010070
71
72class PSAMacroEnumerator:
73 """Information about constructors of various PSA Crypto types.
74
75 This includes macro names as well as information about their arguments
76 when applicable.
77
78 This class only provides ways to enumerate expressions that evaluate to
79 values of the covered types. Derived classes are expected to populate
80 the set of known constructors of each kind, as well as populate
81 `self.arguments_for` for arguments that are not of a kind that is
82 enumerated here.
83 """
Gilles Peskine4c7da692021-04-21 21:39:27 +020084 #pylint: disable=too-many-instance-attributes
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010085
86 def __init__(self) -> None:
87 """Set up an empty set of known constructor macros.
88 """
89 self.statuses = set() #type: Set[str]
Gilles Peskine4c7da692021-04-21 21:39:27 +020090 self.lifetimes = set() #type: Set[str]
91 self.locations = set() #type: Set[str]
92 self.persistence_levels = set() #type: Set[str]
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010093 self.algorithms = set() #type: Set[str]
94 self.ecc_curves = set() #type: Set[str]
95 self.dh_groups = set() #type: Set[str]
96 self.key_types = set() #type: Set[str]
97 self.key_usage_flags = set() #type: Set[str]
98 self.hash_algorithms = set() #type: Set[str]
99 self.mac_algorithms = set() #type: Set[str]
100 self.ka_algorithms = set() #type: Set[str]
101 self.kdf_algorithms = set() #type: Set[str]
102 self.aead_algorithms = set() #type: Set[str]
gabor-mezei-arm044fefc2021-06-24 10:16:44 +0200103 self.sign_algorithms = set() #type: Set[str]
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100104 # macro name -> list of argument names
105 self.argspecs = {} #type: Dict[str, List[str]]
106 # argument name -> list of values
107 self.arguments_for = {
108 'mac_length': [],
109 'min_mac_length': [],
110 'tag_length': [],
111 'min_tag_length': [],
112 } #type: Dict[str, List[str]]
Gilles Peskine46d3a372021-05-20 21:37:06 +0200113 # Whether to include intermediate macros in enumerations. Intermediate
114 # macros serve as category headers and are not valid values of their
115 # type. See `is_internal_name`.
116 # Always false in this class, may be set to true in derived classes.
Gilles Peskineb93f8542021-04-19 13:50:25 +0200117 self.include_intermediate = False
118
119 def is_internal_name(self, name: str) -> bool:
120 """Whether this is an internal macro. Internal macros will be skipped."""
121 if not self.include_intermediate:
122 if name.endswith('_BASE') or name.endswith('_NONE'):
123 return True
124 if '_CATEGORY_' in name:
125 return True
126 return name.endswith('_FLAG') or name.endswith('_MASK')
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100127
128 def gather_arguments(self) -> None:
129 """Populate the list of values for macro arguments.
130
131 Call this after parsing all the inputs.
132 """
133 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
134 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
135 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
136 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
137 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
gabor-mezei-arm044fefc2021-06-24 10:16:44 +0200138 self.arguments_for['sign_alg'] = sorted(self.sign_algorithms)
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100139 self.arguments_for['curve'] = sorted(self.ecc_curves)
140 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine4c7da692021-04-21 21:39:27 +0200141 self.arguments_for['persistence'] = sorted(self.persistence_levels)
142 self.arguments_for['location'] = sorted(self.locations)
143 self.arguments_for['lifetime'] = sorted(self.lifetimes)
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100144
145 @staticmethod
146 def _format_arguments(name: str, arguments: Iterable[str]) -> str:
Gilles Peskine0a93c1b2021-04-21 15:36:58 +0200147 """Format a macro call with arguments.
148
149 The resulting format is consistent with
150 `InputsForTest.normalize_argument`.
151 """
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100152 return name + '(' + ', '.join(arguments) + ')'
153
154 _argument_split_re = re.compile(r' *, *')
155 @classmethod
156 def _argument_split(cls, arguments: str) -> List[str]:
157 return re.split(cls._argument_split_re, arguments)
158
159 def distribute_arguments(self, name: str) -> Iterator[str]:
160 """Generate macro calls with each tested argument set.
161
162 If name is a macro without arguments, just yield "name".
163 If name is a macro with arguments, yield a series of
164 "name(arg1,...,argN)" where each argument takes each possible
165 value at least once.
166 """
167 try:
168 if name not in self.argspecs:
169 yield name
170 return
171 argspec = self.argspecs[name]
172 if argspec == []:
173 yield name + '()'
174 return
175 argument_lists = [self.arguments_for[arg] for arg in argspec]
176 arguments = [values[0] for values in argument_lists]
177 yield self._format_arguments(name, arguments)
178 # Dear Pylint, enumerate won't work here since we're modifying
179 # the array.
180 # pylint: disable=consider-using-enumerate
181 for i in range(len(arguments)):
182 for value in argument_lists[i][1:]:
183 arguments[i] = value
184 yield self._format_arguments(name, arguments)
185 arguments[i] = argument_lists[0][0]
186 except BaseException as e:
187 raise Exception('distribute_arguments({})'.format(name)) from e
188
Gilles Peskine08966e62021-04-21 15:37:34 +0200189 def distribute_arguments_without_duplicates(
190 self, seen: Set[str], name: str
191 ) -> Iterator[str]:
192 """Same as `distribute_arguments`, but don't repeat seen results."""
193 for result in self.distribute_arguments(name):
194 if result not in seen:
195 seen.add(result)
196 yield result
197
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100198 def generate_expressions(self, names: Iterable[str]) -> Iterator[str]:
199 """Generate expressions covering values constructed from the given names.
200
201 `names` can be any iterable collection of macro names.
202
203 For example:
204 * ``generate_expressions(['PSA_ALG_CMAC', 'PSA_ALG_HMAC'])``
205 generates ``'PSA_ALG_CMAC'`` as well as ``'PSA_ALG_HMAC(h)'`` for
206 every known hash algorithm ``h``.
207 * ``macros.generate_expressions(macros.key_types)`` generates all
208 key types.
209 """
Gilles Peskine08966e62021-04-21 15:37:34 +0200210 seen = set() #type: Set[str]
211 return itertools.chain(*(
212 self.distribute_arguments_without_duplicates(seen, name)
213 for name in names
214 ))
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100215
Gilles Peskinee7c44552021-01-25 21:40:45 +0100216
Gilles Peskine33c601c2021-03-10 01:25:50 +0100217class PSAMacroCollector(PSAMacroEnumerator):
Gilles Peskinee7c44552021-01-25 21:40:45 +0100218 """Collect PSA crypto macro definitions from C header files.
219 """
220
Gilles Peskine10ab2672021-03-10 00:59:53 +0100221 def __init__(self, include_intermediate: bool = False) -> None:
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100222 """Set up an object to collect PSA macro definitions.
223
224 Call the read_file method of the constructed object on each header file.
225
226 * include_intermediate: if true, include intermediate macros such as
227 PSA_XXX_BASE that do not designate semantic values.
228 """
Gilles Peskine33c601c2021-03-10 01:25:50 +0100229 super().__init__()
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100230 self.include_intermediate = include_intermediate
Gilles Peskine10ab2672021-03-10 00:59:53 +0100231 self.key_types_from_curve = {} #type: Dict[str, str]
232 self.key_types_from_group = {} #type: Dict[str, str]
Gilles Peskine10ab2672021-03-10 00:59:53 +0100233 self.algorithms_from_hash = {} #type: Dict[str, str]
Gilles Peskinee7c44552021-01-25 21:40:45 +0100234
Gilles Peskine35451032021-10-04 18:10:16 +0200235 @staticmethod
236 def algorithm_tester(name: str) -> str:
237 """The predicate for whether an algorithm is built from the given constructor.
238
239 The given name must be the name of an algorithm constructor of the
240 form ``PSA_ALG_xxx`` which is used as ``PSA_ALG_xxx(yyy)`` to build
241 an algorithm value. Return the corresponding predicate macro which
242 is used as ``predicate(alg)`` to test whether ``alg`` can be built
243 as ``PSA_ALG_xxx(yyy)``. The predicate is usually called
244 ``PSA_ALG_IS_xxx``.
245 """
246 prefix = 'PSA_ALG_'
247 assert name.startswith(prefix)
248 midfix = 'IS_'
249 suffix = name[len(prefix):]
250 if suffix in ['DSA', 'ECDSA']:
251 midfix += 'RANDOMIZED_'
252 return prefix + midfix + suffix
253
Gilles Peskine33c601c2021-03-10 01:25:50 +0100254 def record_algorithm_subtype(self, name: str, expansion: str) -> None:
255 """Record the subtype of an algorithm constructor.
256
257 Given a ``PSA_ALG_xxx`` macro name and its expansion, if the algorithm
258 is of a subtype that is tracked in its own set, add it to the relevant
259 set.
260 """
261 # This code is very ad hoc and fragile. It should be replaced by
262 # something more robust.
263 if re.match(r'MAC(?:_|\Z)', name):
264 self.mac_algorithms.add(name)
265 elif re.match(r'KDF(?:_|\Z)', name):
266 self.kdf_algorithms.add(name)
267 elif re.search(r'0x020000[0-9A-Fa-f]{2}', expansion):
268 self.hash_algorithms.add(name)
269 elif re.search(r'0x03[0-9A-Fa-f]{6}', expansion):
270 self.mac_algorithms.add(name)
271 elif re.search(r'0x05[0-9A-Fa-f]{6}', expansion):
272 self.aead_algorithms.add(name)
273 elif re.search(r'0x09[0-9A-Fa-f]{2}0000', expansion):
274 self.ka_algorithms.add(name)
275 elif re.search(r'0x08[0-9A-Fa-f]{6}', expansion):
276 self.kdf_algorithms.add(name)
277
Gilles Peskinee7c44552021-01-25 21:40:45 +0100278 # "#define" followed by a macro name with either no parameters
279 # or a single parameter and a non-empty expansion.
280 # Grab the macro name in group 1, the parameter name if any in group 2
281 # and the expansion in group 3.
282 _define_directive_re = re.compile(r'\s*#\s*define\s+(\w+)' +
283 r'(?:\s+|\((\w+)\)\s*)' +
284 r'(.+)')
285 _deprecated_definition_re = re.compile(r'\s*MBEDTLS_DEPRECATED')
286
287 def read_line(self, line):
288 """Parse a C header line and record the PSA identifier it defines if any.
289 This function analyzes lines that start with "#define PSA_"
290 (up to non-significant whitespace) and skips all non-matching lines.
291 """
292 # pylint: disable=too-many-branches
293 m = re.match(self._define_directive_re, line)
294 if not m:
295 return
296 name, parameter, expansion = m.groups()
297 expansion = re.sub(r'/\*.*?\*/|//.*', r' ', expansion)
Gilles Peskine33c601c2021-03-10 01:25:50 +0100298 if parameter:
299 self.argspecs[name] = [parameter]
Gilles Peskinee7c44552021-01-25 21:40:45 +0100300 if re.match(self._deprecated_definition_re, expansion):
301 # Skip deprecated values, which are assumed to be
302 # backward compatibility aliases that share
303 # numerical values with non-deprecated values.
304 return
Gilles Peskinef8deb752021-01-25 22:41:45 +0100305 if self.is_internal_name(name):
Gilles Peskinee7c44552021-01-25 21:40:45 +0100306 # Macro only to build actual values
307 return
308 elif (name.startswith('PSA_ERROR_') or name == 'PSA_SUCCESS') \
309 and not parameter:
310 self.statuses.add(name)
311 elif name.startswith('PSA_KEY_TYPE_') and not parameter:
312 self.key_types.add(name)
313 elif name.startswith('PSA_KEY_TYPE_') and parameter == 'curve':
314 self.key_types_from_curve[name] = name[:13] + 'IS_' + name[13:]
315 elif name.startswith('PSA_KEY_TYPE_') and parameter == 'group':
316 self.key_types_from_group[name] = name[:13] + 'IS_' + name[13:]
317 elif name.startswith('PSA_ECC_FAMILY_') and not parameter:
318 self.ecc_curves.add(name)
319 elif name.startswith('PSA_DH_FAMILY_') and not parameter:
320 self.dh_groups.add(name)
321 elif name.startswith('PSA_ALG_') and not parameter:
322 if name in ['PSA_ALG_ECDSA_BASE',
323 'PSA_ALG_RSA_PKCS1V15_SIGN_BASE']:
324 # Ad hoc skipping of duplicate names for some numerical values
325 return
326 self.algorithms.add(name)
Gilles Peskine33c601c2021-03-10 01:25:50 +0100327 self.record_algorithm_subtype(name, expansion)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100328 elif name.startswith('PSA_ALG_') and parameter == 'hash_alg':
Gilles Peskine35451032021-10-04 18:10:16 +0200329 self.algorithms_from_hash[name] = self.algorithm_tester(name)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100330 elif name.startswith('PSA_KEY_USAGE_') and not parameter:
Gilles Peskine33c601c2021-03-10 01:25:50 +0100331 self.key_usage_flags.add(name)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100332 else:
333 # Other macro without parameter
334 return
335
336 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
337 _continued_line_re = re.compile(rb'\\\r?\n\Z')
338 def read_file(self, header_file):
339 for line in header_file:
340 m = re.search(self._continued_line_re, line)
341 while m:
342 cont = next(header_file)
343 line = line[:m.start(0)] + cont
344 m = re.search(self._continued_line_re, line)
345 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
346 self.read_line(line)
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200347
348
Gilles Peskineb93f8542021-04-19 13:50:25 +0200349class InputsForTest(PSAMacroEnumerator):
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200350 # pylint: disable=too-many-instance-attributes
351 """Accumulate information about macros to test.
352enumerate
353 This includes macro names as well as information about their arguments
354 when applicable.
355 """
356
357 def __init__(self) -> None:
358 super().__init__()
359 self.all_declared = set() #type: Set[str]
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200360 # Identifier prefixes
361 self.table_by_prefix = {
362 'ERROR': self.statuses,
363 'ALG': self.algorithms,
364 'ECC_CURVE': self.ecc_curves,
365 'DH_GROUP': self.dh_groups,
Gilles Peskine4c7da692021-04-21 21:39:27 +0200366 'KEY_LIFETIME': self.lifetimes,
367 'KEY_LOCATION': self.locations,
368 'KEY_PERSISTENCE': self.persistence_levels,
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200369 'KEY_TYPE': self.key_types,
370 'KEY_USAGE': self.key_usage_flags,
371 } #type: Dict[str, Set[str]]
372 # Test functions
373 self.table_by_test_function = {
374 # Any function ending in _algorithm also gets added to
375 # self.algorithms.
376 'key_type': [self.key_types],
377 'block_cipher_key_type': [self.key_types],
378 'stream_cipher_key_type': [self.key_types],
379 'ecc_key_family': [self.ecc_curves],
380 'ecc_key_types': [self.ecc_curves],
381 'dh_key_family': [self.dh_groups],
382 'dh_key_types': [self.dh_groups],
383 'hash_algorithm': [self.hash_algorithms],
384 'mac_algorithm': [self.mac_algorithms],
385 'cipher_algorithm': [],
gabor-mezei-arm044fefc2021-06-24 10:16:44 +0200386 'hmac_algorithm': [self.mac_algorithms, self.sign_algorithms],
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200387 'aead_algorithm': [self.aead_algorithms],
388 'key_derivation_algorithm': [self.kdf_algorithms],
389 'key_agreement_algorithm': [self.ka_algorithms],
gabor-mezei-arm044fefc2021-06-24 10:16:44 +0200390 'asymmetric_signature_algorithm': [self.sign_algorithms],
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200391 'asymmetric_signature_wildcard': [self.algorithms],
392 'asymmetric_encryption_algorithm': [],
393 'other_algorithm': [],
Gilles Peskine4c7da692021-04-21 21:39:27 +0200394 'lifetime': [self.lifetimes],
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200395 } #type: Dict[str, List[Set[str]]]
396 self.arguments_for['mac_length'] += ['1', '63']
397 self.arguments_for['min_mac_length'] += ['1', '63']
398 self.arguments_for['tag_length'] += ['1', '63']
399 self.arguments_for['min_tag_length'] += ['1', '63']
400
Gilles Peskined6d2d6a2021-03-30 21:46:35 +0200401 def add_numerical_values(self) -> None:
402 """Add numerical values that are not supported to the known identifiers."""
403 # Sets of names per type
404 self.algorithms.add('0xffffffff')
405 self.ecc_curves.add('0xff')
406 self.dh_groups.add('0xff')
407 self.key_types.add('0xffff')
408 self.key_usage_flags.add('0x80000000')
409
410 # Hard-coded values for unknown algorithms
411 #
412 # These have to have values that are correct for their respective
413 # PSA_ALG_IS_xxx macros, but are also not currently assigned and are
414 # not likely to be assigned in the near future.
415 self.hash_algorithms.add('0x020000fe') # 0x020000ff is PSA_ALG_ANY_HASH
416 self.mac_algorithms.add('0x03007fff')
417 self.ka_algorithms.add('0x09fc0000')
418 self.kdf_algorithms.add('0x080000ff')
419 # For AEAD algorithms, the only variability is over the tag length,
420 # and this only applies to known algorithms, so don't test an
421 # unknown algorithm.
422
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200423 def get_names(self, type_word: str) -> Set[str]:
424 """Return the set of known names of values of the given type."""
425 return {
426 'status': self.statuses,
427 'algorithm': self.algorithms,
428 'ecc_curve': self.ecc_curves,
429 'dh_group': self.dh_groups,
430 'key_type': self.key_types,
431 'key_usage': self.key_usage_flags,
432 }[type_word]
433
434 # Regex for interesting header lines.
435 # Groups: 1=macro name, 2=type, 3=argument list (optional).
436 _header_line_re = \
437 re.compile(r'#define +' +
438 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
439 r'(?:\(([^\n()]*)\))?')
440 # Regex of macro names to exclude.
441 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
442 # Additional excluded macros.
443 _excluded_names = set([
444 # Macros that provide an alternative way to build the same
445 # algorithm as another macro.
446 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG',
447 'PSA_ALG_FULL_LENGTH_MAC',
448 # Auxiliary macro whose name doesn't fit the usual patterns for
449 # auxiliary macros.
450 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG_CASE',
451 ])
452 def parse_header_line(self, line: str) -> None:
453 """Parse a C header line, looking for "#define PSA_xxx"."""
454 m = re.match(self._header_line_re, line)
455 if not m:
456 return
457 name = m.group(1)
458 self.all_declared.add(name)
459 if re.search(self._excluded_name_re, name) or \
Gilles Peskineb93f8542021-04-19 13:50:25 +0200460 name in self._excluded_names or \
461 self.is_internal_name(name):
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200462 return
463 dest = self.table_by_prefix.get(m.group(2))
464 if dest is None:
465 return
466 dest.add(name)
467 if m.group(3):
468 self.argspecs[name] = self._argument_split(m.group(3))
469
470 _nonascii_re = re.compile(rb'[^\x00-\x7f]+') #type: Pattern
471 def parse_header(self, filename: str) -> None:
472 """Parse a C header file, looking for "#define PSA_xxx"."""
473 with read_file_lines(filename, binary=True) as lines:
474 for line in lines:
475 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
476 self.parse_header_line(line)
477
478 _macro_identifier_re = re.compile(r'[A-Z]\w+')
479 def generate_undeclared_names(self, expr: str) -> Iterable[str]:
480 for name in re.findall(self._macro_identifier_re, expr):
481 if name not in self.all_declared:
482 yield name
483
484 def accept_test_case_line(self, function: str, argument: str) -> bool:
485 #pylint: disable=unused-argument
486 undeclared = list(self.generate_undeclared_names(argument))
487 if undeclared:
488 raise Exception('Undeclared names in test case', undeclared)
489 return True
490
Gilles Peskine0a93c1b2021-04-21 15:36:58 +0200491 @staticmethod
492 def normalize_argument(argument: str) -> str:
493 """Normalize whitespace in the given C expression.
494
495 The result uses the same whitespace as
496 ` PSAMacroEnumerator.distribute_arguments`.
497 """
498 return re.sub(r',', r', ', re.sub(r' +', r'', argument))
499
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200500 def add_test_case_line(self, function: str, argument: str) -> None:
501 """Parse a test case data line, looking for algorithm metadata tests."""
502 sets = []
503 if function.endswith('_algorithm'):
504 sets.append(self.algorithms)
505 if function == 'key_agreement_algorithm' and \
506 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
507 # We only want *raw* key agreement algorithms as such, so
508 # exclude ones that are already chained with a KDF.
509 # Keep the expression as one to test as an algorithm.
510 function = 'other_algorithm'
511 sets += self.table_by_test_function[function]
512 if self.accept_test_case_line(function, argument):
513 for s in sets:
Gilles Peskine0a93c1b2021-04-21 15:36:58 +0200514 s.add(self.normalize_argument(argument))
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200515
516 # Regex matching a *.data line containing a test function call and
517 # its arguments. The actual definition is partly positional, but this
518 # regex is good enough in practice.
519 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
520 def parse_test_cases(self, filename: str) -> None:
521 """Parse a test case file (*.data), looking for algorithm metadata tests."""
522 with read_file_lines(filename) as lines:
523 for line in lines:
524 m = re.match(self._test_case_line_re, line)
525 if m:
526 self.add_test_case_line(m.group(1), m.group(2))