blob: 8b08220c0b2af708369452e9ba88d15f8b60664e [file] [log] [blame]
Govindraj Raja4db3c002025-04-10 17:23:19 -05001#!/usr/bin/env python3
2#
3# Copyright (c) 2025, Arm Limited. All rights reserved.
4#
5# SPDX-License-Identifier: BSD-3-Clause
6#
7
8import argparse
9import os
10import re
11import subprocess
12import sys
13import logging
14from pathlib import Path
15
16
17def subprocess_run(cmd, **kwargs):
18 logging.debug("Running command: %r %r", cmd, kwargs)
19 return subprocess.run(cmd, **kwargs)
20
21
22def parse_workarounds(filepath: str):
23 """
24 Parse the file line by line. For every start marker ('workaround_reset_start'
25 or 'workaround_runtime_start'), we look for its matching end marker
26 ('workaround_reset_end' or 'workaround_runtime_end').
27
28 If a start is missing its end, or if we find an end with no corresponding
29 start, set error value to True which is to be returned as a tuple along with
30 the list of dictionaries.
31
32 Returns:
33 A list of dictionaries. Each dictionary has:
34 - start_line: line number of the workaround start
35 - end_line: line number of the matching workaround end
36 - marker_type: 'reset' or 'runtime'
37 - erratum_number: integer if it's an ERRATUM (from ERRATUM(X)), else None
38 - cve_year: integer if it's a CVE, else None
39 - cve_number: integer if it's a CVE, else None
40 Error value set to True if we fail to match workaround start to an end.
41 """
42
43 # Read all lines in memory
44 with open(filepath, "r") as f:
45 lines = f.readlines()
46
47 # We'll keep a stack of active "starts" that haven't yet found their "end"
48 start_stack = []
49 results = []
50 error = False
51
52 # Regex patterns for capturing ERRATUM and CVE
53 # Example: ERRATUM(123) or CVE-2022-789
54 erratum_pattern = re.compile(r"ERRATUM\s*\(\s*(\d+)\s*\)", re.IGNORECASE)
55 cve_pattern = re.compile(r"CVE[-_:]?(\d{4})[-_:]?(\d+)", re.IGNORECASE)
56
57 for i, line in enumerate(lines, start=1):
58 stripped = line.strip()
59
60 # ----------------------------------------------------------------------
61 # 1) Check for "start" markers
62 # We look first for 'workaround_reset_start' or 'workaround_runtime_start'
63 # ----------------------------------------------------------------------
64 if "workaround_reset_start" in stripped:
65 marker_type = "reset"
66 # Attempt to extract ERRATUM or CVE
67 erratum_match = erratum_pattern.search(stripped)
68 cve_match = cve_pattern.search(stripped)
69
70 if erratum_match:
71 erratum_number = int(erratum_match.group(1))
72 cve_year, cve_number = None, None
73 elif cve_match:
74 erratum_number = None
75 cve_year = int(cve_match.group(1))
76 cve_number = int(cve_match.group(2))
77 else:
78 error |= True
79 logging.error(
80 f"Couldn't find a valid Errata number or CVE year "
81 f"in marker type {marker_type} in line number {i}"
82 )
83 return results, error
84
85 # Push onto the stack
86 start_stack.append({
87 "start_line": i,
88 "marker_type": marker_type, # 'reset'
89 "erratum_number": erratum_number,
90 "cve_year": cve_year,
91 "cve_number": cve_number
92 })
93
94 elif "workaround_runtime_start" in stripped:
95 marker_type = "runtime"
96 # Attempt to extract ERRATUM or CVE
97 erratum_match = erratum_pattern.search(stripped)
98 cve_match = cve_pattern.search(stripped)
99
100 if erratum_match:
101 erratum_number = int(erratum_match.group(1))
102 cve_year, cve_number = None, None
103 elif cve_match:
104 erratum_number = None
105 cve_year = int(cve_match.group(1))
106 cve_number = int(cve_match.group(2))
107 else:
108 error |= True
109 logging.error(
110 f"Couldn't find a valid Errata number or CVE year "
111 f"in marker type {marker_type} in line number {i}"
112 )
113 return results, error
114
115 # Push onto the stack
116 start_stack.append({
117 "start_line": i,
118 "marker_type": marker_type, # 'runtime'
119 "erratum_number": erratum_number,
120 "cve_year": cve_year,
121 "cve_number": cve_number
122 })
123
124 # ----------------------------------------------------------------------
125 # 2) Check for "end" markers
126 # We look for 'workaround_reset_end' or 'workaround_runtime_end'
127 # ----------------------------------------------------------------------
128 elif "workaround_reset_end" in stripped:
129 # Attempt to pop the most recent start
130 if not start_stack:
131 logging.error(
132 f"[Line {i}] Found 'workaround_reset_end' "
133 f"without matching 'workaround_reset_start'."
134 )
135 error |= True
136 break
137
138 # Pop the most recent start
139 last_item = start_stack.pop()
140
141 # Check the marker type
142 if last_item["marker_type"] != "reset":
143 error = True
144 logging.error(
145 f"[Line {i}] Found 'workaround_reset_end' "
146 f"that does not match "
147 f"the most recent '{last_item['marker_type']}' "
148 f"start at line {last_item['start_line']}."
149 )
150 error |= True
151 break
152
153 last_item["end_line"] = i
154 results.append(last_item)
155
156 elif "workaround_runtime_end" in stripped:
157 # We need a matching "runtime" start
158 if not start_stack:
159 logging.error(
160 f"[Line {i}] Found 'workaround_runtime_end' "
161 f"without matching start."
162 )
163 error |= True
164 break
165
166 # Pop the most recent start
167 last_item = start_stack.pop()
168
169 # Check the marker type
170 if last_item["marker_type"] != "runtime":
171 logging.error(
172 f"[Line {i}] Found 'workaround_runtime_end' "
173 f"that does not match "
174 f"the most recent '{last_item['marker_type']}' "
175 f"start at line {last_item['start_line']}."
176 )
177 error |= True
178 break
179
180 last_item["end_line"] = i
181 results.append(last_item)
182
183 # ----------------------------------------------------------------------
184 # After processing all lines, if the stack is not empty, it means some
185 # starts have no matching ends
186 # ----------------------------------------------------------------------
187 if start_stack:
188 first_unmatched = start_stack[0]
189 logging.error(
190 f"'workaround_{first_unmatched[1]}_start' "
191 f"at line {first_unmatched[0]} "
192 f"did not have a matching end marker."
193 )
194
195 return results, error
196
197
198def check_ascending_order(data):
199 """
200 Ensures that:
201 1) All ERRATUM blocks appear first (in ascending order of their erratum_number),
202 2) Then all CVE blocks appear (in ascending order of their cve_year and if the
203 year is the same, ascending by cve_number as well).
204
205 Returns:
206 False, If an ERRATUM appears after a CVE has started, or if the ordering within
207 ERRATUMs or CVEs is incorrect, else returns True.
208 """
209
210 # Sort everything by the line number where the workaround starts
211 data_sorted = sorted(data, key=lambda x: x["start_line"])
212
213 # We'll gather ERRATUM items first, in the order they appear,
214 # then CVE items. If we ever see an ERRATUM after we've started
215 # collecting CVEs, we'll raise an error.
216 found_cve = False
217 errata_list = []
218 cve_list = []
219
220 for item in data_sorted:
221 # Is this entry an ERRATUM or a CVE?
222 if item["erratum_number"] is not None: # This is an ERRATUM
223 if found_cve:
224 # We already encountered a CVE, so no more ERRATUMs allowed
225 logging.error(
226 f"ERRATUM({item['erratum_number']}) found "
227 f"at line {item['start_line']} "
228 f"after the first CVE has already appeared."
229 )
230 return False
231 errata_list.append(item)
232 elif item["cve_year"] is not None: # This is a CVE
233 found_cve = True
234 cve_list.append(item)
235 else:
236 # If neither erratum_number nor cve_year is present
237 # return False to fail the check.
238 logging.error(
239 f"ERRATUM or CVE year not found at "
240 f"line {item['start_line']}"
241 )
242 return False
243
244 # -------------------------------------------------------------
245 # 1) Check ascending order of ERRATUM IDs
246 # -------------------------------------------------------------
247 prev_erratum = 0
248 for erratum_item in errata_list:
249 eno = erratum_item["erratum_number"]
250 if prev_erratum and eno < prev_erratum:
251 logging.error(
252 f"ERRATUM IDs are not in ascending order! "
253 f"Found ERRATUM({eno}) "
254 f"after ERRATUM({prev_erratum})."
255 )
256 return False
257 prev_erratum = eno
258
259 # -------------------------------------------------------------
260 # 2) Check CVE year (and then CVE number) are ascending
261 # -------------------------------------------------------------
262 prev_cve_year = 0
263 prev_cve_number = 0
264 for cve_item in cve_list:
265 year = cve_item["cve_year"]
266 num = cve_item["cve_number"]
267
268 if prev_cve_year and year < prev_cve_year:
269 logging.error(
270 f"CVE years are not in ascending order! "
271 f"Found CVE({year},...) "
272 f"after CVE({prev_cve_year},...)."
273 )
274 return False
275 elif year == prev_cve_year:
276 # Years match, so check if this CVE number < previous CVE number
277 if num < prev_cve_number:
278 logging.error(
279 f"CVE Numbers are not in ascending order! "
280 f"Found CVE({year, num} ,...) "
281 f"after CVE({prev_cve_year, prev_cve_number},...)."
282 )
283 return False
284
285 # Update previous references
286 prev_cve_year = year
287 prev_cve_number = num
288
289 # If we reach here, then the ordering is correct return True.
290 return True
291
292
293def patch_has_cpu_files(base_commit, end_commit):
294 """Get the output of a git diff and analyse each modified file."""
295
296 # Get patches of the affected commits with one line of context.
297 gitdiff = subprocess_run(
298 [
299 "git",
300 "diff",
301 "--name-only",
Govindraj Raja8a990892025-09-18 15:57:34 -0500302 "--diff-filter=ACMRT",
Govindraj Raja4db3c002025-04-10 17:23:19 -0500303 base_commit + ".." + end_commit,
304 "lib/cpus/aarch64/"
305 ],
306 stdout=subprocess.PIPE,
307 )
308
309 if gitdiff.returncode != 0:
310 return False
311
312 cpu_files_modified = gitdiff.stdout.decode("utf-8").splitlines()
313 return cpu_files_modified
314
315
316def list_files_in_directory(dir_path):
317 """
318 Returns a list of files in the specified directory.
319 Args:
320 dir_path: The path to the directory.
321
322 Returns:
323 A list of file names in the directory.
324 """
325 try:
326 files = [
327 os.path.join(dir_path, f) for f in os.listdir(dir_path)
328 if os.path.isfile(os.path.join(dir_path, f))
329 ]
330 return files
331 except FileNotFoundError:
332 return f"Directory not found: {dir_path}"
333 except NotADirectoryError:
334 return f"Not a directory: {dir_path}"
335 except Exception as e:
336 return f"An error occurred: {e}"
337
338
339def parse_cmd_line(argv, prog_name):
340 parser = argparse.ArgumentParser(
341 prog=prog_name,
342 formatter_class=argparse.RawTextHelpFormatter,
343 description="Check alphabetical order of #includes",
344 epilog="""
345For each source file in the tree, checks that #include's C preprocessor
346directives are ordered alphabetically (as mandated by the Trusted
347Firmware coding style). System header includes must come before user
348header includes.
349""",
350 )
351
352 parser.add_argument(
353 "--tree",
354 "-t",
355 help="Path to the source tree to check (default: %(default)s)",
356 default=os.curdir,
357 )
358 parser.add_argument(
359 "--patch",
360 "-p",
361 help="""
362Patch mode.
363Instead of checking all files in the source tree, the script will consider
364only files that are modified by the latest patch(es).""",
365 action="store_true",
366 )
367 parser.add_argument(
368 "--from-ref",
369 help="Base commit in patch mode (default: %(default)s)",
370 default="master",
371 )
372 parser.add_argument(
373 "--to-ref",
374 help="Final commit in patch mode (default: %(default)s)",
375 default="HEAD",
376 )
377 parser.add_argument(
378 "--debug",
379 help="Enable debug logging",
380 action="store_true",
381 )
382
383 args = parser.parse_args(argv)
384 return args
385
386
387if __name__ == "__main__":
388 args = parse_cmd_line(sys.argv[1:], sys.argv[0])
389
390 if args.debug:
391 logging.basicConfig(level=logging.DEBUG)
392 else:
393 logging.basicConfig(level=logging.INFO)
394
395 os.chdir(args.tree)
396
397 if args.patch:
398 logging.info(
399 "Checking CPU files modified between patches "
400 + args.from_ref
401 + " and "
402 + args.to_ref
403 + " ..."
404 )
405 list_cpu_files = patch_has_cpu_files(args.from_ref, args.to_ref)
406 if not list_cpu_files:
407 logging.info(f"No CPU files Modified")
408 sys.exit(0)
409 else:
410 dir_path = "lib/cpus/aarch64/"
411 logging.info(f"Checking all CPU files in directory `{dir_path}`")
412 list_cpu_files = list_files_in_directory(dir_path)
413 if not list_cpu_files:
414 logging.error(f"`lib/cpus/aarch64/` directory is empty")
415 sys.exit(1)
416
417 failure = False
418 for file in list_cpu_files:
419 logging.info(f"Checking File {file} .....")
420 # 1. Parse the file for workaround blocks
421 parsed_data, error = parse_workarounds(file)
422 if error:
423 failure |= True
424
425 if args.debug:
426 for entry in parsed_data:
427 logging.debug(entry)
428
429 if not parsed_data:
430 logging.info(f"No Workarounds found in {file}.")
431 continue
432
433 # 2. Check ascending order of Erratum IDs and CVE years
434 if check_ascending_order(parsed_data):
435 # 3. Print out if all is well
436 logging.info(
437 f"Workarounds matched correctly, and Errata "
438 f"IDs and CVE's are in ascending order.")
439 else:
440 logging.error(
441 f"Workarounds didn't match correctly, or Errata "
442 f"IDs and CVE's are not in ascending order.")
443 failure |= True
444
445 if failure:
446 sys.exit(1)
447
448 sys.exit(0)