blob: 6eb0d00d137019e40d211e6548f0c57c32fa0fa2 [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 Peskineb4edff92021-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 Peskine45a43912021-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 Peskine45a43912021-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]
Janos Follath8603fb02021-04-19 15:12:46 +0100102 self.pake_algorithms = set() #type: Set[str]
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100103 self.aead_algorithms = set() #type: Set[str]
104 # 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 Peskine2157e862021-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 Peskine537d5fa2021-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)
138 self.arguments_for['curve'] = sorted(self.ecc_curves)
139 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine45a43912021-04-21 21:39:27 +0200140 self.arguments_for['persistence'] = sorted(self.persistence_levels)
141 self.arguments_for['location'] = sorted(self.locations)
142 self.arguments_for['lifetime'] = sorted(self.lifetimes)
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100143
144 @staticmethod
145 def _format_arguments(name: str, arguments: Iterable[str]) -> str:
Gilles Peskinecccd1ac2021-04-21 15:36:58 +0200146 """Format a macro call with arguments.
147
148 The resulting format is consistent with
149 `InputsForTest.normalize_argument`.
150 """
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100151 return name + '(' + ', '.join(arguments) + ')'
152
153 _argument_split_re = re.compile(r' *, *')
154 @classmethod
155 def _argument_split(cls, arguments: str) -> List[str]:
156 return re.split(cls._argument_split_re, arguments)
157
158 def distribute_arguments(self, name: str) -> Iterator[str]:
159 """Generate macro calls with each tested argument set.
160
161 If name is a macro without arguments, just yield "name".
162 If name is a macro with arguments, yield a series of
163 "name(arg1,...,argN)" where each argument takes each possible
164 value at least once.
165 """
166 try:
167 if name not in self.argspecs:
168 yield name
169 return
170 argspec = self.argspecs[name]
171 if argspec == []:
172 yield name + '()'
173 return
174 argument_lists = [self.arguments_for[arg] for arg in argspec]
175 arguments = [values[0] for values in argument_lists]
176 yield self._format_arguments(name, arguments)
177 # Dear Pylint, enumerate won't work here since we're modifying
178 # the array.
179 # pylint: disable=consider-using-enumerate
180 for i in range(len(arguments)):
181 for value in argument_lists[i][1:]:
182 arguments[i] = value
183 yield self._format_arguments(name, arguments)
184 arguments[i] = argument_lists[0][0]
185 except BaseException as e:
186 raise Exception('distribute_arguments({})'.format(name)) from e
187
Gilles Peskine38ebfec2021-04-21 15:37:34 +0200188 def distribute_arguments_without_duplicates(
189 self, seen: Set[str], name: str
190 ) -> Iterator[str]:
191 """Same as `distribute_arguments`, but don't repeat seen results."""
192 for result in self.distribute_arguments(name):
193 if result not in seen:
194 seen.add(result)
195 yield result
196
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100197 def generate_expressions(self, names: Iterable[str]) -> Iterator[str]:
198 """Generate expressions covering values constructed from the given names.
199
200 `names` can be any iterable collection of macro names.
201
202 For example:
203 * ``generate_expressions(['PSA_ALG_CMAC', 'PSA_ALG_HMAC'])``
204 generates ``'PSA_ALG_CMAC'`` as well as ``'PSA_ALG_HMAC(h)'`` for
205 every known hash algorithm ``h``.
206 * ``macros.generate_expressions(macros.key_types)`` generates all
207 key types.
208 """
Gilles Peskine38ebfec2021-04-21 15:37:34 +0200209 seen = set() #type: Set[str]
210 return itertools.chain(*(
211 self.distribute_arguments_without_duplicates(seen, name)
212 for name in names
213 ))
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100214
Gilles Peskinee7c44552021-01-25 21:40:45 +0100215
Gilles Peskine33c601c2021-03-10 01:25:50 +0100216class PSAMacroCollector(PSAMacroEnumerator):
Gilles Peskinee7c44552021-01-25 21:40:45 +0100217 """Collect PSA crypto macro definitions from C header files.
218 """
219
Gilles Peskine10ab2672021-03-10 00:59:53 +0100220 def __init__(self, include_intermediate: bool = False) -> None:
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100221 """Set up an object to collect PSA macro definitions.
222
223 Call the read_file method of the constructed object on each header file.
224
225 * include_intermediate: if true, include intermediate macros such as
226 PSA_XXX_BASE that do not designate semantic values.
227 """
Gilles Peskine33c601c2021-03-10 01:25:50 +0100228 super().__init__()
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100229 self.include_intermediate = include_intermediate
Gilles Peskine10ab2672021-03-10 00:59:53 +0100230 self.key_types_from_curve = {} #type: Dict[str, str]
231 self.key_types_from_group = {} #type: Dict[str, str]
Gilles Peskine10ab2672021-03-10 00:59:53 +0100232 self.algorithms_from_hash = {} #type: Dict[str, str]
Gilles Peskinee7c44552021-01-25 21:40:45 +0100233
Gilles Peskine33c601c2021-03-10 01:25:50 +0100234 def record_algorithm_subtype(self, name: str, expansion: str) -> None:
235 """Record the subtype of an algorithm constructor.
236
237 Given a ``PSA_ALG_xxx`` macro name and its expansion, if the algorithm
238 is of a subtype that is tracked in its own set, add it to the relevant
239 set.
240 """
241 # This code is very ad hoc and fragile. It should be replaced by
242 # something more robust.
243 if re.match(r'MAC(?:_|\Z)', name):
244 self.mac_algorithms.add(name)
245 elif re.match(r'KDF(?:_|\Z)', name):
246 self.kdf_algorithms.add(name)
247 elif re.search(r'0x020000[0-9A-Fa-f]{2}', expansion):
248 self.hash_algorithms.add(name)
249 elif re.search(r'0x03[0-9A-Fa-f]{6}', expansion):
250 self.mac_algorithms.add(name)
251 elif re.search(r'0x05[0-9A-Fa-f]{6}', expansion):
252 self.aead_algorithms.add(name)
253 elif re.search(r'0x09[0-9A-Fa-f]{2}0000', expansion):
254 self.ka_algorithms.add(name)
255 elif re.search(r'0x08[0-9A-Fa-f]{6}', expansion):
256 self.kdf_algorithms.add(name)
257
Gilles Peskinee7c44552021-01-25 21:40:45 +0100258 # "#define" followed by a macro name with either no parameters
259 # or a single parameter and a non-empty expansion.
260 # Grab the macro name in group 1, the parameter name if any in group 2
261 # and the expansion in group 3.
262 _define_directive_re = re.compile(r'\s*#\s*define\s+(\w+)' +
263 r'(?:\s+|\((\w+)\)\s*)' +
264 r'(.+)')
265 _deprecated_definition_re = re.compile(r'\s*MBEDTLS_DEPRECATED')
266
267 def read_line(self, line):
268 """Parse a C header line and record the PSA identifier it defines if any.
269 This function analyzes lines that start with "#define PSA_"
270 (up to non-significant whitespace) and skips all non-matching lines.
271 """
272 # pylint: disable=too-many-branches
273 m = re.match(self._define_directive_re, line)
274 if not m:
275 return
276 name, parameter, expansion = m.groups()
277 expansion = re.sub(r'/\*.*?\*/|//.*', r' ', expansion)
Gilles Peskine33c601c2021-03-10 01:25:50 +0100278 if parameter:
279 self.argspecs[name] = [parameter]
Gilles Peskinee7c44552021-01-25 21:40:45 +0100280 if re.match(self._deprecated_definition_re, expansion):
281 # Skip deprecated values, which are assumed to be
282 # backward compatibility aliases that share
283 # numerical values with non-deprecated values.
284 return
Gilles Peskinef8deb752021-01-25 22:41:45 +0100285 if self.is_internal_name(name):
Gilles Peskinee7c44552021-01-25 21:40:45 +0100286 # Macro only to build actual values
287 return
288 elif (name.startswith('PSA_ERROR_') or name == 'PSA_SUCCESS') \
289 and not parameter:
290 self.statuses.add(name)
291 elif name.startswith('PSA_KEY_TYPE_') and not parameter:
292 self.key_types.add(name)
293 elif name.startswith('PSA_KEY_TYPE_') and parameter == 'curve':
294 self.key_types_from_curve[name] = name[:13] + 'IS_' + name[13:]
295 elif name.startswith('PSA_KEY_TYPE_') and parameter == 'group':
296 self.key_types_from_group[name] = name[:13] + 'IS_' + name[13:]
297 elif name.startswith('PSA_ECC_FAMILY_') and not parameter:
298 self.ecc_curves.add(name)
299 elif name.startswith('PSA_DH_FAMILY_') and not parameter:
300 self.dh_groups.add(name)
301 elif name.startswith('PSA_ALG_') and not parameter:
302 if name in ['PSA_ALG_ECDSA_BASE',
303 'PSA_ALG_RSA_PKCS1V15_SIGN_BASE']:
304 # Ad hoc skipping of duplicate names for some numerical values
305 return
306 self.algorithms.add(name)
Gilles Peskine33c601c2021-03-10 01:25:50 +0100307 self.record_algorithm_subtype(name, expansion)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100308 elif name.startswith('PSA_ALG_') and parameter == 'hash_alg':
309 if name in ['PSA_ALG_DSA', 'PSA_ALG_ECDSA']:
310 # A naming irregularity
311 tester = name[:8] + 'IS_RANDOMIZED_' + name[8:]
312 else:
313 tester = name[:8] + 'IS_' + name[8:]
314 self.algorithms_from_hash[name] = tester
315 elif name.startswith('PSA_KEY_USAGE_') and not parameter:
Gilles Peskine33c601c2021-03-10 01:25:50 +0100316 self.key_usage_flags.add(name)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100317 else:
318 # Other macro without parameter
319 return
320
321 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
322 _continued_line_re = re.compile(rb'\\\r?\n\Z')
323 def read_file(self, header_file):
324 for line in header_file:
325 m = re.search(self._continued_line_re, line)
326 while m:
327 cont = next(header_file)
328 line = line[:m.start(0)] + cont
329 m = re.search(self._continued_line_re, line)
330 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
331 self.read_line(line)
Gilles Peskineb4edff92021-03-30 19:09:05 +0200332
333
Gilles Peskine537d5fa2021-04-19 13:50:25 +0200334class InputsForTest(PSAMacroEnumerator):
Gilles Peskineb4edff92021-03-30 19:09:05 +0200335 # pylint: disable=too-many-instance-attributes
336 """Accumulate information about macros to test.
337enumerate
338 This includes macro names as well as information about their arguments
339 when applicable.
340 """
341
342 def __init__(self) -> None:
343 super().__init__()
344 self.all_declared = set() #type: Set[str]
Gilles Peskineb4edff92021-03-30 19:09:05 +0200345 # Identifier prefixes
346 self.table_by_prefix = {
347 'ERROR': self.statuses,
348 'ALG': self.algorithms,
349 'ECC_CURVE': self.ecc_curves,
350 'DH_GROUP': self.dh_groups,
Gilles Peskine45a43912021-04-21 21:39:27 +0200351 'KEY_LIFETIME': self.lifetimes,
352 'KEY_LOCATION': self.locations,
353 'KEY_PERSISTENCE': self.persistence_levels,
Gilles Peskineb4edff92021-03-30 19:09:05 +0200354 'KEY_TYPE': self.key_types,
355 'KEY_USAGE': self.key_usage_flags,
356 } #type: Dict[str, Set[str]]
357 # Test functions
358 self.table_by_test_function = {
359 # Any function ending in _algorithm also gets added to
360 # self.algorithms.
361 'key_type': [self.key_types],
362 'block_cipher_key_type': [self.key_types],
363 'stream_cipher_key_type': [self.key_types],
364 'ecc_key_family': [self.ecc_curves],
365 'ecc_key_types': [self.ecc_curves],
366 'dh_key_family': [self.dh_groups],
367 'dh_key_types': [self.dh_groups],
368 'hash_algorithm': [self.hash_algorithms],
369 'mac_algorithm': [self.mac_algorithms],
370 'cipher_algorithm': [],
371 'hmac_algorithm': [self.mac_algorithms],
372 'aead_algorithm': [self.aead_algorithms],
373 'key_derivation_algorithm': [self.kdf_algorithms],
374 'key_agreement_algorithm': [self.ka_algorithms],
375 'asymmetric_signature_algorithm': [],
376 'asymmetric_signature_wildcard': [self.algorithms],
377 'asymmetric_encryption_algorithm': [],
Janos Follath8603fb02021-04-19 15:12:46 +0100378 'pake_algorithm': [self.pake_algorithms],
Gilles Peskineb4edff92021-03-30 19:09:05 +0200379 'other_algorithm': [],
Gilles Peskine45a43912021-04-21 21:39:27 +0200380 'lifetime': [self.lifetimes],
Gilles Peskineb4edff92021-03-30 19:09:05 +0200381 } #type: Dict[str, List[Set[str]]]
382 self.arguments_for['mac_length'] += ['1', '63']
383 self.arguments_for['min_mac_length'] += ['1', '63']
384 self.arguments_for['tag_length'] += ['1', '63']
385 self.arguments_for['min_tag_length'] += ['1', '63']
386
Gilles Peskine3d404b82021-03-30 21:46:35 +0200387 def add_numerical_values(self) -> None:
388 """Add numerical values that are not supported to the known identifiers."""
389 # Sets of names per type
390 self.algorithms.add('0xffffffff')
391 self.ecc_curves.add('0xff')
392 self.dh_groups.add('0xff')
393 self.key_types.add('0xffff')
394 self.key_usage_flags.add('0x80000000')
395
396 # Hard-coded values for unknown algorithms
397 #
398 # These have to have values that are correct for their respective
399 # PSA_ALG_IS_xxx macros, but are also not currently assigned and are
400 # not likely to be assigned in the near future.
401 self.hash_algorithms.add('0x020000fe') # 0x020000ff is PSA_ALG_ANY_HASH
402 self.mac_algorithms.add('0x03007fff')
403 self.ka_algorithms.add('0x09fc0000')
404 self.kdf_algorithms.add('0x080000ff')
Janos Follath8603fb02021-04-19 15:12:46 +0100405 self.pake_algorithms.add('0x0a0000ff')
Gilles Peskine3d404b82021-03-30 21:46:35 +0200406 # For AEAD algorithms, the only variability is over the tag length,
407 # and this only applies to known algorithms, so don't test an
408 # unknown algorithm.
409
Gilles Peskineb4edff92021-03-30 19:09:05 +0200410 def get_names(self, type_word: str) -> Set[str]:
411 """Return the set of known names of values of the given type."""
412 return {
413 'status': self.statuses,
414 'algorithm': self.algorithms,
415 'ecc_curve': self.ecc_curves,
416 'dh_group': self.dh_groups,
417 'key_type': self.key_types,
418 'key_usage': self.key_usage_flags,
419 }[type_word]
420
421 # Regex for interesting header lines.
422 # Groups: 1=macro name, 2=type, 3=argument list (optional).
423 _header_line_re = \
424 re.compile(r'#define +' +
425 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
426 r'(?:\(([^\n()]*)\))?')
427 # Regex of macro names to exclude.
428 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
429 # Additional excluded macros.
430 _excluded_names = set([
431 # Macros that provide an alternative way to build the same
432 # algorithm as another macro.
433 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG',
434 'PSA_ALG_FULL_LENGTH_MAC',
435 # Auxiliary macro whose name doesn't fit the usual patterns for
436 # auxiliary macros.
437 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG_CASE',
438 ])
439 def parse_header_line(self, line: str) -> None:
440 """Parse a C header line, looking for "#define PSA_xxx"."""
441 m = re.match(self._header_line_re, line)
442 if not m:
443 return
444 name = m.group(1)
445 self.all_declared.add(name)
446 if re.search(self._excluded_name_re, name) or \
Gilles Peskine537d5fa2021-04-19 13:50:25 +0200447 name in self._excluded_names or \
448 self.is_internal_name(name):
Gilles Peskineb4edff92021-03-30 19:09:05 +0200449 return
450 dest = self.table_by_prefix.get(m.group(2))
451 if dest is None:
452 return
453 dest.add(name)
454 if m.group(3):
455 self.argspecs[name] = self._argument_split(m.group(3))
456
457 _nonascii_re = re.compile(rb'[^\x00-\x7f]+') #type: Pattern
458 def parse_header(self, filename: str) -> None:
459 """Parse a C header file, looking for "#define PSA_xxx"."""
460 with read_file_lines(filename, binary=True) as lines:
461 for line in lines:
462 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
463 self.parse_header_line(line)
464
465 _macro_identifier_re = re.compile(r'[A-Z]\w+')
466 def generate_undeclared_names(self, expr: str) -> Iterable[str]:
467 for name in re.findall(self._macro_identifier_re, expr):
468 if name not in self.all_declared:
469 yield name
470
471 def accept_test_case_line(self, function: str, argument: str) -> bool:
472 #pylint: disable=unused-argument
473 undeclared = list(self.generate_undeclared_names(argument))
474 if undeclared:
475 raise Exception('Undeclared names in test case', undeclared)
476 return True
477
Gilles Peskinecccd1ac2021-04-21 15:36:58 +0200478 @staticmethod
479 def normalize_argument(argument: str) -> str:
480 """Normalize whitespace in the given C expression.
481
482 The result uses the same whitespace as
483 ` PSAMacroEnumerator.distribute_arguments`.
484 """
485 return re.sub(r',', r', ', re.sub(r' +', r'', argument))
486
Gilles Peskineb4edff92021-03-30 19:09:05 +0200487 def add_test_case_line(self, function: str, argument: str) -> None:
488 """Parse a test case data line, looking for algorithm metadata tests."""
489 sets = []
490 if function.endswith('_algorithm'):
491 sets.append(self.algorithms)
492 if function == 'key_agreement_algorithm' and \
493 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
494 # We only want *raw* key agreement algorithms as such, so
495 # exclude ones that are already chained with a KDF.
496 # Keep the expression as one to test as an algorithm.
497 function = 'other_algorithm'
498 sets += self.table_by_test_function[function]
499 if self.accept_test_case_line(function, argument):
500 for s in sets:
Gilles Peskinecccd1ac2021-04-21 15:36:58 +0200501 s.add(self.normalize_argument(argument))
Gilles Peskineb4edff92021-03-30 19:09:05 +0200502
503 # Regex matching a *.data line containing a test function call and
504 # its arguments. The actual definition is partly positional, but this
505 # regex is good enough in practice.
506 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
507 def parse_test_cases(self, filename: str) -> None:
508 """Parse a test case file (*.data), looking for algorithm metadata tests."""
509 with read_file_lines(filename) as lines:
510 for line in lines:
511 m = re.match(self._test_case_line_re, line)
512 if m:
513 self.add_test_case_line(m.group(1), m.group(2))