mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[esp32] Share the wrap stub helpers and trim the sscanf scanner
This commit is contained in:
@@ -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"}
|
||||
)
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user