From 3d946596e968e2bfbd4ab9f5e17d4fe0e915f335 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 17:49:02 -0500 Subject: [PATCH] Simplify PROGMEM variant to strncasecmp_P and compile it only on ESP8266 --- esphome/components/audio/audio.cpp | 4 +++- esphome/core/helpers.cpp | 16 ++++++---------- esphome/core/helpers.h | 13 +++++++------ tests/components/core/helpers_test.cpp | 25 +++++++------------------ 4 files changed, 23 insertions(+), 35 deletions(-) diff --git a/esphome/components/audio/audio.cpp b/esphome/components/audio/audio.cpp index 402e741059..1f2de37dec 100644 --- a/esphome/components/audio/audio.cpp +++ b/esphome/components/audio/audio.cpp @@ -1,6 +1,7 @@ #include "audio.h" #include "esphome/core/helpers.h" +#include "esphome/core/progmem.h" #include @@ -86,7 +87,8 @@ AudioFileType detect_audio_file_type(const char *content_type, const char *url) // Match "audio/ogg" with a codecs parameter containing "opus" // Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc. // Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis) - if (strncasecmp(content_type, "audio/ogg", 9) == 0 && str_contains_ignore_case(content_type + 9, "opus")) { + if (strncasecmp(content_type, "audio/ogg", 9) == 0 && + str_contains_ignore_case_p(content_type + 9, ESPHOME_PSTR("opus"))) { return AudioFileType::OPUS; } #endif diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index fd6eb7980b..151d14bae2 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -233,27 +233,23 @@ bool str_contains_ignore_case_fallback(const char *haystack, const char *needle) return false; } -bool str_contains_ignore_case_p(const char *haystack, const char *needle) { +#ifdef USE_ESP8266 +bool str_contains_ignore_case_p(const char *haystack, PGM_P needle) { if (haystack == nullptr || needle == nullptr) { return false; } - const auto *needle_p = reinterpret_cast(needle); - if (progmem_read_byte(needle_p) == '\0') { + const size_t needle_len = strlen_P(needle); + if (needle_len == 0) { return true; } for (const char *p = haystack; *p != '\0'; p++) { - size_t i = 0; - uint8_t n; - // Never reads past the haystack terminator: comparing '\0' against a non-'\0' needle byte fails - while ((n = progmem_read_byte(needle_p + i)) != '\0' && ::tolower(static_cast(p[i])) == ::tolower(n)) { - i++; - } - if (n == '\0') { + if (strncasecmp_P(p, needle, needle_len) == 0) { return true; } } return false; } +#endif // USE_ESP8266 // str_truncate, str_until, str_lower_case, str_upper_case, str_snake_case moved to alloc_helpers.cpp char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index ab4ad58291..978cb18bf5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -984,13 +984,10 @@ inline bool str_endswith_ignore_case(const std::string &str, const char *suffix) /// Fallback implementation for case insensitive substring comparison. bool str_contains_ignore_case_fallback(const char *haystack, const char *needle); -/// Case-insensitive check if needle string is contained in haystack (no heap allocation). -/// ESP8266 internal implementation — prefer the `str_contains_ignore_case` macro which wraps -/// needle literals with `PSTR()` automatically so they stay in flash instead of eating RAM. -/// The needle must be a PROGMEM pointer on ESP8266; reads are plain dereferences elsewhere. -bool str_contains_ignore_case_p(const char *haystack, const char *needle); - #ifdef USE_ESP8266 +/// ESP8266 internal implementation reading the needle from flash — prefer the +/// `str_contains_ignore_case` macro which wraps needle literals with PSTR() automatically. +bool str_contains_ignore_case_p(const char *haystack, PGM_P needle); /// Case-insensitive check if needle string is contained in haystack (no heap allocation). /// On ESP8266 the needle literal is wrapped with PSTR() so it stays in flash. #define str_contains_ignore_case(haystack, needle) str_contains_ignore_case_p(haystack, PSTR(needle)) @@ -1010,6 +1007,10 @@ inline bool str_contains_ignore_case(const char *haystack, const char *needle) { return strcasestr(haystack, needle) != nullptr; #endif // defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_ZEPHYR) } +/// PROGMEM is a no-op on this platform, so the flash-needle variant is the plain check. +inline bool str_contains_ignore_case_p(const char *haystack, const char *needle) { + return str_contains_ignore_case(haystack, needle); +} #endif // USE_ESP8266 // str_truncate moved to alloc_helpers.h - remove this include before 2026.11.0 diff --git a/tests/components/core/helpers_test.cpp b/tests/components/core/helpers_test.cpp index 77b8d98c78..3d143dc26d 100644 --- a/tests/components/core/helpers_test.cpp +++ b/tests/components/core/helpers_test.cpp @@ -90,6 +90,10 @@ TEST(StringContainsIgnoreCaseTest, NullPointerAlwaysFalse) { EXPECT_FALSE(str_contains_ignore_case(haystack, needle)); EXPECT_FALSE(str_contains_ignore_case("Hello World", needle)); EXPECT_FALSE(str_contains_ignore_case(haystack, "anything")); + + EXPECT_FALSE(str_contains_ignore_case_p(haystack, needle)); + EXPECT_FALSE(str_contains_ignore_case_p("Hello World", needle)); + EXPECT_FALSE(str_contains_ignore_case_p(haystack, "anything")); } TEST(StringContainsIgnoreCaseTest, EmptySearchMatches) { @@ -121,29 +125,14 @@ TEST(StringContainsIgnoreCaseTest, MiscNotMatching) { TEST(StringContainsIgnoreCaseTest, FallbackMatchesLibc) { const char *haystack = "Hello World"; - for (const char *needle : {"", "Hello", "hELLO", "Hell", "world", "Heaven", "Hello!", "d"}) { + for (const char *needle : {"", "Hello", "hELLO", "HELLO", "Hell", "world", "World", "Heaven", "Hello!", "d"}) { EXPECT_EQ(str_contains_ignore_case_fallback(haystack, needle), str_contains_ignore_case(haystack, needle)) << "needle: " << needle; + EXPECT_EQ(str_contains_ignore_case_p(haystack, needle), str_contains_ignore_case(haystack, needle)) + << "needle: " << needle; } EXPECT_EQ(str_contains_ignore_case_fallback("", ""), str_contains_ignore_case("", "")); EXPECT_EQ(str_contains_ignore_case_fallback("ab", "abc"), str_contains_ignore_case("ab", "abc")); -} - -TEST(StringContainsIgnoreCaseTest, ProgmemVariantNullPointerAlwaysFalse) { - const char *haystack = nullptr; - const char *needle = nullptr; - - EXPECT_FALSE(str_contains_ignore_case_p(haystack, needle)); - EXPECT_FALSE(str_contains_ignore_case_p("Hello World", needle)); - EXPECT_FALSE(str_contains_ignore_case_p(haystack, "anything")); -} - -TEST(StringContainsIgnoreCaseTest, ProgmemVariantMatchesFallback) { - const char *haystack = "Hello World"; - for (const char *needle : {"", "Hello", "hELLO", "HELLO", "Hell", "world", "World", "Heaven", "Hello!", "d"}) { - EXPECT_EQ(str_contains_ignore_case_p(haystack, needle), str_contains_ignore_case_fallback(haystack, needle)) - << "needle: " << needle; - } EXPECT_TRUE(str_contains_ignore_case_p("", "")); EXPECT_FALSE(str_contains_ignore_case_p("", "a")); EXPECT_FALSE(str_contains_ignore_case_p("ab", "abc"));