mirror of
https://github.com/espressif/esp-idf.git
synced 2026-07-13 14:33:06 +03:00
287 lines
9.1 KiB
Python
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())
|