Files
esp-idf/components/esp_blockdev/tools/check_ioctl_overlap.py

287 lines
9.1 KiB
Python

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
"""Check ESP-Blockdev ioctl reservation overlaps.
Each registered file is preprocessed with ESP_BLOCKDEV_CHECK_CMD_OVERLAP defined.
The preprocessor expands reservation macros into sentinel marker calls, which are
then parsed and validated here.
"""
from __future__ import annotations
import argparse
import os
import re
import subprocess
import sys
from collections.abc import Iterable
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
MARKER_TOKEN = 'ESP_BLOCKDEV_RESERVE_MARKER'
@dataclass(frozen=True)
class Reservation:
component: str
start: int
end: int
source_file: str
class ReservationError(Exception):
pass
class ExpressionError(ReservationError):
pass
class MarkerParseError(ReservationError):
pass
def format_hex(value: int) -> str:
return f'0x{value:02X}'
def preprocess_file(path: str | Path, compiler: str, include_dirs: Iterable[str]) -> str:
cmd = [compiler, '-E', '-P', '-DESP_BLOCKDEV_CHECK_CMD_OVERLAP']
for include_dir in include_dirs:
cmd.extend(['-I', include_dir])
cmd.append(str(path))
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise ReservationError(f'preprocessor failed for {path}:\n{proc.stderr.strip() or proc.stdout.strip()}')
return proc.stdout
def _iter_marker_payloads(text: str) -> Iterator[str]:
idx = 0
while True:
hit = text.find(MARKER_TOKEN, idx)
if hit < 0:
return
pos = hit + len(MARKER_TOKEN)
while pos < len(text) and text[pos].isspace():
pos += 1
if pos >= len(text) or text[pos] != '(':
raise MarkerParseError(f'found {MARKER_TOKEN} without argument list')
depth = 0
start = pos + 1
pos = start
while pos < len(text):
ch = text[pos]
if ch == '(':
depth += 1
elif ch == ')':
if depth == 0:
yield text[start:pos]
idx = pos + 1
break
depth -= 1
pos += 1
else:
raise MarkerParseError(f'unterminated {MARKER_TOKEN} call')
def _split_top_level_commas(payload: str) -> list[str]:
parts: list[str] = []
depth = 0
start = 0
for idx, ch in enumerate(payload):
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if depth < 0:
raise MarkerParseError(f'unbalanced parentheses in {payload!r}')
elif ch == ',' and depth == 0:
parts.append(payload[start:idx].strip())
start = idx + 1
parts.append(payload[start:].strip())
if depth != 0:
raise MarkerParseError(f'unbalanced parentheses in {payload!r}')
return parts
_TOKEN_RE = re.compile(r'\s*(0[xX][0-9a-fA-F]+|\d+|[()+-])')
class _ExprParser:
def __init__(self, text: str) -> None:
self.text = text
self.pos = 0
def parse(self) -> int:
value = self._parse_expr()
self._skip_ws()
if self.pos != len(self.text):
raise ExpressionError(f'unexpected trailing input: {self.text[self.pos :]!r}')
return value
def _skip_ws(self) -> None:
while self.pos < len(self.text) and self.text[self.pos].isspace():
self.pos += 1
def _peek(self) -> str | None:
self._skip_ws()
if self.pos >= len(self.text):
return None
return self.text[self.pos]
def _consume(self, ch: str) -> bool:
self._skip_ws()
if self.pos < len(self.text) and self.text[self.pos] == ch:
self.pos += 1
return True
return False
def _parse_expr(self) -> int:
value = self._parse_factor()
while True:
if self._consume('+'):
value += self._parse_factor()
elif self._consume('-'):
value -= self._parse_factor()
else:
return value
def _parse_factor(self) -> int:
if self._consume('+'):
return self._parse_factor()
if self._consume('-'):
return -self._parse_factor()
if self._consume('('):
value = self._parse_expr()
if not self._consume(')'):
raise ExpressionError(f"missing ')' in {self.text!r}")
return value
return self._parse_int()
def _parse_int(self) -> int:
self._skip_ws()
match = _TOKEN_RE.match(self.text, self.pos)
if not match:
raise ExpressionError(f'expected integer expression near: {self.text[self.pos :]!r}')
token = match.group(1)
self.pos = match.end()
return int(token, 0)
def eval_int_expr(expr: str) -> int:
return _ExprParser(expr).parse()
def extract_reservations(text: str, source_file: str) -> list[Reservation]:
reservations: list[Reservation] = []
for payload in _iter_marker_payloads(text):
parts = _split_top_level_commas(payload)
if len(parts) != 3:
raise MarkerParseError(f'expected 3 arguments in {MARKER_TOKEN}, got {len(parts)}: {payload!r}')
component, start_expr, end_expr = parts
start = eval_int_expr(start_expr)
end = eval_int_expr(end_expr)
reservations.append(Reservation(component=component, start=start, end=end, source_file=source_file))
return reservations
def validate_reservations(reservations: Iterable[Reservation]) -> list[str]:
errors: list[str] = []
items = list(reservations)
# Deduplicate identical reservations (same component and range from different source files)
seen: set[tuple[str, int, int]] = set()
unique_items: list[Reservation] = []
for item in items:
key = (item.component, item.start, item.end)
if key not in seen:
seen.add(key)
unique_items.append(item)
items = unique_items
for item in items:
if item.start > item.end:
errors.append(
f' {item.component}: start {format_hex(item.start)} > end'
f' {format_hex(item.end)} from {item.source_file}'
)
if not 0 <= item.start <= 0xFF:
errors.append(
f' {item.component}: start {format_hex(item.start)} out of range (0x00..0xFF) from {item.source_file}'
)
if not 0 <= item.end <= 0xFF:
errors.append(
f' {item.component}: end {format_hex(item.end)} out of range (0x00..0xFF) from {item.source_file}'
)
sorted_items = sorted(items, key=lambda r: (r.start, r.end, r.component, r.source_file))
if not sorted_items:
return errors
active = sorted_items[0]
for current in sorted_items[1:]:
if current.start <= active.end:
errors.append('ioctl reservation overlap:')
width = max(len(active.component), len(current.component))
pad_a = ' ' * (width - len(active.component) + 1)
pad_c = ' ' * (width - len(current.component) + 1)
errors.append(
f' {active.component}:{pad_a}'
f'{format_hex(active.start)}..{format_hex(active.end)}'
f' from {active.source_file}'
)
errors.append(
f' {current.component}:{pad_c}'
f'{format_hex(current.start)}..{format_hex(current.end)}'
f' from {current.source_file}'
)
if current.end > active.end:
active = current
elif current.end > active.end:
active = current
return errors
def check_files(files: Iterable[str | Path], compiler: str, include_dirs: Iterable[str]) -> list[Reservation]:
reservations: list[Reservation] = []
for file_path in files:
text = preprocess_file(file_path, compiler=compiler, include_dirs=include_dirs)
reservations.extend(extract_reservations(text, source_file=str(file_path)))
errors = validate_reservations(reservations)
if errors:
raise ReservationError('\n'.join(errors))
return reservations
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'--compiler',
default=os.environ.get('CC', 'gcc'),
help='C compiler executable used for preprocessing (default: $CC or gcc)',
)
parser.add_argument(
'--include-dir', action='append', default=[], help='Additional include directory for preprocessing (repeatable)'
)
parser.add_argument('--files', nargs='+', required=True, help='Reservation definition files to check for overlaps')
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_arg_parser()
args = parser.parse_args(argv)
try:
check_files(args.files, compiler=args.compiler, include_dirs=args.include_dir)
except ReservationError as exc:
print(str(exc), file=sys.stderr)
return 1
return 0
if __name__ == '__main__':
raise SystemExit(main())