[esp32] Share the wrap stub helpers and trim the sscanf scanner

This commit is contained in:
J. Nick Koston
2026-09-11 07:44:14 -05:00
parent 7d8f4b60e1
commit 8aa1424ae4
6 changed files with 219 additions and 271 deletions
+40 -33
View File
@@ -2025,7 +2025,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[]
): cv.ensure_list(cv.string_strict),
cv.Optional(CONF_ENABLE_FULL_PRINTF, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_FULL_SCANF, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_FULL_SCANF): cv.boolean,
cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean,
cv.Optional(
@@ -2326,36 +2326,52 @@ async def _set_libc_picolibc_newlib_compat() -> None:
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_sscanf_stub(enable_full_scanf: bool) -> None:
"""Wrap sscanf when Bluetooth is the only reason newlib's scanf engine links.
def _libc_is_newlib() -> bool:
"""Whether the ESP-IDF build links newlib rather than picolibc.
Runs at FINAL priority so every request_bluetooth() call has happened.
bluedroid is the only sscanf caller in an ESPHome image (~13 KB of engine
for two "%02x" parses); see sscanf_stubs.cpp. Newlib only, like the printf
wrap: IDF 6.0+ switches to picolibc, where the saving is unmeasured. A
lambda that scans a float keeps the libc sscanf, since the stub has no
floating-point conversions and the wrap applies to the whole image. The
--undefined flag is needed because libsrc.a is scanned before libbt.a, so
the stub would otherwise never be pulled from the archive.
IDF 5.x defaults to newlib on every variant; IDF 6.0+ switches to picolibc
on every variant. The printf and sscanf wraps are only measured against
newlib.
"""
return not CORE.using_arduino and idf_version() < cv.Version(6, 0, 0)
def _add_wrap_stub(symbol: str, define: str) -> None:
"""Emit a linker wrap for ``symbol`` served by a stub compiled under ``define``.
The --undefined flag is needed because libsrc.a is scanned before the IDF
libraries that reference the symbol, so the stub would otherwise never be
pulled from the archive.
"""
cg.add_define(define)
cg.add_build_flag(f"-Wl,--wrap={symbol}")
cg.add_build_flag(f"-Wl,--undefined=__wrap_{symbol}")
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_sscanf_stub(enable_full_scanf: bool | None) -> None:
"""Wrap sscanf when bluedroid is the only reason newlib's scanf engine links.
Runs at FINAL priority so every request_bluetooth() call has happened;
see sscanf_stubs.cpp for what the stub covers. A lambda that scans a float
keeps the libc sscanf unless enable_full_scanf is set explicitly, since the
stub has no floating-point conversions and the wrap applies to the whole
image.
"""
if (
enable_full_scanf
or CORE.using_arduino
or idf_version() >= cv.Version(6, 0, 0)
or not _libc_is_newlib()
or not _network_sdkconfig().bluetooth
or get_esp32_variant() in ROM_SSCANF_VARIANTS
):
return
if lambdas_use_scanf_float(CORE.config):
if enable_full_scanf is None and lambdas_use_scanf_float(CORE.config):
_LOGGER.warning(
"Lambda uses scanf with a float format specifier; "
"keeping the libc sscanf (~13KB flash)"
)
return
cg.add_define("USE_ESP32_SSCANF_STUB")
cg.add_build_flag("-Wl,--wrap=sscanf")
cg.add_build_flag("-Wl,--undefined=__wrap_sscanf")
_add_wrap_stub("sscanf", "USE_ESP32_SSCANF_STUB")
@coroutine_with_priority(CoroPriority.FINAL)
@@ -2700,12 +2716,8 @@ async def to_code(config):
# implements vsnprintf by building a string-output FILE and calling
# vfprintf, so vfprintf is unconditionally linked in by any caller
# of snprintf/vsnprintf — effectively every build — and the wrap
# saves nothing while costing ~170 B of shim. IDF 5.x defaults to
# newlib on every variant; IDF 6.0+ switches to picolibc on every
# variant.
if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF] or idf_version() >= cv.Version(
6, 0, 0
):
# saves nothing while costing ~170 B of shim.
if conf[CONF_ADVANCED][CONF_ENABLE_FULL_PRINTF] or not _libc_is_newlib():
cg.add_define("USE_FULL_PRINTF")
else:
for symbol in ("vprintf", "printf", "fprintf", "vfprintf"):
@@ -2713,14 +2725,9 @@ async def to_code(config):
# esp_http_client calls vasprintf, which on the ESP32-C6 is the only
# reference to newlib's full printf engine (~20 KB: _svfprintf_r,
# _dtoa_r and their helpers); every other caller resolves to the
# ROM. See vasprintf_stubs.cpp. The --undefined flag is needed
# because libsrc.a is scanned before the IDF libraries that
# reference the symbol, so the stub would otherwise never be pulled
# from the archive.
# ROM. See vasprintf_stubs.cpp.
if variant in ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS:
cg.add_define("USE_ESP32_VASPRINTF_STUB")
cg.add_build_flag("-Wl,--wrap=vasprintf")
cg.add_build_flag("-Wl,--undefined=__wrap_vasprintf")
_add_wrap_stub("vasprintf", "USE_ESP32_VASPRINTF_STUB")
else:
cg.add_build_flag("-DUSE_ARDUINO")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
@@ -3144,7 +3151,7 @@ async def to_code(config):
CORE.add_job(_reconcile_network_sdkconfig)
# FINAL priority: runs after every request_bluetooth() call
CORE.add_job(_add_sscanf_stub, advanced[CONF_ENABLE_FULL_SCANF])
CORE.add_job(_add_sscanf_stub, advanced.get(CONF_ENABLE_FULL_SCANF))
# FINAL priority: runs after every require_certificate_bundle() call
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
@@ -3694,5 +3701,5 @@ def process_stacktrace(config, line, backtrace_state):
# gpio.cpp only implements ESP32InternalGPIOPin and its ISR helpers, which
# are instantiated solely by the pin schema codegen (esp32_pin_to_code)
FILTER_SOURCE_FILES = filter_source_files_from_defines(
{"gpio.cpp": "USE_ESP32_INTERNAL_GPIO"}
{"gpio.cpp": "USE_ESP32_INTERNAL_GPIO", "sscanf_stubs.cpp": "USE_ESP32_SSCANF_STUB"}
)
+90 -137
View File
@@ -1,19 +1,20 @@
/*
* Linker wrap stub for sscanf() (ESP-IDF, newlib only).
* Linker wrap stub for sscanf on ESP-IDF, newlib only.
*
* Nothing in ESPHome calls sscanf(), but bluedroid does in two places:
* btc_config_get_bin() decodes stored bonding keys with "%02x" and
* string_to_bdaddr() parses "%02x:%02x:%02x:%02x:%02x:%02x". Those two
* Nothing in ESPHome calls sscanf, but bluedroid does in two places:
* btc_config_get_bin decodes stored bonding keys with "%02x" and
* string_to_bdaddr parses "%02x:%02x:%02x:%02x:%02x:%02x". Those two
* calls are the only reference to newlib's scanf engine (__ssvfscanf_r,
* _strtod_l and their helpers, ~13 KB) in a Bluetooth build on variants
* whose ROM does not export sscanf.
*
* This stub replaces sscanf() with a small scanner covering the integer,
* character, string and scanset conversions, so any other sscanf() caller
* in the image (user lambdas included) keeps working. Floating-point
* conversions are the one gap: supporting them would pull _strtod_l
* straight back, so a format using %f and friends aborts with a message
* pointing at enable_full_scanf: true.
* This stub replaces sscanf with a small scanner covering the integer,
* character, string and scanset conversions, so any other caller in the
* image (user lambdas included) keeps working. Floating-point conversions
* are the one gap: supporting them would pull _strtod_l straight back, so
* a format using %f and friends aborts with a message pointing at
* enable_full_scanf: true. Codegen already keeps the libc sscanf when a
* lambda scans a float.
*
* Only compiled in when codegen defines USE_ESP32_SSCANF_STUB, which is
* gated on a Bluetooth component being present, on the variant's ROM and
@@ -30,75 +31,59 @@
#include <cstdio>
#include "esp_system.h"
#include "esphome/core/helpers.h"
namespace esphome::esp32 {
namespace {
// Store width of an integer conversion. Every ESP32 target is ILP32, so the
// bare length, l, z and t all store 32 bits and ll and j store 64.
enum class SscanfLength : uint8_t {
SSCANF_LENGTH_NONE,
SSCANF_LENGTH_HH,
SSCANF_LENGTH_H,
SSCANF_LENGTH_L,
SSCANF_LENGTH_LL,
SSCANF_LENGTH_Z,
SSCANF_LENGTH_J,
SSCANF_LENGTH_T,
};
bool is_space(char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }
int digit_value(char c, unsigned base) {
unsigned value;
if (c >= '0' && c <= '9') {
value = c - '0';
} else if (c >= 'a' && c <= 'z') {
value = c - 'a' + 10;
} else if (c >= 'A' && c <= 'Z') {
value = c - 'A' + 10;
} else {
return -1;
}
return value < base ? static_cast<int>(value) : -1;
}
// Stores through the unsigned counterpart of each length; the bit pattern is
// what the signed conversions want as well.
// The bit pattern is what the signed conversions want as well. All object
// pointers share one representation here, so the argument is fetched once
// as void * rather than once per pointee type.
void store_int(va_list &ap, SscanfLength length, unsigned long long value) {
void *dest = va_arg(ap, void *);
switch (length) {
case SscanfLength::SSCANF_LENGTH_HH:
*va_arg(ap, unsigned char *) = static_cast<unsigned char>(value);
*static_cast<uint8_t *>(dest) = static_cast<uint8_t>(value);
break;
case SscanfLength::SSCANF_LENGTH_H:
*va_arg(ap, unsigned short *) = static_cast<unsigned short>(value);
break;
case SscanfLength::SSCANF_LENGTH_L:
*va_arg(ap, unsigned long *) = static_cast<unsigned long>(value);
*static_cast<uint16_t *>(dest) = static_cast<uint16_t>(value);
break;
case SscanfLength::SSCANF_LENGTH_LL:
case SscanfLength::SSCANF_LENGTH_J:
*va_arg(ap, unsigned long long *) = value;
break;
case SscanfLength::SSCANF_LENGTH_Z:
*va_arg(ap, size_t *) = static_cast<size_t>(value);
break;
case SscanfLength::SSCANF_LENGTH_T:
*va_arg(ap, ptrdiff_t *) = static_cast<ptrdiff_t>(value);
*static_cast<uint64_t *>(dest) = value;
break;
default:
*va_arg(ap, unsigned int *) = static_cast<unsigned int>(value);
*static_cast<uint32_t *>(dest) = static_cast<uint32_t>(value);
break;
}
}
// Parses a "[set]" directive starting just after the '[' into a 256-bit
// membership bitmap and returns the position after the closing ']'.
const char *parse_scanset(const char *f, uint8_t *bitmap, bool &negate) {
for (size_t i = 0; i < 32; i++)
bitmap[i] = 0;
negate = false;
struct Scanset {
uint8_t bits[32]{};
bool negate{false};
bool contains(char c) const {
auto u = static_cast<uint8_t>(c);
return ((this->bits[u >> 3] >> (u & 7)) & 1u) != this->negate;
}
};
// Parses a "[set]" directive starting just after the '[' and returns the
// position after the closing ']'.
const char *parse_scanset(const char *f, Scanset &set) {
if (*f == '^') {
negate = true;
set.negate = true;
f++;
}
// A ']' right after '[' or '[^' is a member, not the terminator.
@@ -112,20 +97,33 @@ const char *parse_scanset(const char *f, uint8_t *bitmap, bool &negate) {
f += 2;
}
for (unsigned c = lo; c <= hi; c++)
bitmap[c >> 3] |= static_cast<uint8_t>(1u << (c & 7));
set.bits[c >> 3] |= static_cast<uint8_t>(1u << (c & 7));
}
return *f == ']' ? f + 1 : f;
}
bool in_scanset(const uint8_t *bitmap, bool negate, char c) {
auto u = static_cast<uint8_t>(c);
return ((bitmap[u >> 3] >> (u & 7)) & 1u) != negate;
}
[[noreturn]] void unsupported_conversion() {
esp_system_abort("sscanf: unsupported conversion; set enable_full_scanf: true in esp32 framework advanced config");
}
// Radix of an integer conversion, 0 meaning "detect from the prefix" (%i).
unsigned int_base(char conv) {
switch (conv) {
case 'd':
case 'u':
return 10;
case 'i':
return 0;
case 'o':
return 8;
case 'x':
case 'X':
return 16;
default:
unsupported_conversion();
}
}
int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
const char *in = str;
int assigned = 0;
@@ -136,7 +134,15 @@ int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
in++;
continue;
}
if (*f != '%') {
bool literal = *f != '%';
if (!literal && f[1] == '%') {
// "%%" matches one '%' after skipping input whitespace
f++;
while (is_space(*in))
in++;
literal = true;
}
if (literal) {
if (*in != *f) {
input_failure = *in == '\0';
break;
@@ -145,16 +151,6 @@ int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
continue;
}
f++;
if (*f == '%') {
while (is_space(*in))
in++;
if (*in != '%') {
input_failure = *in == '\0';
break;
}
in++;
continue;
}
bool suppress = false;
if (*f == '*') {
suppress = true;
@@ -168,6 +164,7 @@ int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
if (width == 0)
width = SIZE_MAX;
auto length = SscanfLength::SSCANF_LENGTH_NONE;
bool has_length = true;
switch (*f) {
case 'h':
f++;
@@ -183,26 +180,18 @@ int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
if (*f == 'l') {
length = SscanfLength::SSCANF_LENGTH_LL;
f++;
} else {
length = SscanfLength::SSCANF_LENGTH_L;
}
break;
case 'j':
length = SscanfLength::SSCANF_LENGTH_J;
length = SscanfLength::SSCANF_LENGTH_LL;
f++;
break;
case 'z':
length = SscanfLength::SSCANF_LENGTH_Z;
f++;
break;
case 't':
length = SscanfLength::SSCANF_LENGTH_T;
f++;
break;
case 'L':
f++;
break;
default:
has_length = false;
break;
}
const char conv = *f;
@@ -213,7 +202,9 @@ int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
store_int(ap, length, static_cast<unsigned long long>(in - str));
continue;
}
const bool wide = length == SscanfLength::SSCANF_LENGTH_L || length == SscanfLength::SSCANF_LENGTH_LL;
const bool string_conv = conv == 'c' || conv == 's' || conv == '[';
if (string_conv && has_length)
unsupported_conversion(); // wide characters
if (conv != 'c' && conv != '[') {
while (is_space(*in))
in++;
@@ -222,43 +213,18 @@ int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
input_failure = true;
break;
}
unsigned base;
switch (conv) {
case 'd':
case 'u':
base = 10;
break;
case 'i':
base = 0;
break;
case 'o':
base = 8;
break;
case 'x':
case 'X':
base = 16;
break;
case 'c':
case 's':
case '[':
if (wide)
unsupported_conversion();
base = 1;
break;
default:
unsupported_conversion();
}
if (base != 1) {
if (!string_conv) {
unsigned base = int_base(conv);
const char *p = in;
size_t n = 0;
bool negative = false;
if ((*p == '+' || *p == '-') && n < width) {
if (*p == '+' || *p == '-') {
negative = *p == '-';
p++;
n++;
}
if ((base == 0 || base == 16) && p[0] == '0' && (p[1] == 'x' || p[1] == 'X') && n + 2 < width &&
digit_value(p[2], 16) >= 0) {
parse_hex_char(p[2]) != INVALID_HEX_CHAR) {
p += 2;
n += 2;
base = 16;
@@ -267,13 +233,8 @@ int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
base = *p == '0' ? 8 : 10;
unsigned long long value = 0;
bool any_digit = false;
while (n < width) {
int digit = digit_value(*p, base);
if (digit < 0)
break;
value = value * base + static_cast<unsigned>(digit);
p++;
n++;
for (uint8_t digit; n < width && (digit = parse_hex_char(*p)) < base; p++, n++) {
value = value * base + digit;
any_digit = true;
}
if (!any_digit)
@@ -286,33 +247,25 @@ int vsscanf_stub(const char *str, const char *fmt, va_list ap) {
continue;
}
char *dest = suppress ? nullptr : va_arg(ap, char *);
if (conv == 'c') {
// Like newlib, a short read still assigns whatever was available.
const size_t count = width == SIZE_MAX ? 1 : width;
size_t n = 0;
while (n < count && in[n] != '\0') {
if (dest != nullptr)
dest[n] = in[n];
n++;
}
in += n;
} else {
uint8_t bitmap[32];
bool negate = false;
if (conv == '[')
f = parse_scanset(f + 1, bitmap, negate) - 1;
size_t n = 0;
while (n < width && in[n] != '\0' && (conv == '[' ? in_scanset(bitmap, negate, in[n]) : !is_space(in[n]))) {
if (dest != nullptr)
dest[n] = in[n];
n++;
}
Scanset set;
if (conv == '[')
f = parse_scanset(f + 1, set) - 1; // the loop's f++ steps past the ']'
// %c reads exactly width characters (default 1); like newlib, a short
// read still assigns whatever was available.
const size_t limit = conv == 'c' && width == SIZE_MAX ? 1 : width;
size_t n = 0;
while (n < limit && in[n] != '\0' && (conv == 'c' || (conv == '[' ? set.contains(in[n]) : !is_space(in[n])))) {
if (dest != nullptr)
dest[n] = in[n];
n++;
}
if (conv != 'c') {
if (n == 0)
break;
if (dest != nullptr)
dest[n] = '\0';
in += n;
}
in += n;
if (!suppress)
assigned++;
}
-2
View File
@@ -1055,8 +1055,6 @@ def lint_no_std_to_string(fname, match):
# [^\w] ensures we match function calls, not substrings
r"[^\w]((?:std::)?v?[fs]?scanf)\s*\(" + CPP_RE_EOL,
include=cpp_include,
# The linker wrap that keeps the scanf engine out of Bluetooth builds
exclude=["esphome/components/esp32/sscanf_stubs.cpp"],
)
def lint_no_scanf(fname, match):
func = match.group(1)
+29 -40
View File
@@ -1416,54 +1416,43 @@ def test_mbedtls_tls_openthread_requires_server_and_extras(
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] == _OPENTHREAD_EXTRAS
_VASPRINTF_STUB_FLAGS = {"-Wl,--wrap=vasprintf", "-Wl,--undefined=__wrap_vasprintf"}
@pytest.mark.parametrize(
("config_file", "expected"),
("config_file", "symbol", "expected"),
[
pytest.param("vasprintf_stub_c6.yaml", True, id="c6"),
pytest.param("vasprintf_stub_c6_full_printf.yaml", False, id="c6_full_printf"),
pytest.param("exclusion_reincludes.yaml", False, id="esp32"),
# vasprintf: only where the ROM lacks vasprintf but has vsnprintf
pytest.param("vasprintf_stub_c6.yaml", "vasprintf", True, id="vasprintf_c6"),
pytest.param(
"vasprintf_stub_c6_full_printf.yaml",
"vasprintf",
False,
id="vasprintf_c6_full_printf",
),
pytest.param(
"exclusion_reincludes.yaml", "vasprintf", False, id="vasprintf_esp32"
),
# sscanf: only for Bluetooth builds on variants whose ROM lacks sscanf,
# and not when a lambda scans a float
pytest.param("sscanf_stub_ble.yaml", "sscanf", True, id="sscanf_esp32_ble"),
pytest.param("sscanf_stub_ble_c6.yaml", "sscanf", False, id="sscanf_c6_rom"),
pytest.param(
"sscanf_stub_ble_full_scanf.yaml", "sscanf", False, id="sscanf_full_scanf"
),
pytest.param(
"sscanf_stub_ble_lambda.yaml", "sscanf", False, id="sscanf_lambda_float"
),
pytest.param("exclusion_reincludes.yaml", "sscanf", False, id="sscanf_no_ble"),
],
)
def test_vasprintf_stub_only_on_rom_vsnprintf_variants(
def test_wrap_stub_emission(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
symbol: str,
expected: bool,
) -> None:
"""The vasprintf wrap is emitted only where the ROM lacks vasprintf but has vsnprintf."""
"""A linker wrap stub is emitted as a define plus --wrap/--undefined flag pair."""
generate_main(component_config_path(config_file))
assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected
flags = {f"-Wl,--wrap={symbol}", f"-Wl,--undefined=__wrap_{symbol}"}
assert (CORE.build_flags >= flags) is expected
defines = {define.name for define in CORE.defines}
assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected
_SSCANF_STUB_FLAGS = {"-Wl,--wrap=sscanf", "-Wl,--undefined=__wrap_sscanf"}
@pytest.mark.parametrize(
("config_file", "expected"),
[
pytest.param("sscanf_stub_ble.yaml", True, id="esp32_ble"),
pytest.param("sscanf_stub_ble_c6.yaml", False, id="c6_ble_rom_sscanf"),
pytest.param("sscanf_stub_ble_full_scanf.yaml", False, id="full_scanf"),
pytest.param("sscanf_stub_ble_lambda.yaml", False, id="lambda_calls_sscanf"),
pytest.param("exclusion_reincludes.yaml", False, id="esp32_no_ble"),
],
)
def test_sscanf_stub_only_for_bluetooth_without_rom_sscanf(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
expected: bool,
) -> None:
"""The sscanf wrap is emitted only for Bluetooth builds on variants whose ROM lacks sscanf.
User code that calls a scanf function opts the build out automatically.
"""
generate_main(component_config_path(config_file))
assert (CORE.build_flags >= _SSCANF_STUB_FLAGS) is expected
defines = {define.name for define in CORE.defines}
assert ("USE_ESP32_SSCANF_STUB" in defines) is expected
assert (f"USE_ESP32_{symbol.upper()}_STUB" in defines) is expected
+2 -58
View File
@@ -8,64 +8,8 @@ from unittest.mock import MagicMock, patch
import pytest
from esphome.components import esp8266
from esphome.components.esp8266 import check_rosetta, lambdas_use_scanf_float
from esphome.core import EsphomeError, Lambda
from esphome.types import ConfigType
@pytest.mark.parametrize(
("src", "expected"),
[
# Basic float formats
('sscanf(buf, "%f", &v)', True),
('sscanf(buf, "%F", &v)', True),
('sscanf(buf, "%e", &v)', True),
('sscanf(buf, "%E", &v)', True),
('sscanf(buf, "%g", &v)', True),
('sscanf(buf, "%G", &v)', True),
('sscanf(buf, "%a", &v)', True),
('sscanf(buf, "%A", &v)', True),
# With modifiers
('sscanf(buf, "%lf", &v)', True),
('sscanf(buf, "%Lf", &v)', True),
('sscanf(buf, "%8lf", &v)', True),
('sscanf(buf, "%*f")', True),
('sscanf(buf, "%.2f", &v)', True),
# Mixed formats
('sscanf(buf, "%d,%f", &a, &b)', True),
# fscanf and std::sscanf
('fscanf(fp, "%f", &v)', True),
('std::sscanf(buf, "%f", &v)', True),
# Multi-line
('sscanf(buf,\n"%f", &v)', True),
# No float format
('sscanf(buf, "%d", &v)', False),
('sscanf(buf, "%s", s)', False),
# printf not scanf
('printf("%f", val)', False),
# %f in a different statement after scanf
('sscanf(buf, "%d", &x); printf("%f", val);', False),
# scanf %f in comment only
('// sscanf(buf, "%f", &v)\nsscanf(buf, "%d", &x)', False),
('/* sscanf(buf, "%f") */\nsscanf(buf, "%d", &x)', False),
],
)
def test_lambdas_use_scanf_float(src: str, expected: bool) -> None:
"""Test scanf float detection in lambda source."""
config: ConfigType = {"test": [Lambda(src)]}
assert lambdas_use_scanf_float(config) is expected
def test_lambdas_use_scanf_float_no_lambdas() -> None:
"""Test with config containing no lambdas."""
config: ConfigType = {"key": "value", "list": [1, 2]}
assert lambdas_use_scanf_float(config) is False
def test_lambdas_use_scanf_float_nested() -> None:
"""Test detection in deeply nested config."""
config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}}
assert lambdas_use_scanf_float(config) is True
from esphome.components.esp8266 import check_rosetta
from esphome.core import EsphomeError
@pytest.fixture
+58 -1
View File
@@ -10,6 +10,7 @@ from esphome.config_helpers import (
filter_source_files_from_platform,
frameworks_for_platforms,
get_logger_level,
lambdas_use_scanf_float,
)
from esphome.const import (
CONF_LEVEL,
@@ -19,7 +20,8 @@ from esphome.const import (
KEY_TARGET_PLATFORM,
PlatformFramework,
)
from esphome.core import Define
from esphome.core import Define, Lambda
from esphome.types import ConfigType
def test_filter_source_files_from_platform_esp32() -> None:
@@ -172,3 +174,58 @@ def test_filter_source_files_from_defines() -> None:
mock_core.defines = set()
assert sorted(filter_func()) == ["automation.cpp", "filter.cpp"]
@pytest.mark.parametrize(
("src", "expected"),
[
# Basic float formats
('sscanf(buf, "%f", &v)', True),
('sscanf(buf, "%F", &v)', True),
('sscanf(buf, "%e", &v)', True),
('sscanf(buf, "%E", &v)', True),
('sscanf(buf, "%g", &v)', True),
('sscanf(buf, "%G", &v)', True),
('sscanf(buf, "%a", &v)', True),
('sscanf(buf, "%A", &v)', True),
# With modifiers
('sscanf(buf, "%lf", &v)', True),
('sscanf(buf, "%Lf", &v)', True),
('sscanf(buf, "%8lf", &v)', True),
('sscanf(buf, "%*f")', True),
('sscanf(buf, "%.2f", &v)', True),
# Mixed formats
('sscanf(buf, "%d,%f", &a, &b)', True),
# fscanf and std::sscanf
('fscanf(fp, "%f", &v)', True),
('std::sscanf(buf, "%f", &v)', True),
# Multi-line
('sscanf(buf,\n"%f", &v)', True),
# No float format
('sscanf(buf, "%d", &v)', False),
('sscanf(buf, "%s", s)', False),
# printf not scanf
('printf("%f", val)', False),
# %f in a different statement after scanf
('sscanf(buf, "%d", &x); printf("%f", val);', False),
# scanf %f in comment only
('// sscanf(buf, "%f", &v)\nsscanf(buf, "%d", &x)', False),
('/* sscanf(buf, "%f") */\nsscanf(buf, "%d", &x)', False),
],
)
def test_lambdas_use_scanf_float(src: str, expected: bool) -> None:
"""Test scanf float detection in lambda source."""
config: ConfigType = {"test": [Lambda(src)]}
assert lambdas_use_scanf_float(config) is expected
def test_lambdas_use_scanf_float_no_lambdas() -> None:
"""Test with config containing no lambdas."""
config: ConfigType = {"key": "value", "list": [1, 2]}
assert lambdas_use_scanf_float(config) is False
def test_lambdas_use_scanf_float_nested() -> None:
"""Test detection in deeply nested config."""
config: ConfigType = {"a": {"b": {"c": [Lambda('sscanf(buf, "%f", &v)')]}}}
assert lambdas_use_scanf_float(config) is True