blob: bead60cd8f1f1825e09cadaf2b473804d9fba180 [file] [log] [blame]
Gilles Peskine24827022018-09-25 18:49:23 +02001#!/usr/bin/env python3
Gilles Peskinea3b93ff2019-06-03 11:23:56 +02002"""Test the program psa_constant_names.
Gilles Peskine24827022018-09-25 18:49:23 +02003Gather constant names from header files and test cases. Compile a C program
4to print out their numerical values, feed these numerical values to
5psa_constant_names, and check that the output is the original name.
6Return 0 if all test cases pass, 1 if the output was not always as expected,
Gilles Peskinea3b93ff2019-06-03 11:23:56 +02007or 1 (with a Python backtrace) if there was an operational error.
8"""
Gilles Peskine24827022018-09-25 18:49:23 +02009
Bence Szépkúti1e148272020-08-07 13:07:28 +020010# Copyright The Mbed TLS Contributors
Bence Szépkútic7da1fe2020-05-26 01:54:15 +020011# SPDX-License-Identifier: Apache-2.0
12#
13# Licensed under the Apache License, Version 2.0 (the "License"); you may
14# not use this file except in compliance with the License.
15# You may obtain a copy of the License at
16#
17# http://www.apache.org/licenses/LICENSE-2.0
18#
19# Unless required by applicable law or agreed to in writing, software
20# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
21# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22# See the License for the specific language governing permissions and
23# limitations under the License.
Bence Szépkúti700ee442020-05-26 00:33:31 +020024
Gilles Peskine24827022018-09-25 18:49:23 +020025import argparse
Gilles Peskinea5000f12019-11-21 17:51:11 +010026from collections import namedtuple
Gilles Peskine24827022018-09-25 18:49:23 +020027import itertools
28import os
29import platform
30import re
31import subprocess
32import sys
33import tempfile
34
Gilles Peskinea0a315c2018-10-19 11:27:10 +020035class ReadFileLineException(Exception):
36 def __init__(self, filename, line_number):
37 message = 'in {} at {}'.format(filename, line_number)
38 super(ReadFileLineException, self).__init__(message)
39 self.filename = filename
40 self.line_number = line_number
41
42class read_file_lines:
Gilles Peskine54f54452019-05-27 18:31:59 +020043 # Dear Pylint, conventionally, a context manager class name is lowercase.
44 # pylint: disable=invalid-name,too-few-public-methods
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020045 """Context manager to read a text file line by line.
46
47 ```
48 with read_file_lines(filename) as lines:
49 for line in lines:
50 process(line)
51 ```
52 is equivalent to
53 ```
54 with open(filename, 'r') as input_file:
55 for line in input_file:
56 process(line)
57 ```
58 except that if process(line) raises an exception, then the read_file_lines
59 snippet annotates the exception with the file name and line number.
60 """
Gilles Peskine49af2d32019-12-06 19:20:13 +010061 def __init__(self, filename, binary=False):
Gilles Peskinea0a315c2018-10-19 11:27:10 +020062 self.filename = filename
63 self.line_number = 'entry'
Gilles Peskine54f54452019-05-27 18:31:59 +020064 self.generator = None
Gilles Peskine49af2d32019-12-06 19:20:13 +010065 self.binary = binary
Gilles Peskinea0a315c2018-10-19 11:27:10 +020066 def __enter__(self):
Gilles Peskine49af2d32019-12-06 19:20:13 +010067 self.generator = enumerate(open(self.filename,
68 'rb' if self.binary else 'r'))
Gilles Peskinea0a315c2018-10-19 11:27:10 +020069 return self
70 def __iter__(self):
71 for line_number, content in self.generator:
72 self.line_number = line_number
73 yield content
74 self.line_number = 'exit'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020075 def __exit__(self, exc_type, exc_value, exc_traceback):
76 if exc_type is not None:
Gilles Peskinea0a315c2018-10-19 11:27:10 +020077 raise ReadFileLineException(self.filename, self.line_number) \
Gilles Peskine42a0a0a2019-05-27 18:29:47 +020078 from exc_value
Gilles Peskinea0a315c2018-10-19 11:27:10 +020079
Gilles Peskine24827022018-09-25 18:49:23 +020080class Inputs:
Gilles Peskine8c8694c2019-11-21 19:22:45 +010081 # pylint: disable=too-many-instance-attributes
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020082 """Accumulate information about macros to test.
Gilles Peskine4408dfd2019-11-21 17:16:21 +010083
Gilles Peskinea3b93ff2019-06-03 11:23:56 +020084 This includes macro names as well as information about their arguments
85 when applicable.
86 """
87
Gilles Peskine24827022018-09-25 18:49:23 +020088 def __init__(self):
Gilles Peskine2bcfc712019-11-21 19:49:26 +010089 self.all_declared = set()
Gilles Peskine24827022018-09-25 18:49:23 +020090 # Sets of names per type
91 self.statuses = set(['PSA_SUCCESS'])
92 self.algorithms = set(['0xffffffff'])
Gilles Peskinef65ed6f2019-12-04 17:18:41 +010093 self.ecc_curves = set(['0xff'])
94 self.dh_groups = set(['0xff'])
95 self.key_types = set(['0xffff'])
Gilles Peskine24827022018-09-25 18:49:23 +020096 self.key_usage_flags = set(['0x80000000'])
Bence Szépkúti4af65602020-12-08 11:10:21 +010097 # Hard-coded values for unknown algorithms
98 #
99 # These have to have values that are correct for their respective
100 # PSA_ALG_IS_xxx macros, but are also not currently assigned and are
101 # not likely to be assigned in the near future.
102 self.hash_algorithms = set(['0x020000fe']) # 0x020000ff is PSA_ALG_ANY_HASH
Bence Szépkúti7e37bf92020-12-08 07:33:08 +0100103 self.mac_algorithms = set(['0x0300ffff'])
104 self.ka_algorithms = set(['0x09fc0000'])
105 self.kdf_algorithms = set(['0x080000ff'])
Gilles Peskine434899f2018-10-19 11:30:26 +0200106 # For AEAD algorithms, the only variability is over the tag length,
107 # and this only applies to known algorithms, so don't test an
108 # unknown algorithm.
109 self.aead_algorithms = set()
Gilles Peskine24827022018-09-25 18:49:23 +0200110 # Identifier prefixes
111 self.table_by_prefix = {
112 'ERROR': self.statuses,
113 'ALG': self.algorithms,
Gilles Peskine98a710c2019-11-21 18:58:36 +0100114 'ECC_CURVE': self.ecc_curves,
115 'DH_GROUP': self.dh_groups,
Gilles Peskine24827022018-09-25 18:49:23 +0200116 'KEY_TYPE': self.key_types,
117 'KEY_USAGE': self.key_usage_flags,
118 }
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100119 # Test functions
120 self.table_by_test_function = {
Gilles Peskine8fa13482019-11-25 17:10:12 +0100121 # Any function ending in _algorithm also gets added to
122 # self.algorithms.
123 'key_type': [self.key_types],
Gilles Peskinef8210f22019-12-02 17:26:44 +0100124 'block_cipher_key_type': [self.key_types],
125 'stream_cipher_key_type': [self.key_types],
Gilles Peskine228abc52019-12-03 17:24:19 +0100126 'ecc_key_family': [self.ecc_curves],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100127 'ecc_key_types': [self.ecc_curves],
Gilles Peskine228abc52019-12-03 17:24:19 +0100128 'dh_key_family': [self.dh_groups],
Gilles Peskine8fa13482019-11-25 17:10:12 +0100129 'dh_key_types': [self.dh_groups],
130 'hash_algorithm': [self.hash_algorithms],
131 'mac_algorithm': [self.mac_algorithms],
132 'cipher_algorithm': [],
133 'hmac_algorithm': [self.mac_algorithms],
134 'aead_algorithm': [self.aead_algorithms],
135 'key_derivation_algorithm': [self.kdf_algorithms],
136 'key_agreement_algorithm': [self.ka_algorithms],
137 'asymmetric_signature_algorithm': [],
138 'asymmetric_signature_wildcard': [self.algorithms],
139 'asymmetric_encryption_algorithm': [],
140 'other_algorithm': [],
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100141 }
Gilles Peskine24827022018-09-25 18:49:23 +0200142 # macro name -> list of argument names
143 self.argspecs = {}
144 # argument name -> list of values
Gilles Peskine434899f2018-10-19 11:30:26 +0200145 self.arguments_for = {
146 'mac_length': ['1', '63'],
147 'tag_length': ['1', '63'],
148 }
Gilles Peskine24827022018-09-25 18:49:23 +0200149
Gilles Peskineffe2d6e2019-11-21 17:17:01 +0100150 def get_names(self, type_word):
151 """Return the set of known names of values of the given type."""
152 return {
153 'status': self.statuses,
154 'algorithm': self.algorithms,
155 'ecc_curve': self.ecc_curves,
156 'dh_group': self.dh_groups,
157 'key_type': self.key_types,
158 'key_usage': self.key_usage_flags,
159 }[type_word]
160
Gilles Peskine24827022018-09-25 18:49:23 +0200161 def gather_arguments(self):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200162 """Populate the list of values for macro arguments.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100163
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200164 Call this after parsing all the inputs.
165 """
Gilles Peskine24827022018-09-25 18:49:23 +0200166 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200167 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
Gilles Peskine882e57e2019-04-12 00:12:07 +0200168 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
Gilles Peskine17542082019-01-04 19:46:31 +0100169 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
Gilles Peskine434899f2018-10-19 11:30:26 +0200170 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
Gilles Peskine24827022018-09-25 18:49:23 +0200171 self.arguments_for['curve'] = sorted(self.ecc_curves)
Gilles Peskinedcaefae2019-05-16 12:55:35 +0200172 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine24827022018-09-25 18:49:23 +0200173
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200174 @staticmethod
175 def _format_arguments(name, arguments):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200176 """Format a macro call with arguments.."""
Gilles Peskine24827022018-09-25 18:49:23 +0200177 return name + '(' + ', '.join(arguments) + ')'
178
179 def distribute_arguments(self, name):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200180 """Generate macro calls with each tested argument set.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100181
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200182 If name is a macro without arguments, just yield "name".
183 If name is a macro with arguments, yield a series of
184 "name(arg1,...,argN)" where each argument takes each possible
185 value at least once.
186 """
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200187 try:
188 if name not in self.argspecs:
189 yield name
190 return
191 argspec = self.argspecs[name]
192 if argspec == []:
193 yield name + '()'
194 return
195 argument_lists = [self.arguments_for[arg] for arg in argspec]
196 arguments = [values[0] for values in argument_lists]
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200197 yield self._format_arguments(name, arguments)
Gilles Peskine54f54452019-05-27 18:31:59 +0200198 # Dear Pylint, enumerate won't work here since we're modifying
199 # the array.
200 # pylint: disable=consider-using-enumerate
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200201 for i in range(len(arguments)):
202 for value in argument_lists[i][1:]:
203 arguments[i] = value
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200204 yield self._format_arguments(name, arguments)
Gilles Peskinef96ed662018-10-19 11:29:56 +0200205 arguments[i] = argument_lists[0][0]
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200206 except BaseException as e:
207 raise Exception('distribute_arguments({})'.format(name)) from e
Gilles Peskine24827022018-09-25 18:49:23 +0200208
Gilles Peskine5a994c12019-11-21 16:46:51 +0100209 def generate_expressions(self, names):
210 return itertools.chain(*map(self.distribute_arguments, names))
211
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200212 _argument_split_re = re.compile(r' *, *')
213 @classmethod
214 def _argument_split(cls, arguments):
215 return re.split(cls._argument_split_re, arguments)
216
Gilles Peskine24827022018-09-25 18:49:23 +0200217 # Regex for interesting header lines.
218 # Groups: 1=macro name, 2=type, 3=argument list (optional).
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200219 _header_line_re = \
Gilles Peskine24827022018-09-25 18:49:23 +0200220 re.compile(r'#define +' +
Gilles Peskine98a710c2019-11-21 18:58:36 +0100221 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
Gilles Peskine24827022018-09-25 18:49:23 +0200222 r'(?:\(([^\n()]*)\))?')
223 # Regex of macro names to exclude.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200224 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
Gilles Peskinec68ce962018-10-19 11:31:52 +0200225 # Additional excluded macros.
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200226 _excluded_names = set([
227 # Macros that provide an alternative way to build the same
228 # algorithm as another macro.
229 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH',
230 'PSA_ALG_FULL_LENGTH_MAC',
231 # Auxiliary macro whose name doesn't fit the usual patterns for
232 # auxiliary macros.
233 'PSA_ALG_AEAD_WITH_DEFAULT_TAG_LENGTH_CASE',
Gilles Peskine5c196fb2019-05-17 12:04:41 +0200234 ])
Gilles Peskine24827022018-09-25 18:49:23 +0200235 def parse_header_line(self, line):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200236 """Parse a C header line, looking for "#define PSA_xxx"."""
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200237 m = re.match(self._header_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200238 if not m:
239 return
240 name = m.group(1)
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100241 self.all_declared.add(name)
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200242 if re.search(self._excluded_name_re, name) or \
243 name in self._excluded_names:
Gilles Peskine24827022018-09-25 18:49:23 +0200244 return
245 dest = self.table_by_prefix.get(m.group(2))
246 if dest is None:
247 return
248 dest.add(name)
249 if m.group(3):
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200250 self.argspecs[name] = self._argument_split(m.group(3))
Gilles Peskine24827022018-09-25 18:49:23 +0200251
Gilles Peskine49af2d32019-12-06 19:20:13 +0100252 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
Gilles Peskine24827022018-09-25 18:49:23 +0200253 def parse_header(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200254 """Parse a C header file, looking for "#define PSA_xxx"."""
Gilles Peskine49af2d32019-12-06 19:20:13 +0100255 with read_file_lines(filename, binary=True) as lines:
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200256 for line in lines:
Gilles Peskine49af2d32019-12-06 19:20:13 +0100257 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
Gilles Peskine24827022018-09-25 18:49:23 +0200258 self.parse_header_line(line)
259
Gilles Peskine49af2d32019-12-06 19:20:13 +0100260 _macro_identifier_re = re.compile(r'[A-Z]\w+')
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100261 def generate_undeclared_names(self, expr):
262 for name in re.findall(self._macro_identifier_re, expr):
263 if name not in self.all_declared:
264 yield name
265
266 def accept_test_case_line(self, function, argument):
267 #pylint: disable=unused-argument
268 undeclared = list(self.generate_undeclared_names(argument))
269 if undeclared:
270 raise Exception('Undeclared names in test case', undeclared)
271 return True
272
Gilles Peskine24827022018-09-25 18:49:23 +0200273 def add_test_case_line(self, function, argument):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200274 """Parse a test case data line, looking for algorithm metadata tests."""
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100275 sets = []
Gilles Peskine24827022018-09-25 18:49:23 +0200276 if function.endswith('_algorithm'):
Gilles Peskine8c8694c2019-11-21 19:22:45 +0100277 sets.append(self.algorithms)
Gilles Peskine79616682019-11-21 20:08:10 +0100278 if function == 'key_agreement_algorithm' and \
279 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
280 # We only want *raw* key agreement algorithms as such, so
281 # exclude ones that are already chained with a KDF.
282 # Keep the expression as one to test as an algorithm.
283 function = 'other_algorithm'
Gilles Peskine8fa13482019-11-25 17:10:12 +0100284 sets += self.table_by_test_function[function]
Gilles Peskine2bcfc712019-11-21 19:49:26 +0100285 if self.accept_test_case_line(function, argument):
286 for s in sets:
287 s.add(argument)
Gilles Peskine24827022018-09-25 18:49:23 +0200288
289 # Regex matching a *.data line containing a test function call and
290 # its arguments. The actual definition is partly positional, but this
291 # regex is good enough in practice.
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200292 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
Gilles Peskine24827022018-09-25 18:49:23 +0200293 def parse_test_cases(self, filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200294 """Parse a test case file (*.data), looking for algorithm metadata tests."""
Gilles Peskinea0a315c2018-10-19 11:27:10 +0200295 with read_file_lines(filename) as lines:
296 for line in lines:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200297 m = re.match(self._test_case_line_re, line)
Gilles Peskine24827022018-09-25 18:49:23 +0200298 if m:
299 self.add_test_case_line(m.group(1), m.group(2))
300
Gilles Peskine84a45812019-11-21 19:50:33 +0100301def gather_inputs(headers, test_suites, inputs_class=Inputs):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200302 """Read the list of inputs to test psa_constant_names with."""
Gilles Peskine84a45812019-11-21 19:50:33 +0100303 inputs = inputs_class()
Gilles Peskine24827022018-09-25 18:49:23 +0200304 for header in headers:
305 inputs.parse_header(header)
306 for test_cases in test_suites:
307 inputs.parse_test_cases(test_cases)
308 inputs.gather_arguments()
309 return inputs
310
311def remove_file_if_exists(filename):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200312 """Remove the specified file, ignoring errors."""
Gilles Peskine24827022018-09-25 18:49:23 +0200313 if not filename:
314 return
315 try:
316 os.remove(filename)
Gilles Peskine54f54452019-05-27 18:31:59 +0200317 except OSError:
Gilles Peskine24827022018-09-25 18:49:23 +0200318 pass
319
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100320def run_c(type_word, expressions, include_path=None, keep_c=False):
Gilles Peskine5a994c12019-11-21 16:46:51 +0100321 """Generate and run a program to print out numerical values for expressions."""
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100322 if include_path is None:
323 include_path = []
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200324 if type_word == 'status':
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100325 cast_to = 'long'
326 printf_format = '%ld'
327 else:
328 cast_to = 'unsigned long'
329 printf_format = '0x%08lx'
Gilles Peskine24827022018-09-25 18:49:23 +0200330 c_name = None
331 exe_name = None
332 try:
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200333 c_fd, c_name = tempfile.mkstemp(prefix='tmp-{}-'.format(type_word),
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100334 suffix='.c',
Gilles Peskine24827022018-09-25 18:49:23 +0200335 dir='programs/psa')
336 exe_suffix = '.exe' if platform.system() == 'Windows' else ''
337 exe_name = c_name[:-2] + exe_suffix
338 remove_file_if_exists(exe_name)
339 c_file = os.fdopen(c_fd, 'w', encoding='ascii')
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100340 c_file.write('/* Generated by test_psa_constant_names.py for {} values */'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200341 .format(type_word))
Gilles Peskine95ab71a2019-01-04 19:46:59 +0100342 c_file.write('''
Gilles Peskine24827022018-09-25 18:49:23 +0200343#include <stdio.h>
344#include <psa/crypto.h>
345int main(void)
346{
347''')
Gilles Peskine5a994c12019-11-21 16:46:51 +0100348 for expr in expressions:
Gilles Peskinec4cd2ad2019-02-13 18:42:53 +0100349 c_file.write(' printf("{}\\n", ({}) {});\n'
Gilles Peskine5a994c12019-11-21 16:46:51 +0100350 .format(printf_format, cast_to, expr))
Gilles Peskine24827022018-09-25 18:49:23 +0200351 c_file.write(''' return 0;
352}
353''')
354 c_file.close()
355 cc = os.getenv('CC', 'cc')
356 subprocess.check_call([cc] +
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100357 ['-I' + dir for dir in include_path] +
Gilles Peskine24827022018-09-25 18:49:23 +0200358 ['-o', exe_name, c_name])
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100359 if keep_c:
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200360 sys.stderr.write('List of {} tests kept at {}\n'
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200361 .format(type_word, c_name))
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200362 else:
363 os.remove(c_name)
Gilles Peskine24827022018-09-25 18:49:23 +0200364 output = subprocess.check_output([exe_name])
365 return output.decode('ascii').strip().split('\n')
366 finally:
367 remove_file_if_exists(exe_name)
368
Gilles Peskine42a0a0a2019-05-27 18:29:47 +0200369NORMALIZE_STRIP_RE = re.compile(r'\s+')
Gilles Peskine24827022018-09-25 18:49:23 +0200370def normalize(expr):
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200371 """Normalize the C expression so as not to care about trivial differences.
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100372
Gilles Peskinea3b93ff2019-06-03 11:23:56 +0200373 Currently "trivial differences" means whitespace.
374 """
Gilles Peskine5a6dc892019-11-21 16:48:07 +0100375 return re.sub(NORMALIZE_STRIP_RE, '', expr)
Gilles Peskine24827022018-09-25 18:49:23 +0200376
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100377def collect_values(inputs, type_word, include_path=None, keep_c=False):
Gilles Peskinec2317112019-11-21 17:17:39 +0100378 """Generate expressions using known macro names and calculate their values.
379
380 Return a list of pairs of (expr, value) where expr is an expression and
381 value is a string representation of its integer value.
382 """
383 names = inputs.get_names(type_word)
384 expressions = sorted(inputs.generate_expressions(names))
Gilles Peskineb86b6d32019-11-21 17:26:10 +0100385 values = run_c(type_word, expressions,
386 include_path=include_path, keep_c=keep_c)
Gilles Peskinec2317112019-11-21 17:17:39 +0100387 return expressions, values
388
Gilles Peskine24609332019-11-21 17:44:21 +0100389class Tests:
390 """An object representing tests and their results."""
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100391
Gilles Peskinea5000f12019-11-21 17:51:11 +0100392 Error = namedtuple('Error',
393 ['type', 'expression', 'value', 'output'])
394
Gilles Peskine24609332019-11-21 17:44:21 +0100395 def __init__(self, options):
396 self.options = options
397 self.count = 0
398 self.errors = []
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100399
Gilles Peskine24609332019-11-21 17:44:21 +0100400 def run_one(self, inputs, type_word):
401 """Test psa_constant_names for the specified type.
Gilles Peskine24827022018-09-25 18:49:23 +0200402
Gilles Peskine24609332019-11-21 17:44:21 +0100403 Run the program on the names for this type.
404 Use the inputs to figure out what arguments to pass to macros that
405 take arguments.
406 """
407 expressions, values = collect_values(inputs, type_word,
408 include_path=self.options.include,
409 keep_c=self.options.keep_c)
410 output = subprocess.check_output([self.options.program, type_word] +
411 values)
412 outputs = output.decode('ascii').strip().split('\n')
413 self.count += len(expressions)
414 for expr, value, output in zip(expressions, values, outputs):
Gilles Peskine32558482019-12-03 19:03:35 +0100415 if self.options.show:
416 sys.stdout.write('{} {}\t{}\n'.format(type_word, value, output))
Gilles Peskine24609332019-11-21 17:44:21 +0100417 if normalize(expr) != normalize(output):
Gilles Peskinea5000f12019-11-21 17:51:11 +0100418 self.errors.append(self.Error(type=type_word,
419 expression=expr,
420 value=value,
421 output=output))
Gilles Peskine24827022018-09-25 18:49:23 +0200422
Gilles Peskine24609332019-11-21 17:44:21 +0100423 def run_all(self, inputs):
424 """Run psa_constant_names on all the gathered inputs."""
425 for type_word in ['status', 'algorithm', 'ecc_curve', 'dh_group',
426 'key_type', 'key_usage']:
427 self.run_one(inputs, type_word)
Gilles Peskine4408dfd2019-11-21 17:16:21 +0100428
Gilles Peskine24609332019-11-21 17:44:21 +0100429 def report(self, out):
430 """Describe each case where the output is not as expected.
431
432 Write the errors to ``out``.
433 Also write a total.
434 """
Gilles Peskinea5000f12019-11-21 17:51:11 +0100435 for error in self.errors:
Gilles Peskine24609332019-11-21 17:44:21 +0100436 out.write('For {} "{}", got "{}" (value: {})\n'
Gilles Peskinea5000f12019-11-21 17:51:11 +0100437 .format(error.type, error.expression,
438 error.output, error.value))
Gilles Peskine24609332019-11-21 17:44:21 +0100439 out.write('{} test cases'.format(self.count))
440 if self.errors:
441 out.write(', {} FAIL\n'.format(len(self.errors)))
442 else:
443 out.write(' PASS\n')
Gilles Peskine24827022018-09-25 18:49:23 +0200444
Gilles Peskine69f93b52019-11-21 16:49:50 +0100445HEADERS = ['psa/crypto.h', 'psa/crypto_extra.h', 'psa/crypto_values.h']
446TEST_SUITES = ['tests/suites/test_suite_psa_crypto_metadata.data']
447
Gilles Peskine54f54452019-05-27 18:31:59 +0200448def main():
Gilles Peskine24827022018-09-25 18:49:23 +0200449 parser = argparse.ArgumentParser(description=globals()['__doc__'])
450 parser.add_argument('--include', '-I',
451 action='append', default=['include'],
452 help='Directory for header files')
Gilles Peskinecf9c18e2018-10-19 11:28:42 +0200453 parser.add_argument('--keep-c',
454 action='store_true', dest='keep_c', default=False,
455 help='Keep the intermediate C file')
456 parser.add_argument('--no-keep-c',
457 action='store_false', dest='keep_c',
458 help='Don\'t keep the intermediate C file (default)')
Gilles Peskine8f5a5012019-11-21 16:49:10 +0100459 parser.add_argument('--program',
460 default='programs/psa/psa_constant_names',
461 help='Program to test')
Gilles Peskine32558482019-12-03 19:03:35 +0100462 parser.add_argument('--show',
463 action='store_true',
464 help='Keep the intermediate C file')
465 parser.add_argument('--no-show',
466 action='store_false', dest='show',
467 help='Don\'t show tested values (default)')
Gilles Peskine24827022018-09-25 18:49:23 +0200468 options = parser.parse_args()
Gilles Peskine69f93b52019-11-21 16:49:50 +0100469 headers = [os.path.join(options.include[0], h) for h in HEADERS]
470 inputs = gather_inputs(headers, TEST_SUITES)
Gilles Peskine24609332019-11-21 17:44:21 +0100471 tests = Tests(options)
472 tests.run_all(inputs)
473 tests.report(sys.stdout)
474 if tests.errors:
Gilles Peskine8b022352020-03-24 18:36:56 +0100475 sys.exit(1)
Gilles Peskine54f54452019-05-27 18:31:59 +0200476
477if __name__ == '__main__':
478 main()