From fbd7e43d5d26e6463f925bc12e4494a6c9dcbe3e Mon Sep 17 00:00:00 2001 From: Aleksandr Zharov Date: Fri, 16 Oct 2026 17:01:26 +0500 Subject: [PATCH 1/3] [Rules] Fix endless loop on unknown {...} in rules on ESP8266 In parse_string_commands() the branch without FEATURE_EXTENDED_STRING_FUNCTIONS masked '{' and '}' in the whole line instead of in replacement (regression from 4e191fc5b). The following line.replace() put the unmasked braces back, so get_next_inner_bracket() found them again and the loop never ended (SW watchdog / exception reset). Mask replacement, as before 4e191fc5b. Fixes #5653 Co-Authored-By: Claude Opus 5.5 --- src/src/ESPEasyCore/ESPEasyRules.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/src/ESPEasyCore/ESPEasyRules.cpp b/src/src/ESPEasyCore/ESPEasyRules.cpp index 1621ff53c..6c539e3c8 100644 --- a/src/src/ESPEasyCore/ESPEasyRules.cpp +++ b/src/src/ESPEasyCore/ESPEasyRules.cpp @@ -884,8 +884,8 @@ void parse_string_commands(String& line) { #if FEATURE_EXTENDED_STRING_FUNCTIONS hideMasked(replacement, mustReplaceMaskedChars); // apply #else // if FEATURE_EXTENDED_STRING_FUNCTIONS - line.replace('{', static_cast(0x02)); - line.replace('}', static_cast(0x03)); + replacement.replace('{', static_cast(0x02)); + replacement.replace('}', static_cast(0x03)); #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS } From 0cf0b0df25a8159f0e6d2f18c7a7edbe942efc10 Mon Sep 17 00:00:00 2001 From: Aleksandr Zharov Date: Fri, 16 Oct 2026 19:34:33 +0500 Subject: [PATCH 2/3] [Rules] Use the escape/mask helpers also without FEATURE_EXTENDED_STRING_FUNCTIONS parse_string_commands() had #else copies of hideEscaped(), hideMasked() and restoreMaskedAndEscaped() for builds without FEATURE_EXTENDED_STRING_FUNCTIONS, and the previous commit fixed a bug in one of those copies. Build the helpers always and keep only the escaped colon (\:) handling under FEATURE_EXTENDED_STRING_FUNCTIONS, the same as in stripEscapeCharacters() / addEscapeCharacters(). Co-Authored-By: Claude Opus 5.5 --- src/src/ESPEasyCore/ESPEasyRules.cpp | 62 ++-------------------------- 1 file changed, 4 insertions(+), 58 deletions(-) diff --git a/src/src/ESPEasyCore/ESPEasyRules.cpp b/src/src/ESPEasyCore/ESPEasyRules.cpp index 6c539e3c8..13f26f84c 100644 --- a/src/src/ESPEasyCore/ESPEasyRules.cpp +++ b/src/src/ESPEasyCore/ESPEasyRules.cpp @@ -507,7 +507,6 @@ enum class string_commands_e { }; - #if FEATURE_EXTENDED_STRING_FUNCTIONS void hideEscaped(String &line, bool &mustReplaceEscapedBracket, bool &mustReplaceEscapedCurlyBracket, bool &mustReplaceEscapedColon) { String MaskEscapedBracket; @@ -531,6 +530,7 @@ void hideEscaped(String &line, bool &mustReplaceEscapedBracket, bool &mustReplac mustReplaceEscapedCurlyBracket = true; } + #if FEATURE_EXTENDED_STRING_FUNCTIONS if (hasEscapedCharacter(line, ':')) { // replace the \: with another character to mask the escaped colon so we can continue parsing. // We have to unmask then after we're finished. @@ -538,6 +538,7 @@ void hideEscaped(String &line, bool &mustReplaceEscapedBracket, bool &mustReplac line.replace(F("\\:"), MaskEscapedBracket); mustReplaceEscapedColon = true; } + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS } void hideMasked(String &line, bool mustReplaceMaskedChars) { @@ -575,14 +576,15 @@ void restoreMaskedAndEscaped(String &line, bool mustReplaceMaskedChars, bool mus line.replace(MaskEscapedBracket, F("\\}")); } + #if FEATURE_EXTENDED_STRING_FUNCTIONS if (mustReplaceEscapedColon) { // We now have to check if we did mask some escaped colon and unmask them. // Let's hope we don't mess up any Unicode here. MaskEscapedBracket = static_cast(0x15); // ASCII 0x15 = NAK line.replace(MaskEscapedBracket, F("\\:")); } + #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS } -#endif // if FEATURE_EXTENDED_STRING_FUNCTIONS void parse_string_commands(String& line) { unsigned int startIndex = 0; @@ -604,33 +606,9 @@ void parse_string_commands(String& line) { bool mustReplaceMaskedChars = false; bool mustReplaceEscapedBracket = false; bool mustReplaceEscapedCurlyBracket = false; - String MaskEscapedBracket; - - #if FEATURE_EXTENDED_STRING_FUNCTIONS bool mustReplaceEscapedColon = false; hideEscaped(line, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); - #else // if FEATURE_EXTENDED_STRING_FUNCTIONS - if (hasEscapedCharacter(line,'(') || hasEscapedCharacter(line,')')) { - // replace the \( and \) with other characters to mask the escaped brackets so we can continue parsing. - // We have to unmask then after we're finished. - MaskEscapedBracket = static_cast(0x11); // ASCII 0x11 = Device control 1 - line.replace(F("\\("), MaskEscapedBracket); - MaskEscapedBracket = static_cast(0x12); // ASCII 0x12 = Device control 2 - line.replace(F("\\)"), MaskEscapedBracket); - mustReplaceEscapedBracket = true; - } - - if (hasEscapedCharacter(line,'{') || hasEscapedCharacter(line,'}')) { - // replace the \{ and \} with other characters to mask the escaped curly brackets so we can continue parsing. - // We have to unmask then after we're finished. - MaskEscapedBracket = static_cast(0x13); // ASCII 0x13 = Device control 3 - line.replace(F("\\{"), MaskEscapedBracket); - MaskEscapedBracket = static_cast(0x14); // ASCII 0x14 = Device control 4 - line.replace(F("\\}"), MaskEscapedBracket); - mustReplaceEscapedCurlyBracket = true; - } - #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS while (get_next_inner_bracket(line, startIndex, closingIndex, '}')) { // Command without opening and closing brackets. @@ -881,12 +859,7 @@ void parse_string_commands(String& line) { // See: https://github.com/letscontrolit/ESPEasy/issues/2932#issuecomment-596139096 replacement = line.substring(startIndex, closingIndex + 1); mustReplaceMaskedChars = true; - #if FEATURE_EXTENDED_STRING_FUNCTIONS hideMasked(replacement, mustReplaceMaskedChars); // apply - #else // if FEATURE_EXTENDED_STRING_FUNCTIONS - replacement.replace('{', static_cast(0x02)); - replacement.replace('}', static_cast(0x03)); - #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS } // Replace the full command including opening and closing brackets. @@ -907,34 +880,7 @@ void parse_string_commands(String& line) { } #endif // if FEATURE_JSON_PARSE && FEATURE_EXTENDED_STRING_FUNCTIONS - #if FEATURE_EXTENDED_STRING_FUNCTIONS restoreMaskedAndEscaped(line, mustReplaceMaskedChars, mustReplaceEscapedBracket, mustReplaceEscapedCurlyBracket, mustReplaceEscapedColon); - #else // if FEATURE_EXTENDED_STRING_FUNCTIONS - if (mustReplaceMaskedChars) { - // We now have to check if we did mask some parts and unmask them. - // Let's hope we don't mess up any Unicode here. - line.replace(static_cast(0x02), '{'); - line.replace(static_cast(0x03), '}'); - } - - if (mustReplaceEscapedBracket) { - // We now have to check if we did mask some escaped bracket and unmask them. - // Let's hope we don't mess up any Unicode here. - MaskEscapedBracket = static_cast(0x11); // ASCII 0x11 = Device control 1 - line.replace(MaskEscapedBracket, F("\\(")); - MaskEscapedBracket = static_cast(0x12); // ASCII 0x12 = Device control 2 - line.replace(MaskEscapedBracket, F("\\)")); - } - - if (mustReplaceEscapedCurlyBracket) { - // We now have to check if we did mask some escaped curly bracket and unmask them. - // Let's hope we don't mess up any Unicode here. - MaskEscapedBracket = static_cast(0x13); // ASCII 0x13 = Device control 3 - line.replace(MaskEscapedBracket, F("\\{")); - MaskEscapedBracket = static_cast(0x14); // ASCII 0x14 = Device control 4 - line.replace(MaskEscapedBracket, F("\\}")); - } - #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS } void substitute_eventvalue(String& line, const String& event) { From f23953d428979ceaccbec7117d64c6bee775a0e2 Mon Sep 17 00:00:00 2001 From: Aleksandr Zharov Date: Fri, 16 Oct 2026 20:21:12 +0500 Subject: [PATCH 3/3] [Rules] Make the escape/mask helpers static hideEscaped(), hideMasked() and restoreMaskedAndEscaped() are only used in ESPEasyRules.cpp. With internal linkage GCC inlines them where they are called once (builds without FEATURE_EXTENDED_STRING_FUNCTIONS), so the ESP8266 build is the same size as before the previous commit. Co-Authored-By: Claude Opus 5.5 --- src/src/ESPEasyCore/ESPEasyRules.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/src/ESPEasyCore/ESPEasyRules.cpp b/src/src/ESPEasyCore/ESPEasyRules.cpp index 13f26f84c..fb8e64073 100644 --- a/src/src/ESPEasyCore/ESPEasyRules.cpp +++ b/src/src/ESPEasyCore/ESPEasyRules.cpp @@ -507,7 +507,7 @@ enum class string_commands_e { }; -void hideEscaped(String &line, bool &mustReplaceEscapedBracket, bool &mustReplaceEscapedCurlyBracket, bool &mustReplaceEscapedColon) { +static void hideEscaped(String &line, bool &mustReplaceEscapedBracket, bool &mustReplaceEscapedCurlyBracket, bool &mustReplaceEscapedColon) { String MaskEscapedBracket; if (hasEscapedCharacter(line,'(') || hasEscapedCharacter(line,')')) { @@ -541,14 +541,14 @@ void hideEscaped(String &line, bool &mustReplaceEscapedBracket, bool &mustReplac #endif // if FEATURE_EXTENDED_STRING_FUNCTIONS } -void hideMasked(String &line, bool mustReplaceMaskedChars) { +static void hideMasked(String &line, bool mustReplaceMaskedChars) { if (mustReplaceMaskedChars) { line.replace('{', static_cast(0x02)); line.replace('}', static_cast(0x03)); } } -void restoreMaskedAndEscaped(String &line, bool mustReplaceMaskedChars, bool mustReplaceEscapedBracket, bool mustReplaceEscapedCurlyBracket, bool mustReplaceEscapedColon) { +static void restoreMaskedAndEscaped(String &line, bool mustReplaceMaskedChars, bool mustReplaceEscapedBracket, bool mustReplaceEscapedCurlyBracket, bool mustReplaceEscapedColon) { String MaskEscapedBracket; if (mustReplaceMaskedChars) {