| Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 1 | #!/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 | 
| Dave Rodgman | 16799db | 2023-11-02 19:47:20 +0000 | [diff] [blame] | 7 | # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later | 
| Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 8 |  | 
|  | 9 | import os | 
| Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 10 | import re | 
| Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 11 | import sys | 
|  | 12 |  | 
| Gilles Peskine | 9bbba5e | 2021-01-12 00:55:55 +0100 | [diff] [blame] | 13 | CLASSIC_DEPENDENCIES = frozenset([ | 
| Bence Szépkúti | bb0cfeb | 2021-05-28 09:42:25 +0200 | [diff] [blame] | 14 | # This list is manually filtered from mbedtls_config.h. | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 15 |  | 
|  | 16 | # Mbed TLS feature support. | 
|  | 17 | # Only features that affect what can be done are listed here. | 
|  | 18 | # Options that control optimizations or alternative implementations | 
|  | 19 | # are omitted. | 
| Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 20 | 'MBEDTLS_CIPHER_MODE_CBC', | 
|  | 21 | 'MBEDTLS_CIPHER_MODE_CFB', | 
|  | 22 | 'MBEDTLS_CIPHER_MODE_CTR', | 
|  | 23 | 'MBEDTLS_CIPHER_MODE_OFB', | 
|  | 24 | 'MBEDTLS_CIPHER_MODE_XTS', | 
|  | 25 | 'MBEDTLS_CIPHER_NULL_CIPHER', | 
|  | 26 | 'MBEDTLS_CIPHER_PADDING_PKCS7', | 
|  | 27 | 'MBEDTLS_CIPHER_PADDING_ONE_AND_ZEROS', | 
|  | 28 | 'MBEDTLS_CIPHER_PADDING_ZEROS_AND_LEN', | 
|  | 29 | 'MBEDTLS_CIPHER_PADDING_ZEROS', | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 30 | #curve#'MBEDTLS_ECP_DP_SECP192R1_ENABLED', | 
|  | 31 | #curve#'MBEDTLS_ECP_DP_SECP224R1_ENABLED', | 
|  | 32 | #curve#'MBEDTLS_ECP_DP_SECP256R1_ENABLED', | 
|  | 33 | #curve#'MBEDTLS_ECP_DP_SECP384R1_ENABLED', | 
|  | 34 | #curve#'MBEDTLS_ECP_DP_SECP521R1_ENABLED', | 
|  | 35 | #curve#'MBEDTLS_ECP_DP_SECP192K1_ENABLED', | 
|  | 36 | #curve#'MBEDTLS_ECP_DP_SECP224K1_ENABLED', | 
|  | 37 | #curve#'MBEDTLS_ECP_DP_SECP256K1_ENABLED', | 
|  | 38 | #curve#'MBEDTLS_ECP_DP_BP256R1_ENABLED', | 
|  | 39 | #curve#'MBEDTLS_ECP_DP_BP384R1_ENABLED', | 
|  | 40 | #curve#'MBEDTLS_ECP_DP_BP512R1_ENABLED', | 
|  | 41 | #curve#'MBEDTLS_ECP_DP_CURVE25519_ENABLED', | 
|  | 42 | #curve#'MBEDTLS_ECP_DP_CURVE448_ENABLED', | 
|  | 43 | 'MBEDTLS_ECDSA_DETERMINISTIC', | 
|  | 44 | #'MBEDTLS_GENPRIME', #needed for RSA key generation | 
|  | 45 | 'MBEDTLS_PKCS1_V15', | 
|  | 46 | 'MBEDTLS_PKCS1_V21', | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 47 |  | 
|  | 48 | # Mbed TLS modules. | 
|  | 49 | # Only modules that provide cryptographic mechanisms are listed here. | 
|  | 50 | # Platform, data formatting, X.509 or TLS modules are omitted. | 
| Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 51 | 'MBEDTLS_AES_C', | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 52 | 'MBEDTLS_BIGNUM_C', | 
| Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 53 | 'MBEDTLS_CAMELLIA_C', | 
|  | 54 | 'MBEDTLS_ARIA_C', | 
|  | 55 | 'MBEDTLS_CCM_C', | 
|  | 56 | 'MBEDTLS_CHACHA20_C', | 
|  | 57 | 'MBEDTLS_CHACHAPOLY_C', | 
|  | 58 | 'MBEDTLS_CMAC_C', | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 59 | 'MBEDTLS_CTR_DRBG_C', | 
| Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 60 | 'MBEDTLS_DES_C', | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 61 | 'MBEDTLS_ECDH_C', | 
|  | 62 | 'MBEDTLS_ECDSA_C', | 
|  | 63 | 'MBEDTLS_ECJPAKE_C', | 
|  | 64 | 'MBEDTLS_ECP_C', | 
|  | 65 | 'MBEDTLS_ENTROPY_C', | 
| Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 66 | 'MBEDTLS_GCM_C', | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 67 | 'MBEDTLS_HKDF_C', | 
|  | 68 | 'MBEDTLS_HMAC_DRBG_C', | 
| Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 69 | 'MBEDTLS_NIST_KW_C', | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 70 | 'MBEDTLS_MD5_C', | 
|  | 71 | 'MBEDTLS_PKCS5_C', | 
|  | 72 | 'MBEDTLS_PKCS12_C', | 
| Ronald Cron | 9f97c6e | 2021-03-18 16:05:03 +0100 | [diff] [blame] | 73 | 'MBEDTLS_POLY1305_C', | 
|  | 74 | 'MBEDTLS_RIPEMD160_C', | 
| Gilles Peskine | 81dec00 | 2021-01-12 00:59:09 +0100 | [diff] [blame] | 75 | 'MBEDTLS_RSA_C', | 
|  | 76 | 'MBEDTLS_SHA1_C', | 
|  | 77 | 'MBEDTLS_SHA256_C', | 
|  | 78 | 'MBEDTLS_SHA512_C', | 
| Gilles Peskine | 9bbba5e | 2021-01-12 00:55:55 +0100 | [diff] [blame] | 79 | ]) | 
|  | 80 |  | 
|  | 81 | def is_classic_dependency(dep): | 
|  | 82 | """Whether dep is a classic dependency that PSA test cases should not use.""" | 
|  | 83 | if dep.startswith('!'): | 
|  | 84 | dep = dep[1:] | 
|  | 85 | return dep in CLASSIC_DEPENDENCIES | 
|  | 86 |  | 
| Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 87 | def is_systematic_dependency(dep): | 
|  | 88 | """Whether dep is a PSA dependency which is determined systematically.""" | 
| Ronald Cron | 6ac020d | 2021-03-23 17:40:47 +0100 | [diff] [blame] | 89 | if dep.startswith('PSA_WANT_ECC_'): | 
|  | 90 | return False | 
| Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 91 | return dep.startswith('PSA_WANT_') | 
|  | 92 |  | 
| Gilles Peskine | fa37961 | 2021-01-12 21:14:46 +0100 | [diff] [blame] | 93 | WITHOUT_SYSTEMATIC_DEPENDENCIES = frozenset([ | 
| Bence Szépkúti | a63b20d | 2020-12-16 11:36:46 +0100 | [diff] [blame] | 94 | 'PSA_ALG_AEAD_WITH_SHORTENED_TAG', # only a modifier | 
| Gilles Peskine | f032fa9 | 2021-01-12 01:01:26 +0100 | [diff] [blame] | 95 | 'PSA_ALG_ANY_HASH', # only meaningful in policies | 
|  | 96 | 'PSA_ALG_KEY_AGREEMENT', # only a way to combine algorithms | 
|  | 97 | 'PSA_ALG_TRUNCATED_MAC', # only a modifier | 
| Gilles Peskine | 60b29fe | 2021-02-16 14:06:50 +0100 | [diff] [blame] | 98 | 'PSA_KEY_TYPE_NONE', # not a real key type | 
|  | 99 | 'PSA_KEY_TYPE_DERIVE', # always supported, don't list it to reduce noise | 
|  | 100 | 'PSA_KEY_TYPE_RAW_DATA', # always supported, don't list it to reduce noise | 
| Steven Cooreman | 2c2efa4 | 2021-02-23 09:36:42 +0100 | [diff] [blame] | 101 | 'PSA_ALG_AT_LEAST_THIS_LENGTH_MAC', #only a modifier | 
|  | 102 | 'PSA_ALG_AEAD_WITH_AT_LEAST_THIS_LENGTH_TAG', #only a modifier | 
| Gilles Peskine | e4f539c | 2021-01-12 00:58:36 +0100 | [diff] [blame] | 103 | ]) | 
|  | 104 |  | 
| Gilles Peskine | f032fa9 | 2021-01-12 01:01:26 +0100 | [diff] [blame] | 105 | SPECIAL_SYSTEMATIC_DEPENDENCIES = { | 
|  | 106 | 'PSA_ALG_ECDSA_ANY': frozenset(['PSA_WANT_ALG_ECDSA']), | 
|  | 107 | 'PSA_ALG_RSA_PKCS1V15_SIGN_RAW': frozenset(['PSA_WANT_ALG_RSA_PKCS1V15_SIGN']), | 
|  | 108 | } | 
|  | 109 |  | 
| Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 110 | def dependencies_of_symbol(symbol): | 
|  | 111 | """Return the dependencies for a symbol that designates a cryptographic mechanism.""" | 
| Gilles Peskine | fa37961 | 2021-01-12 21:14:46 +0100 | [diff] [blame] | 112 | if symbol in WITHOUT_SYSTEMATIC_DEPENDENCIES: | 
| Gilles Peskine | e4f539c | 2021-01-12 00:58:36 +0100 | [diff] [blame] | 113 | return frozenset() | 
| Gilles Peskine | f032fa9 | 2021-01-12 01:01:26 +0100 | [diff] [blame] | 114 | if symbol in SPECIAL_SYSTEMATIC_DEPENDENCIES: | 
|  | 115 | return SPECIAL_SYSTEMATIC_DEPENDENCIES[symbol] | 
| Gilles Peskine | 20987b9 | 2021-01-12 01:11:32 +0100 | [diff] [blame] | 116 | if symbol.startswith('PSA_ALG_CATEGORY_') or \ | 
|  | 117 | symbol.startswith('PSA_KEY_TYPE_CATEGORY_'): | 
|  | 118 | # Categories are used in test data when an unsupported but plausible | 
|  | 119 | # mechanism number needed. They have no associated dependency. | 
|  | 120 | return frozenset() | 
| Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 121 | return {symbol.replace('_', '_WANT_', 1)} | 
|  | 122 |  | 
|  | 123 | def systematic_dependencies(file_name, function_name, arguments): | 
| Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 124 | """List the systematically determined dependency for a test case.""" | 
|  | 125 | deps = set() | 
| Gilles Peskine | 72d8e0a | 2021-01-12 01:11:42 +0100 | [diff] [blame] | 126 |  | 
|  | 127 | # Run key policy negative tests even if the algorithm to attempt performing | 
| Ronald Cron | 9838dc2 | 2021-03-24 09:18:23 +0100 | [diff] [blame] | 128 | # is not supported but in the case where the test is to check an | 
|  | 129 | # incompatibility between a requested algorithm for a cryptographic | 
|  | 130 | # operation and a key policy. In the latter, we want to filter out the | 
|  | 131 | # cases # where PSA_ERROR_NOT_SUPPORTED is returned instead of | 
|  | 132 | # PSA_ERROR_NOT_PERMITTED. | 
| Gilles Peskine | 72d8e0a | 2021-01-12 01:11:42 +0100 | [diff] [blame] | 133 | if function_name.endswith('_key_policy') and \ | 
| Ronald Cron | 9838dc2 | 2021-03-24 09:18:23 +0100 | [diff] [blame] | 134 | arguments[-1].startswith('PSA_ERROR_') and \ | 
|  | 135 | arguments[-1] != ('PSA_ERROR_NOT_PERMITTED'): | 
| Gilles Peskine | 72d8e0a | 2021-01-12 01:11:42 +0100 | [diff] [blame] | 136 | arguments[-2] = '' | 
| Gilles Peskine | b51d72f | 2021-01-12 21:15:52 +0100 | [diff] [blame] | 137 | if function_name == 'copy_fail' and \ | 
|  | 138 | arguments[-1].startswith('PSA_ERROR_'): | 
|  | 139 | arguments[-2] = '' | 
|  | 140 | arguments[-3] = '' | 
| Gilles Peskine | 72d8e0a | 2021-01-12 01:11:42 +0100 | [diff] [blame] | 141 |  | 
| Gilles Peskine | 80a9708 | 2021-01-12 21:18:36 +0100 | [diff] [blame] | 142 | # Storage format tests that only look at how the file is structured and | 
|  | 143 | # don't care about the format of the key material don't depend on any | 
|  | 144 | # cryptographic mechanisms. | 
|  | 145 | if os.path.basename(file_name) == 'test_suite_psa_crypto_persistent_key.data' and \ | 
|  | 146 | function_name in {'format_storage_data_check', | 
|  | 147 | 'parse_storage_data_check'}: | 
|  | 148 | return [] | 
|  | 149 |  | 
| Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 150 | for arg in arguments: | 
|  | 151 | for symbol in re.findall(r'PSA_(?:ALG|KEY_TYPE)_\w+', arg): | 
|  | 152 | deps.update(dependencies_of_symbol(symbol)) | 
|  | 153 | return sorted(deps) | 
|  | 154 |  | 
| Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 155 | def updated_dependencies(file_name, function_name, arguments, dependencies): | 
|  | 156 | """Rework the list of dependencies into PSA_WANT_xxx. | 
|  | 157 |  | 
|  | 158 | Remove classic crypto dependencies such as MBEDTLS_RSA_C, | 
|  | 159 | MBEDTLS_PKCS1_V15, etc. | 
|  | 160 |  | 
|  | 161 | Add systematic PSA_WANT_xxx dependencies based on the called function and | 
|  | 162 | its arguments, replacing existing PSA_WANT_xxx dependencies. | 
|  | 163 | """ | 
| Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 164 | automatic = systematic_dependencies(file_name, function_name, arguments) | 
|  | 165 | manual = [dep for dep in dependencies | 
| Gilles Peskine | 9bbba5e | 2021-01-12 00:55:55 +0100 | [diff] [blame] | 166 | if not (is_systematic_dependency(dep) or | 
|  | 167 | is_classic_dependency(dep))] | 
| Gilles Peskine | 2d2e924 | 2021-01-12 00:52:31 +0100 | [diff] [blame] | 168 | return automatic + manual | 
| Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 169 |  | 
| Gilles Peskine | 45e9e73 | 2021-01-12 00:47:03 +0100 | [diff] [blame] | 170 | def keep_manual_dependencies(file_name, function_name, arguments): | 
|  | 171 | #pylint: disable=unused-argument | 
|  | 172 | """Declare test functions with unusual dependencies here.""" | 
| Gilles Peskine | a87e178 | 2021-01-12 01:13:39 +0100 | [diff] [blame] | 173 | # If there are no arguments, we can't do any useful work. Assume that if | 
|  | 174 | # there are dependencies, they are warranted. | 
|  | 175 | if not arguments: | 
|  | 176 | return True | 
| Gilles Peskine | 20987b9 | 2021-01-12 01:11:32 +0100 | [diff] [blame] | 177 | # When PSA_ERROR_NOT_SUPPORTED is expected, usually, at least one of the | 
|  | 178 | # constants mentioned in the test should not be supported. It isn't | 
|  | 179 | # possible to determine which one in a systematic way. So let the programmer | 
|  | 180 | # decide. | 
|  | 181 | if arguments[-1] == 'PSA_ERROR_NOT_SUPPORTED': | 
|  | 182 | return True | 
| Gilles Peskine | 45e9e73 | 2021-01-12 00:47:03 +0100 | [diff] [blame] | 183 | return False | 
|  | 184 |  | 
| Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 185 | def process_data_stanza(stanza, file_name, test_case_number): | 
|  | 186 | """Update PSA crypto dependencies in one Mbed TLS test case. | 
|  | 187 |  | 
|  | 188 | stanza is the test case text (including the description, the dependencies, | 
|  | 189 | the line with the function and arguments, and optionally comments). Return | 
|  | 190 | a new stanza with an updated dependency line, preserving everything else | 
|  | 191 | (description, comments, arguments, etc.). | 
|  | 192 | """ | 
| Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 193 | if not stanza.lstrip('\n'): | 
|  | 194 | # Just blank lines | 
|  | 195 | return stanza | 
|  | 196 | # Expect 2 or 3 non-comment lines: description, optional dependencies, | 
|  | 197 | # function-and-arguments. | 
|  | 198 | content_matches = list(re.finditer(r'^[\t ]*([^\t #].*)$', stanza, re.M)) | 
|  | 199 | if len(content_matches) < 2: | 
|  | 200 | raise Exception('Not enough content lines in paragraph {} in {}' | 
|  | 201 | .format(test_case_number, file_name)) | 
|  | 202 | if len(content_matches) > 3: | 
|  | 203 | raise Exception('Too many content lines in paragraph {} in {}' | 
|  | 204 | .format(test_case_number, file_name)) | 
|  | 205 | arguments = content_matches[-1].group(0).split(':') | 
|  | 206 | function_name = arguments.pop(0) | 
| Gilles Peskine | 45e9e73 | 2021-01-12 00:47:03 +0100 | [diff] [blame] | 207 | if keep_manual_dependencies(file_name, function_name, arguments): | 
|  | 208 | return stanza | 
| Gilles Peskine | 82ebaa4 | 2021-01-12 00:45:14 +0100 | [diff] [blame] | 209 | if len(content_matches) == 2: | 
|  | 210 | # Insert a line for the dependencies. If it turns out that there are | 
|  | 211 | # no dependencies, we'll remove that empty line below. | 
|  | 212 | dependencies_location = content_matches[-1].start() | 
|  | 213 | text_before = stanza[:dependencies_location] | 
|  | 214 | text_after = '\n' + stanza[dependencies_location:] | 
|  | 215 | old_dependencies = [] | 
|  | 216 | dependencies_leader = 'depends_on:' | 
|  | 217 | else: | 
|  | 218 | dependencies_match = content_matches[-2] | 
|  | 219 | text_before = stanza[:dependencies_match.start()] | 
|  | 220 | text_after = stanza[dependencies_match.end():] | 
|  | 221 | old_dependencies = dependencies_match.group(0).split(':') | 
|  | 222 | dependencies_leader = old_dependencies.pop(0) + ':' | 
|  | 223 | if dependencies_leader != 'depends_on:': | 
|  | 224 | raise Exception('Next-to-last line does not start with "depends_on:"' | 
|  | 225 | ' in paragraph {} in {}' | 
|  | 226 | .format(test_case_number, file_name)) | 
|  | 227 | new_dependencies = updated_dependencies(file_name, function_name, arguments, | 
|  | 228 | old_dependencies) | 
|  | 229 | if new_dependencies: | 
|  | 230 | stanza = (text_before + | 
|  | 231 | dependencies_leader + ':'.join(new_dependencies) + | 
|  | 232 | text_after) | 
|  | 233 | else: | 
|  | 234 | # The dependencies have become empty. Remove the depends_on: line. | 
|  | 235 | assert text_after[0] == '\n' | 
|  | 236 | stanza = text_before + text_after[1:] | 
| Gilles Peskine | bdffaea | 2021-01-12 00:37:38 +0100 | [diff] [blame] | 237 | return stanza | 
|  | 238 |  | 
|  | 239 | def process_data_file(file_name, old_content): | 
|  | 240 | """Update PSA crypto dependencies in an Mbed TLS test suite data file. | 
|  | 241 |  | 
|  | 242 | Process old_content (the old content of the file) and return the new content. | 
|  | 243 | """ | 
|  | 244 | old_stanzas = old_content.split('\n\n') | 
|  | 245 | new_stanzas = [process_data_stanza(stanza, file_name, n) | 
|  | 246 | for n, stanza in enumerate(old_stanzas, start=1)] | 
|  | 247 | return '\n\n'.join(new_stanzas) | 
|  | 248 |  | 
|  | 249 | def update_file(file_name, old_content, new_content): | 
|  | 250 | """Update the given file with the given new content. | 
|  | 251 |  | 
|  | 252 | Replace the existing file. The previous version is renamed to *.bak. | 
|  | 253 | Don't modify the file if the content was unchanged. | 
|  | 254 | """ | 
|  | 255 | if new_content == old_content: | 
|  | 256 | return | 
|  | 257 | backup = file_name + '.bak' | 
|  | 258 | tmp = file_name + '.tmp' | 
|  | 259 | with open(tmp, 'w', encoding='utf-8') as new_file: | 
|  | 260 | new_file.write(new_content) | 
|  | 261 | os.replace(file_name, backup) | 
|  | 262 | os.replace(tmp, file_name) | 
|  | 263 |  | 
|  | 264 | def process_file(file_name): | 
|  | 265 | """Update PSA crypto dependencies in an Mbed TLS test suite data file. | 
|  | 266 |  | 
|  | 267 | Replace the existing file. The previous version is renamed to *.bak. | 
|  | 268 | Don't modify the file if the content was unchanged. | 
|  | 269 | """ | 
|  | 270 | old_content = open(file_name, encoding='utf-8').read() | 
|  | 271 | if file_name.endswith('.data'): | 
|  | 272 | new_content = process_data_file(file_name, old_content) | 
|  | 273 | else: | 
|  | 274 | raise Exception('File type not recognized: {}' | 
|  | 275 | .format(file_name)) | 
|  | 276 | update_file(file_name, old_content, new_content) | 
|  | 277 |  | 
|  | 278 | def main(args): | 
|  | 279 | for file_name in args: | 
|  | 280 | process_file(file_name) | 
|  | 281 |  | 
|  | 282 | if __name__ == '__main__': | 
|  | 283 | main(sys.argv[1:]) |