mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
feat(esp_blockdev): add build-time ioctl command reservation overlap checker
(cherry picked from commit a9f545bcea)
This commit is contained in:
committed by
Martin Vychodil
parent
93056b5253
commit
0188e1fe7b
@@ -71,6 +71,14 @@ test_partition_table_on_host:
|
|||||||
- cd components/partition_table/test_gen_esp32part_host
|
- cd components/partition_table/test_gen_esp32part_host
|
||||||
- pytest_for_ut ./gen_esp32part_tests.py
|
- pytest_for_ut ./gen_esp32part_tests.py
|
||||||
|
|
||||||
|
test_blockdev_ioctl_overlap_checker:
|
||||||
|
extends:
|
||||||
|
- .host_test_template
|
||||||
|
- .rules:build:check
|
||||||
|
script:
|
||||||
|
- cd components/esp_blockdev/test
|
||||||
|
- pytest_for_ut ./test_check_ioctl_overlap.py
|
||||||
|
|
||||||
test_ldgen_on_host:
|
test_ldgen_on_host:
|
||||||
extends: .host_test_template
|
extends: .host_test_template
|
||||||
script:
|
script:
|
||||||
|
|||||||
@@ -1 +1,8 @@
|
|||||||
idf_component_register(INCLUDE_DIRS include)
|
idf_component_register(INCLUDE_DIRS include)
|
||||||
|
|
||||||
|
# Register this component's own ioctl definitions for overlap checking
|
||||||
|
idf_build_set_property(
|
||||||
|
ESP_BLOCKDEV_IOCTL_DEF_FILES
|
||||||
|
"${CMAKE_CURRENT_LIST_DIR}/include/esp_blockdev.h"
|
||||||
|
APPEND
|
||||||
|
)
|
||||||
|
|||||||
@@ -97,6 +97,31 @@ and auxiliary operations like getting device statistics or debugging information
|
|||||||
|
|
||||||
All the APIs are optional, so if any API function is not available for given device the corresponding pointer is set NULL.
|
All the APIs are optional, so if any API function is not available for given device the corresponding pointer is set NULL.
|
||||||
|
|
||||||
|
### Ioctl reservation overlap checking
|
||||||
|
|
||||||
|
Components that define ioctl commands can also reserve command values or inclusive ranges for build-time validation. The reservation markers are real macro invocations; comments are ignored.
|
||||||
|
|
||||||
|
Add reservation markers in a public header or other definition file:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "esp_blockdev.h"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(nand_flash, ESP_BLOCKDEV_CMD_SYSTEM_BASE + 10, ESP_BLOCKDEV_CMD_SYSTEM_BASE + 20);
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD(sdcard, ESP_BLOCKDEV_CMD_USER_BASE + 1);
|
||||||
|
```
|
||||||
|
|
||||||
|
Register each definition file with the build system:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
idf_build_set_property(
|
||||||
|
ESP_BLOCKDEV_IOCTL_DEF_FILES
|
||||||
|
"${CMAKE_CURRENT_LIST_DIR}/include/foo_ioctl_defs.h"
|
||||||
|
APPEND
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The esp_blockdev checker component preprocesses every registered file during the build and fails if any reservations overlap or are invalid.
|
||||||
|
|
||||||
### Error handling
|
### Error handling
|
||||||
|
|
||||||
BDL interface doesn't define specific requirements for return values and/or error codes of the API functions declared below. The only expectation is returning ESP_OK on successful run and any sort of ESP_ERR_* on failure (to stay compatible with ESP_ERROR_CHECK and other standard IDF error validation helpers).
|
BDL interface doesn't define specific requirements for return values and/or error codes of the API functions declared below. The only expectation is returning ESP_OK on successful run and any sort of ESP_ERR_* on failure (to stay compatible with ESP_ERROR_CHECK and other standard IDF error validation helpers).
|
||||||
|
|||||||
@@ -25,6 +25,55 @@ extern "C" {
|
|||||||
* See the in-code comments and README.md for more details.
|
* See the in-code comments and README.md for more details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Register a reserved ioctl command value or inclusive range for overlap checking
|
||||||
|
*
|
||||||
|
* These macros are machine-readable markers for the build-time checker. They do not affect
|
||||||
|
* normal builds unless @c ESP_BLOCKDEV_CHECK_CMD_OVERLAP is defined.
|
||||||
|
*
|
||||||
|
* Register reservation declarations in headers or other files that are appended to
|
||||||
|
* @c ESP_BLOCKDEV_IOCTL_DEF_FILES.
|
||||||
|
*
|
||||||
|
* @code{c}
|
||||||
|
* ESP_BLOCKDEV_RESERVE_CMD_RANGE(nand_flash, ESP_BLOCKDEV_CMD_SYSTEM_BASE + 10, ESP_BLOCKDEV_CMD_SYSTEM_BASE + 20);
|
||||||
|
* ESP_BLOCKDEV_RESERVE_CMD(sdcard, ESP_BLOCKDEV_CMD_USER_BASE + 1);
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
/** @cond */
|
||||||
|
#ifdef ESP_BLOCKDEV_CHECK_CMD_OVERLAP
|
||||||
|
|
||||||
|
#define ESP_BLOCKDEV_RESERVE_CMD_RANGE(component, start, end) \
|
||||||
|
ESP_BLOCKDEV_RESERVE_MARKER(component, (start), (end))
|
||||||
|
|
||||||
|
#define ESP_BLOCKDEV_RESERVE_CMD(component, start) \
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(component, start, start)
|
||||||
|
|
||||||
|
#else
|
||||||
|
/** @endcond */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reserve an inclusive range of ioctl command values for build-time overlap checking
|
||||||
|
*
|
||||||
|
* @param component Unquoted identifier naming the owning component
|
||||||
|
* @param start First command value in the range (0x00–0xFF)
|
||||||
|
* @param end Last command value in the range (0x00–0xFF, >= start)
|
||||||
|
*/
|
||||||
|
#define ESP_BLOCKDEV_RESERVE_CMD_RANGE(component, start, end)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reserve a single ioctl command value for build-time overlap checking
|
||||||
|
*
|
||||||
|
* Equivalent to ``ESP_BLOCKDEV_RESERVE_CMD_RANGE(component, start, start)``.
|
||||||
|
*
|
||||||
|
* @param component Unquoted identifier naming the owning component
|
||||||
|
* @param start The command value to reserve (0x00–0xFF)
|
||||||
|
*/
|
||||||
|
#define ESP_BLOCKDEV_RESERVE_CMD(component, start)
|
||||||
|
|
||||||
|
/** @cond */
|
||||||
|
#endif
|
||||||
|
/** @endcond */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @defgroup esp_blockdev_ioctl_cmds Block device ioctl commands
|
* @defgroup esp_blockdev_ioctl_cmds Block device ioctl commands
|
||||||
*
|
*
|
||||||
@@ -34,6 +83,8 @@ extern "C" {
|
|||||||
* @{
|
* @{
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* --- ESP_BLOCKDEV_CMD_ reservation section begin --- */
|
||||||
|
|
||||||
#define ESP_BLOCKDEV_CMD_SYSTEM_BASE 0x00 /*!< System commands base value */
|
#define ESP_BLOCKDEV_CMD_SYSTEM_BASE 0x00 /*!< System commands base value */
|
||||||
#define ESP_BLOCKDEV_CMD_USER_BASE 0x80 /*!< User commands base value */
|
#define ESP_BLOCKDEV_CMD_USER_BASE 0x80 /*!< User commands base value */
|
||||||
|
|
||||||
@@ -76,6 +127,13 @@ extern "C" {
|
|||||||
*/
|
*/
|
||||||
#define ESP_BLOCKDEV_CMD_ERASE_CONTENTS (ESP_BLOCKDEV_CMD_SYSTEM_BASE + 1)
|
#define ESP_BLOCKDEV_CMD_ERASE_CONTENTS (ESP_BLOCKDEV_CMD_SYSTEM_BASE + 1)
|
||||||
|
|
||||||
|
/** @cond */
|
||||||
|
/** Reserve the core esp_blockdev commands for overlap checking */
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(esp_blockdev, ESP_BLOCKDEV_CMD_SYSTEM_BASE, ESP_BLOCKDEV_CMD_SYSTEM_BASE + 1);
|
||||||
|
/** @endcond */
|
||||||
|
|
||||||
|
/* --- ESP_BLOCKDEV_CMD_ reservation section end --- */
|
||||||
|
|
||||||
/** @} */
|
/** @} */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
set(ESP_BLOCKDEV_COMPONENT_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
|
||||||
|
# Skip bootloader builds — ioctl checking is only relevant for the app.
|
||||||
|
if(BOOTLOADER_BUILD)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Define the callback function that performs the actual check.
|
||||||
|
function(__esp_blockdev_ioctl_check_post_elf target)
|
||||||
|
idf_build_get_property(python PYTHON)
|
||||||
|
idf_build_get_property(idf_path IDF_PATH)
|
||||||
|
idf_build_get_property(ioctl_def_files ESP_BLOCKDEV_IOCTL_DEF_FILES)
|
||||||
|
|
||||||
|
if(NOT ioctl_def_files)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
TARGET ${target}
|
||||||
|
POST_BUILD
|
||||||
|
COMMAND ${python}
|
||||||
|
"${ESP_BLOCKDEV_COMPONENT_DIR}/tools/check_ioctl_overlap.py"
|
||||||
|
--compiler ${CMAKE_C_COMPILER}
|
||||||
|
--include-dir "${ESP_BLOCKDEV_COMPONENT_DIR}/include"
|
||||||
|
--include-dir "${idf_path}/components/esp_common/include"
|
||||||
|
--include-dir "${CMAKE_BINARY_DIR}/config"
|
||||||
|
--files ${ioctl_def_files}
|
||||||
|
COMMENT "Checking ioctl definition overlaps"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Register with the build system. CMakev2 provides a proper build-event API;
|
||||||
|
# CMakev1 uses cmake_language(DEFER) as a fallback.
|
||||||
|
if(COMMAND idf_component_register_build_event_callback)
|
||||||
|
# cmakev2: use the official callback mechanism
|
||||||
|
idf_component_register_build_event_callback(
|
||||||
|
EVENT POST_ELF
|
||||||
|
CALLBACK __esp_blockdev_ioctl_check_post_elf
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
# cmakev1: defer until the ELF target exists
|
||||||
|
cmake_language(DEFER DIRECTORY ${CMAKE_SOURCE_DIR} CALL __esp_blockdev_ioctl_check_deferred)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
function(__esp_blockdev_ioctl_check_deferred)
|
||||||
|
set(project_elf ${CMAKE_PROJECT_NAME}.elf)
|
||||||
|
if(NOT TARGET ${project_elf})
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
__esp_blockdev_ioctl_check_post_elf(${project_elf})
|
||||||
|
endfunction()
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
COMPONENT_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
TOOLS_DIR = COMPONENT_DIR / 'tools'
|
||||||
|
ESP_BLOCKDEV_H = COMPONENT_DIR / 'include' / 'esp_blockdev.h'
|
||||||
|
ESP_COMMON_INCLUDE = COMPONENT_DIR.parent / 'esp_common' / 'include'
|
||||||
|
|
||||||
|
sys.path.insert(0, str(TOOLS_DIR))
|
||||||
|
import check_ioctl_overlap as checker # noqa: E402
|
||||||
|
|
||||||
|
COMPILER = shutil.which('gcc') or shutil.which('cc')
|
||||||
|
INCLUDE_DIRS = [str(ESP_COMMON_INCLUDE), str(ESP_BLOCKDEV_H.parent)]
|
||||||
|
|
||||||
|
# The header itself reserves [0x00..0x01] for esp_blockdev core commands.
|
||||||
|
# Tests that include esp_blockdev.h will see that reservation.
|
||||||
|
CORE_RESERVATION_END = 0x01
|
||||||
|
|
||||||
|
|
||||||
|
def run_check(contents: str) -> list[checker.Reservation]:
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
tmp_path = Path(tmp)
|
||||||
|
(tmp_path / 'sdkconfig.h').write_text('/* test stub */\n')
|
||||||
|
header = tmp_path / 'defs.h'
|
||||||
|
header.write_text(contents)
|
||||||
|
include_dirs = [tmp, *INCLUDE_DIRS]
|
||||||
|
return checker.check_files([header], compiler=COMPILER, include_dirs=include_dirs) # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
|
||||||
|
class TestIoctlOverlapChecker(unittest.TestCase):
|
||||||
|
@unittest.skipUnless(COMPILER, 'C compiler required for preprocessing')
|
||||||
|
def test_no_overlap(self) -> None:
|
||||||
|
# Use ranges that don't overlap with core [0x00..0x01]
|
||||||
|
reservations = run_check(
|
||||||
|
f'''#include "{ESP_BLOCKDEV_H}"
|
||||||
|
|
||||||
|
#define BASE 0x10
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(nand_flash, BASE + 0, BASE + 0x0F);
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(sdcard, BASE + 0x10, BASE + 0x1F);
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD(system, 0x40);
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
# 3 from this file + 1 from esp_blockdev.h core
|
||||||
|
self.assertEqual(len(reservations), 4)
|
||||||
|
system_reservations = [r for r in reservations if r.component == 'system']
|
||||||
|
self.assertEqual(len(system_reservations), 1)
|
||||||
|
self.assertEqual(system_reservations[0].start, 0x40)
|
||||||
|
self.assertEqual(system_reservations[0].end, 0x40)
|
||||||
|
|
||||||
|
@unittest.skipUnless(COMPILER, 'C compiler required for preprocessing')
|
||||||
|
def test_direct_overlap(self) -> None:
|
||||||
|
with self.assertRaises(checker.ReservationError) as ctx:
|
||||||
|
run_check(
|
||||||
|
f'''#include "{ESP_BLOCKDEV_H}"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(nand_flash, 0x10, 0x20);
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(sdcard, 0x1F, 0x30);
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
msg = str(ctx.exception)
|
||||||
|
self.assertIn('ioctl reservation overlap:', msg)
|
||||||
|
self.assertIn('nand_flash', msg)
|
||||||
|
self.assertIn('sdcard', msg)
|
||||||
|
|
||||||
|
@unittest.skipUnless(COMPILER, 'C compiler required for preprocessing')
|
||||||
|
def test_adjacent_non_overlap(self) -> None:
|
||||||
|
reservations = run_check(
|
||||||
|
f'''#include "{ESP_BLOCKDEV_H}"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(a, 0x10, 0x1F);
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(b, 0x20, 0x2F);
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
# 2 from this file + 1 core
|
||||||
|
self.assertEqual(len(reservations), 3)
|
||||||
|
|
||||||
|
@unittest.skipUnless(COMPILER, 'C compiler required for preprocessing')
|
||||||
|
def test_single_command_reservation(self) -> None:
|
||||||
|
reservations = run_check(
|
||||||
|
f'''#include "{ESP_BLOCKDEV_H}"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD(single_cmd, 0x42);
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(other, 0x43, 0x44);
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
single = [r for r in reservations if r.component == 'single_cmd']
|
||||||
|
self.assertEqual(len(single), 1)
|
||||||
|
self.assertEqual(single[0].start, 0x42)
|
||||||
|
self.assertEqual(single[0].end, 0x42)
|
||||||
|
|
||||||
|
@unittest.skipUnless(COMPILER, 'C compiler required for preprocessing')
|
||||||
|
def test_reversed_range(self) -> None:
|
||||||
|
with self.assertRaises(checker.ReservationError) as ctx:
|
||||||
|
run_check(
|
||||||
|
f'''#include "{ESP_BLOCKDEV_H}"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(reversed, 0x20, 0x10);
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
self.assertIn('start 0x20 > end 0x10', str(ctx.exception))
|
||||||
|
|
||||||
|
@unittest.skipUnless(COMPILER, 'C compiler required for preprocessing')
|
||||||
|
def test_out_of_range_values(self) -> None:
|
||||||
|
with self.assertRaises(checker.ReservationError) as ctx:
|
||||||
|
run_check(
|
||||||
|
f'''#include "{ESP_BLOCKDEV_H}"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(big, 0x100, 0x101);
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
self.assertIn('out of range', str(ctx.exception))
|
||||||
|
|
||||||
|
@unittest.skipUnless(COMPILER, 'C compiler required for preprocessing')
|
||||||
|
def test_macro_expanded_constants(self) -> None:
|
||||||
|
reservations = run_check(
|
||||||
|
f'''#include "{ESP_BLOCKDEV_H}"
|
||||||
|
|
||||||
|
#define BASE ESP_BLOCKDEV_CMD_SYSTEM_BASE
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(range_a, BASE + 10, BASE + 20);
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD(single, BASE + 2);
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
# These overlap with core [0x00..0x01], but single=2 is adjacent (OK),
|
||||||
|
# range_a=[10..20] is non-overlapping with core. However single=2 > core end=1, OK.
|
||||||
|
range_a = [r for r in reservations if r.component == 'range_a']
|
||||||
|
single = [r for r in reservations if r.component == 'single']
|
||||||
|
self.assertEqual(len(range_a), 1)
|
||||||
|
self.assertEqual(range_a[0].start, 10)
|
||||||
|
self.assertEqual(range_a[0].end, 20)
|
||||||
|
self.assertEqual(len(single), 1)
|
||||||
|
self.assertEqual(single[0].start, 2)
|
||||||
|
self.assertEqual(single[0].end, 2)
|
||||||
|
|
||||||
|
@unittest.skipUnless(COMPILER, 'C compiler required for preprocessing')
|
||||||
|
def test_overlap_with_core_commands(self) -> None:
|
||||||
|
"""Reserving a range that overlaps with core [0x00..0x01] must fail."""
|
||||||
|
with self.assertRaises(checker.ReservationError) as ctx:
|
||||||
|
run_check(
|
||||||
|
f'''#include "{ESP_BLOCKDEV_H}"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(my_comp, 0x00, 0x05);
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
msg = str(ctx.exception)
|
||||||
|
self.assertIn('esp_blockdev', msg)
|
||||||
|
self.assertIn('my_comp', msg)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
#!/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())
|
||||||
@@ -155,6 +155,76 @@ Validation
|
|||||||
|
|
||||||
Implementations should include tests that cover alignment checks, flag-driven behaviour (read-only, erase-before-write, NAND-style writes), and correct propagation of errors through stacked devices. Middleware that wraps lower handles must also verify that handle lifetime management remains consistent across the stack.
|
Implementations should include tests that cover alignment checks, flag-driven behaviour (read-only, erase-before-write, NAND-style writes), and correct propagation of errors through stacked devices. Middleware that wraps lower handles must also verify that handle lifetime management remains consistent across the stack.
|
||||||
|
|
||||||
|
.. _blockdev-ioctl-management:
|
||||||
|
|
||||||
|
Ioctl Command Management
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
The ioctl command space (``0x00``–``0xFF``) is shared across all components in an ESP-IDF project. To prevent silent collisions at integration time, the Block Device Layer provides a build-time overlap checker that validates command reservations across the entire component tree.
|
||||||
|
|
||||||
|
Command Ranges
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
============= ==================================== ==============================
|
||||||
|
Range Base macro Purpose
|
||||||
|
============= ==================================== ==============================
|
||||||
|
``0x00–0x7F`` ``ESP_BLOCKDEV_CMD_SYSTEM_BASE`` ESP-IDF system commands
|
||||||
|
``0x80–0xFF`` ``ESP_BLOCKDEV_CMD_USER_BASE`` User / component extensions
|
||||||
|
============= ==================================== ==============================
|
||||||
|
|
||||||
|
Reservation Macros
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Components declare their ioctl command allocations using the following macros (defined in ``esp_blockdev.h``):
|
||||||
|
|
||||||
|
.. code-block:: c
|
||||||
|
|
||||||
|
// Reserve an inclusive range of command values
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(component, start, end);
|
||||||
|
|
||||||
|
// Reserve exactly one command value (shorthand for start == end)
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD(component, start);
|
||||||
|
|
||||||
|
Both ``start`` and ``end`` are inclusive and must satisfy:
|
||||||
|
|
||||||
|
* ``0 <= start <= end <= 255``
|
||||||
|
|
||||||
|
Example
|
||||||
|
"""""""
|
||||||
|
|
||||||
|
.. code-block:: c
|
||||||
|
|
||||||
|
#include "esp_blockdev.h"
|
||||||
|
|
||||||
|
/* Reserve system commands 0x0A..0x14 for nand_flash */
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(nand_flash,
|
||||||
|
ESP_BLOCKDEV_CMD_SYSTEM_BASE + 10,
|
||||||
|
ESP_BLOCKDEV_CMD_SYSTEM_BASE + 20);
|
||||||
|
|
||||||
|
/* Reserve a single user command for sdcard */
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD(sdcard, ESP_BLOCKDEV_CMD_USER_BASE + 1);
|
||||||
|
|
||||||
|
Registering Definition Files
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Each component that owns ioctl definitions must register the file(s) containing reservation macros with the build system. This is done in the component's ``CMakeLists.txt``:
|
||||||
|
|
||||||
|
.. code-block:: cmake
|
||||||
|
|
||||||
|
idf_build_set_property(
|
||||||
|
ESP_BLOCKDEV_IOCTL_DEF_FILES
|
||||||
|
"${CMAKE_CURRENT_LIST_DIR}/include/my_ioctl_defs.h"
|
||||||
|
APPEND
|
||||||
|
)
|
||||||
|
|
||||||
|
The ``esp_blockdev`` component itself registers its own header, so core commands (``ESP_BLOCKDEV_CMD_MARK_DELETED``, ``ESP_BLOCKDEV_CMD_ERASE_CONTENTS``) are always checked.
|
||||||
|
|
||||||
|
When an overlap or invalid range is detected, the build fails with a diagnostic showing the conflicting component names, hex ranges, and source file paths.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Adjacent ranges are valid. For example ``0x10..0x1F`` and ``0x20..0x2F`` do **not** overlap.
|
||||||
|
|
||||||
.. _blockdev-apis:
|
.. _blockdev-apis:
|
||||||
|
|
||||||
API Reference
|
API Reference
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Build-system integration tests for the esp_blockdev ioctl overlap checker (cmakev2)."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from test_build_system_helpers import IdfPyFunc
|
||||||
|
|
||||||
|
COMP_A_DEFS_NO_OVERLAP = """\
|
||||||
|
#include "esp_blockdev.h"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(comp_a, 0x10, 0x1F);
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMP_B_DEFS_NO_OVERLAP = """\
|
||||||
|
#include "esp_blockdev.h"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(comp_b, 0x20, 0x2F);
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMP_B_DEFS_OVERLAP = """\
|
||||||
|
#include "esp_blockdev.h"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(comp_b, 0x18, 0x2F);
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPONENT_CMAKELISTS = """\
|
||||||
|
idf_component_register()
|
||||||
|
|
||||||
|
idf_build_set_property(
|
||||||
|
ESP_BLOCKDEV_IOCTL_DEF_FILES
|
||||||
|
"${{CMAKE_CURRENT_LIST_DIR}}/include/{filename}"
|
||||||
|
APPEND
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _add_component(app_path: Path, name: str, defs_content: str) -> None:
|
||||||
|
comp_dir = app_path / 'components' / name
|
||||||
|
comp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
include_dir = comp_dir / 'include'
|
||||||
|
include_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
filename = f'{name}_ioctl_defs.h'
|
||||||
|
(include_dir / filename).write_text(defs_content)
|
||||||
|
(comp_dir / 'CMakeLists.txt').write_text(COMPONENT_CMAKELISTS.format(filename=filename))
|
||||||
|
|
||||||
|
|
||||||
|
def test_ioctl_overlap_checker_passes_clean_build_v2(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
|
||||||
|
"""Build succeeds when registered ioctl ranges do not overlap (cmakev2)."""
|
||||||
|
_add_component(test_app_copy, 'comp_a', COMP_A_DEFS_NO_OVERLAP)
|
||||||
|
_add_component(test_app_copy, 'comp_b', COMP_B_DEFS_NO_OVERLAP)
|
||||||
|
|
||||||
|
ret = idf_py('build')
|
||||||
|
assert ret.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_ioctl_overlap_checker_fails_on_overlap_v2(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
|
||||||
|
"""Build fails when registered ioctl ranges overlap (cmakev2)."""
|
||||||
|
_add_component(test_app_copy, 'comp_a', COMP_A_DEFS_NO_OVERLAP)
|
||||||
|
_add_component(test_app_copy, 'comp_b', COMP_B_DEFS_OVERLAP)
|
||||||
|
|
||||||
|
ret = idf_py('build', check=False)
|
||||||
|
assert ret.returncode != 0
|
||||||
|
output = ret.stdout + ret.stderr
|
||||||
|
assert 'ioctl reservation overlap' in output
|
||||||
|
assert 'comp_a' in output
|
||||||
|
assert 'comp_b' in output
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Build-system integration tests for the esp_blockdev ioctl overlap checker."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from test_build_system_helpers import IdfPyFunc
|
||||||
|
|
||||||
|
COMP_A_DEFS_NO_OVERLAP = """\
|
||||||
|
#include "esp_blockdev.h"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(comp_a, 0x10, 0x1F);
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMP_B_DEFS_NO_OVERLAP = """\
|
||||||
|
#include "esp_blockdev.h"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(comp_b, 0x20, 0x2F);
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMP_B_DEFS_OVERLAP = """\
|
||||||
|
#include "esp_blockdev.h"
|
||||||
|
|
||||||
|
ESP_BLOCKDEV_RESERVE_CMD_RANGE(comp_b, 0x18, 0x2F);
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPONENT_CMAKELISTS = """\
|
||||||
|
idf_component_register()
|
||||||
|
|
||||||
|
idf_build_set_property(
|
||||||
|
ESP_BLOCKDEV_IOCTL_DEF_FILES
|
||||||
|
"${{CMAKE_CURRENT_LIST_DIR}}/include/{filename}"
|
||||||
|
APPEND
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _add_component(app_path: Path, name: str, defs_content: str) -> None:
|
||||||
|
comp_dir = app_path / 'components' / name
|
||||||
|
comp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
include_dir = comp_dir / 'include'
|
||||||
|
include_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
filename = f'{name}_ioctl_defs.h'
|
||||||
|
(include_dir / filename).write_text(defs_content)
|
||||||
|
(comp_dir / 'CMakeLists.txt').write_text(COMPONENT_CMAKELISTS.format(filename=filename))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('test_app_copy')
|
||||||
|
def test_ioctl_overlap_checker_passes_clean_build(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
|
||||||
|
"""Build succeeds when registered ioctl ranges do not overlap."""
|
||||||
|
logging.info('Testing ioctl overlap checker with non-overlapping ranges')
|
||||||
|
_add_component(test_app_copy, 'comp_a', COMP_A_DEFS_NO_OVERLAP)
|
||||||
|
_add_component(test_app_copy, 'comp_b', COMP_B_DEFS_NO_OVERLAP)
|
||||||
|
|
||||||
|
ret = idf_py('build')
|
||||||
|
assert ret.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('test_app_copy')
|
||||||
|
def test_ioctl_overlap_checker_fails_on_overlap(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
|
||||||
|
"""Build fails when registered ioctl ranges overlap."""
|
||||||
|
logging.info('Testing ioctl overlap checker detects overlapping ranges')
|
||||||
|
_add_component(test_app_copy, 'comp_a', COMP_A_DEFS_NO_OVERLAP)
|
||||||
|
_add_component(test_app_copy, 'comp_b', COMP_B_DEFS_OVERLAP)
|
||||||
|
|
||||||
|
ret = idf_py('build', check=False)
|
||||||
|
assert ret.returncode != 0
|
||||||
|
output = ret.stdout + ret.stderr
|
||||||
|
assert 'ioctl reservation overlap' in output
|
||||||
|
assert 'comp_a' in output
|
||||||
|
assert 'comp_b' in output
|
||||||
Reference in New Issue
Block a user