blob: e423ec5950278bdd6948222b3d423e6f24584766 [file] [log] [blame]
Gilles Peskinebdffaea2021-01-12 00:37:38 +01001#!/usr/bin/env python3
2
3"""Edit test cases to use PSA dependencies instead of classic dependencies.
4"""
5
6# Copyright The Mbed TLS Contributors
7# SPDX-License-Identifier: Apache-2.0
8#
9# Licensed under the Apache License, Version 2.0 (the "License"); you may
10# not use this file except in compliance with the License.
11# You may obtain a copy of the License at
12#
13# http://www.apache.org/licenses/LICENSE-2.0
14#
15# Unless required by applicable law or agreed to in writing, software
16# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
17# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18# See the License for the specific language governing permissions and
19# limitations under the License.
20
21import os
Gilles Peskine82ebaa42021-01-12 00:45:14 +010022import re
Gilles Peskinebdffaea2021-01-12 00:37:38 +010023import sys
24
Gilles Peskine9bbba5e2021-01-12 00:55:55 +010025CLASSIC_DEPENDENCIES = frozenset([
26])
27
28def is_classic_dependency(dep):
29 """Whether dep is a classic dependency that PSA test cases should not use."""
30 if dep.startswith('!'):
31 dep = dep[1:]
32 return dep in CLASSIC_DEPENDENCIES
33
Gilles Peskine2d2e9242021-01-12 00:52:31 +010034def is_systematic_dependency(dep):
35 """Whether dep is a PSA dependency which is determined systematically."""
36 return dep.startswith('PSA_WANT_')
37
Gilles Peskinee4f539c2021-01-12 00:58:36 +010038OMITTED_SYSTEMATIC_DEPENDENCIES = frozenset([
39 # Not implemented yet: cipher-related key types and algorithms.
40 # Manually extracted from crypto_values.h.
41 'PSA_KEY_TYPE_AES',
42 'PSA_KEY_TYPE_DES',
43 'PSA_KEY_TYPE_CAMELLIA',
44 'PSA_KEY_TYPE_ARC4',
45 'PSA_KEY_TYPE_CHACHA20',
46 'PSA_ALG_CBC_MAC',
47 'PSA_ALG_CMAC',
48 'PSA_ALG_STREAM_CIPHER',
49 'PSA_ALG_CTR',
50 'PSA_ALG_CFB',
51 'PSA_ALG_OFB',
52 'PSA_ALG_XTS',
53 'PSA_ALG_ECB_NO_PADDING',
54 'PSA_ALG_CBC_NO_PADDING',
55 'PSA_ALG_CBC_PKCS7',
56 'PSA_ALG_CCM',
57 'PSA_ALG_GCM',
58 'PSA_ALG_CHACHA20_POLY1305',
59])
60
Gilles Peskine2d2e9242021-01-12 00:52:31 +010061def dependencies_of_symbol(symbol):
62 """Return the dependencies for a symbol that designates a cryptographic mechanism."""
Gilles Peskinee4f539c2021-01-12 00:58:36 +010063 if symbol in OMITTED_SYSTEMATIC_DEPENDENCIES:
64 return frozenset()
Gilles Peskine2d2e9242021-01-12 00:52:31 +010065 return {symbol.replace('_', '_WANT_', 1)}
66
67def systematic_dependencies(file_name, function_name, arguments):
68 #pylint: disable=unused-argument
69 """List the systematically determined dependency for a test case."""
70 deps = set()
71 for arg in arguments:
72 for symbol in re.findall(r'PSA_(?:ALG|KEY_TYPE)_\w+', arg):
73 deps.update(dependencies_of_symbol(symbol))
74 return sorted(deps)
75
Gilles Peskine82ebaa42021-01-12 00:45:14 +010076def updated_dependencies(file_name, function_name, arguments, dependencies):
77 """Rework the list of dependencies into PSA_WANT_xxx.
78
79 Remove classic crypto dependencies such as MBEDTLS_RSA_C,
80 MBEDTLS_PKCS1_V15, etc.
81
82 Add systematic PSA_WANT_xxx dependencies based on the called function and
83 its arguments, replacing existing PSA_WANT_xxx dependencies.
84 """
Gilles Peskine2d2e9242021-01-12 00:52:31 +010085 automatic = systematic_dependencies(file_name, function_name, arguments)
86 manual = [dep for dep in dependencies
Gilles Peskine9bbba5e2021-01-12 00:55:55 +010087 if not (is_systematic_dependency(dep) or
88 is_classic_dependency(dep))]
Gilles Peskine2d2e9242021-01-12 00:52:31 +010089 return automatic + manual
Gilles Peskine82ebaa42021-01-12 00:45:14 +010090
Gilles Peskine45e9e732021-01-12 00:47:03 +010091def keep_manual_dependencies(file_name, function_name, arguments):
92 #pylint: disable=unused-argument
93 """Declare test functions with unusual dependencies here."""
94 return False
95
Gilles Peskinebdffaea2021-01-12 00:37:38 +010096def process_data_stanza(stanza, file_name, test_case_number):
97 """Update PSA crypto dependencies in one Mbed TLS test case.
98
99 stanza is the test case text (including the description, the dependencies,
100 the line with the function and arguments, and optionally comments). Return
101 a new stanza with an updated dependency line, preserving everything else
102 (description, comments, arguments, etc.).
103 """
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100104 if not stanza.lstrip('\n'):
105 # Just blank lines
106 return stanza
107 # Expect 2 or 3 non-comment lines: description, optional dependencies,
108 # function-and-arguments.
109 content_matches = list(re.finditer(r'^[\t ]*([^\t #].*)$', stanza, re.M))
110 if len(content_matches) < 2:
111 raise Exception('Not enough content lines in paragraph {} in {}'
112 .format(test_case_number, file_name))
113 if len(content_matches) > 3:
114 raise Exception('Too many content lines in paragraph {} in {}'
115 .format(test_case_number, file_name))
116 arguments = content_matches[-1].group(0).split(':')
117 function_name = arguments.pop(0)
Gilles Peskine45e9e732021-01-12 00:47:03 +0100118 if keep_manual_dependencies(file_name, function_name, arguments):
119 return stanza
Gilles Peskine82ebaa42021-01-12 00:45:14 +0100120 if len(content_matches) == 2:
121 # Insert a line for the dependencies. If it turns out that there are
122 # no dependencies, we'll remove that empty line below.
123 dependencies_location = content_matches[-1].start()
124 text_before = stanza[:dependencies_location]
125 text_after = '\n' + stanza[dependencies_location:]
126 old_dependencies = []
127 dependencies_leader = 'depends_on:'
128 else:
129 dependencies_match = content_matches[-2]
130 text_before = stanza[:dependencies_match.start()]
131 text_after = stanza[dependencies_match.end():]
132 old_dependencies = dependencies_match.group(0).split(':')
133 dependencies_leader = old_dependencies.pop(0) + ':'
134 if dependencies_leader != 'depends_on:':
135 raise Exception('Next-to-last line does not start with "depends_on:"'
136 ' in paragraph {} in {}'
137 .format(test_case_number, file_name))
138 new_dependencies = updated_dependencies(file_name, function_name, arguments,
139 old_dependencies)
140 if new_dependencies:
141 stanza = (text_before +
142 dependencies_leader + ':'.join(new_dependencies) +
143 text_after)
144 else:
145 # The dependencies have become empty. Remove the depends_on: line.
146 assert text_after[0] == '\n'
147 stanza = text_before + text_after[1:]
Gilles Peskinebdffaea2021-01-12 00:37:38 +0100148 return stanza
149
150def process_data_file(file_name, old_content):
151 """Update PSA crypto dependencies in an Mbed TLS test suite data file.
152
153 Process old_content (the old content of the file) and return the new content.
154 """
155 old_stanzas = old_content.split('\n\n')
156 new_stanzas = [process_data_stanza(stanza, file_name, n)
157 for n, stanza in enumerate(old_stanzas, start=1)]
158 return '\n\n'.join(new_stanzas)
159
160def update_file(file_name, old_content, new_content):
161 """Update the given file with the given new content.
162
163 Replace the existing file. The previous version is renamed to *.bak.
164 Don't modify the file if the content was unchanged.
165 """
166 if new_content == old_content:
167 return
168 backup = file_name + '.bak'
169 tmp = file_name + '.tmp'
170 with open(tmp, 'w', encoding='utf-8') as new_file:
171 new_file.write(new_content)
172 os.replace(file_name, backup)
173 os.replace(tmp, file_name)
174
175def process_file(file_name):
176 """Update PSA crypto dependencies in an Mbed TLS test suite data file.
177
178 Replace the existing file. The previous version is renamed to *.bak.
179 Don't modify the file if the content was unchanged.
180 """
181 old_content = open(file_name, encoding='utf-8').read()
182 if file_name.endswith('.data'):
183 new_content = process_data_file(file_name, old_content)
184 else:
185 raise Exception('File type not recognized: {}'
186 .format(file_name))
187 update_file(file_name, old_content, new_content)
188
189def main(args):
190 for file_name in args:
191 process_file(file_name)
192
193if __name__ == '__main__':
194 main(sys.argv[1:])