diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index fef4f1d8526..a14dd728a80 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -2,6 +2,8 @@ #include #include +#include "esphome/core/alloc_helpers.h" + namespace esphome { namespace anova { @@ -105,14 +107,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) { } case READ_TARGET_TEMPERATURE: case SET_TARGET_TEMPERATURE: { - this->target_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); + this->target_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); // NOLINT if (this->fahrenheit_) this->target_temp_ = ftoc(this->target_temp_); this->has_target_temp_ = true; break; } case READ_CURRENT_TEMPERATURE: { - this->current_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); + this->current_temp_ = parse_number(str_until(buf, '\r')).value_or(0.0f); // NOLINT if (this->fahrenheit_) this->current_temp_ = ftoc(this->current_temp_); this->has_current_temp_ = true; diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 2c74638f12c..d45208ed5df 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -22,7 +22,7 @@ void HttpRequestComponent::dump_config() { } std::string HttpContainer::get_response_header(const std::string &header_name) { - auto lower = str_lower_case(header_name); + auto lower = str_lower_case(header_name); // NOLINT for (const auto &entry : this->response_headers_) { if (entry.name == lower) { ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str()); diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index ae73983bab2..f37bf776333 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -11,6 +11,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" +#include "esphome/core/alloc_helpers.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -400,7 +401,7 @@ class HttpRequestComponent : public Component { std::vector lower; lower.reserve(collect_headers.size()); for (const auto &h : collect_headers) { - lower.push_back(str_lower_case(h)); + lower.push_back(str_lower_case(h)); // NOLINT } return this->perform(url, method, body, request_headers, lower); } @@ -415,7 +416,7 @@ class HttpRequestComponent : public Component { std::vector lower; lower.reserve(collect_headers.size()); for (const auto &h : collect_headers) { - lower.push_back(str_lower_case(h)); + lower.push_back(str_lower_case(h)); // NOLINT } return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), lower); } diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index f0dd6492852..05f9db1c069 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -161,7 +161,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->response_headers_.clear(); auto header_count = container->client_.headers(); for (int i = 0; i < header_count; i++) { - const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); + const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); // NOLINT if (should_collect_header(lower_case_collect_headers, header_name)) { std::string header_value = container->client_.header(i).c_str(); ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 60ab4d68a04..85c6e8b3c7b 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -115,7 +115,7 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, container->content_length = container->response_body_.size(); for (auto header : response.headers) { ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str()); - auto lower_name = str_lower_case(header.first); + auto lower_name = str_lower_case(header.first); // NOLINT if (should_collect_header(lower_case_collect_headers, lower_name)) { container->response_headers_.push_back({lower_name, header.second}); } diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 30f53eecdc7..3e341395a46 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -38,7 +38,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { switch (evt->event_id) { case HTTP_EVENT_ON_HEADER: { - const std::string header_name = str_lower_case(evt->header_key); + const std::string header_name = str_lower_case(evt->header_key); // NOLINT if (should_collect_header(user_data->lower_case_collect_headers, header_name)) { const std::string header_value = evt->header_value; ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str()); diff --git a/esphome/core/alloc_helpers.cpp b/esphome/core/alloc_helpers.cpp new file mode 100644 index 00000000000..11c7abe3f7b --- /dev/null +++ b/esphome/core/alloc_helpers.cpp @@ -0,0 +1,229 @@ +#include "esphome/core/alloc_helpers.h" + +#include "esphome/core/helpers.h" + +#include +#include +#include +#include +#include +#include + +namespace esphome { + +// --- String helpers --- + +std::string str_truncate(const std::string &str, size_t length) { + return str.length() > length ? str.substr(0, length) : str; +} + +std::string str_until(const char *str, char ch) { + const char *pos = strchr(str, ch); + return pos == nullptr ? std::string(str) : std::string(str, pos - str); +} +std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); } + +// wrapper around std::transform to run safely on functions from the ctype.h header +// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes +template std::string str_ctype_transform(const std::string &str) { + std::string result; + result.resize(str.length()); + std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); }); + return result; +} +std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } + +std::string str_upper_case(const std::string &str) { + std::string result; + result.resize(str.length()); + std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return std::toupper(ch); }); + return result; +} + +std::string str_snake_case(const std::string &str) { + std::string result = str; + for (char &c : result) { + c = to_snake_case_char(c); + } + return result; +} + +std::string str_sanitize(const std::string &str) { + std::string result; + result.resize(str.size()); + str_sanitize_to(&result[0], str.size() + 1, str.c_str()); + return result; +} + +std::string str_snprintf(const char *fmt, size_t len, ...) { + std::string str; + va_list args; + + str.resize(len); + va_start(args, len); + size_t out_length = vsnprintf(&str[0], len + 1, fmt, args); + va_end(args); + + if (out_length < len) + str.resize(out_length); + + return str; +} + +std::string str_sprintf(const char *fmt, ...) { + std::string str; + va_list args; + + va_start(args, fmt); + size_t length = vsnprintf(nullptr, 0, fmt, args); + va_end(args); + + str.resize(length); + va_start(args, fmt); + vsnprintf(&str[0], length + 1, fmt, args); + va_end(args); + + return str; +} + +// --- Value formatting helpers --- + +std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { + char buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(buf, value, accuracy_decimals); + return std::string(buf); +} + +// --- Base64 helpers --- + +static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +// Encode 3 input bytes to 4 base64 characters, append 'count' to ret. +static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) { + char char_array_4[4]; + char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; + char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); + char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); + char_array_4[3] = char_array_3[2] & 0x3f; + + for (int j = 0; j < count; j++) + ret += BASE64_CHARS[static_cast(char_array_4[j])]; +} + +std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } + +std::string base64_encode(const uint8_t *buf, size_t buf_len) { + std::string ret; + int i = 0; + char char_array_3[3]; + + while (buf_len--) { + char_array_3[i++] = *(buf++); + if (i == 3) { + base64_encode_triple(char_array_3, 4, ret); + i = 0; + } + } + + if (i) { + for (int j = i; j < 3; j++) + char_array_3[j] = '\0'; + + base64_encode_triple(char_array_3, i + 1, ret); + + while ((i++ < 3)) + ret += '='; + } + + return ret; +} + +std::vector base64_decode(const std::string &encoded_string) { + // Calculate maximum decoded size: every 4 base64 chars = 3 bytes + size_t max_len = ((encoded_string.size() + 3) / 4) * 3; + std::vector ret(max_len); + size_t actual_len = base64_decode(encoded_string, ret.data(), max_len); + ret.resize(actual_len); + return ret; +} + +// --- Hex/binary formatting helpers --- + +std::string format_mac_address_pretty(const uint8_t *mac) { + char buf[18]; + format_mac_addr_upper(mac, buf); + return std::string(buf); +} + +std::string format_hex(const uint8_t *data, size_t length) { + std::string ret; + ret.resize(length * 2); + format_hex_to(&ret[0], length * 2 + 1, data, length); + return ret; +} + +std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } + +// Shared implementation for uint8_t and string hex pretty formatting +static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { + if (data == nullptr || length == 0) + return ""; + std::string ret; + size_t hex_len = separator ? (length * 3 - 1) : (length * 2); + ret.resize(hex_len); + format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); + if (show_length && length > 4) + return ret + " (" + std::to_string(length) + ")"; + return ret; +} + +std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { + return format_hex_pretty_uint8(data, length, separator, show_length); +} +std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) { + return format_hex_pretty(data.data(), data.size(), separator, show_length); +} + +std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) { + if (data == nullptr || length == 0) + return ""; + std::string ret; + size_t hex_len = separator ? (length * 5 - 1) : (length * 4); + ret.resize(hex_len); + format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); + if (show_length && length > 4) + return ret + " (" + std::to_string(length) + ")"; + return ret; +} +std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) { + return format_hex_pretty(data.data(), data.size(), separator, show_length); +} +std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { + return format_hex_pretty_uint8(reinterpret_cast(data.data()), data.length(), separator, show_length); +} + +std::string format_bin(const uint8_t *data, size_t length) { + std::string result; + result.resize(length * 8); + format_bin_to(&result[0], length * 8 + 1, data, length); + return result; +} + +// --- MAC address helpers --- + +std::string get_mac_address() { + uint8_t mac[6]; + get_mac_address_raw(mac); + char buf[13]; + format_mac_addr_lower_no_sep(mac, buf); + return std::string(buf); +} + +std::string get_mac_address_pretty() { + char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + return std::string(get_mac_address_pretty_into_buffer(buf)); +} + +} // namespace esphome diff --git a/esphome/core/alloc_helpers.h b/esphome/core/alloc_helpers.h new file mode 100644 index 00000000000..fe350886b76 --- /dev/null +++ b/esphome/core/alloc_helpers.h @@ -0,0 +1,128 @@ +#pragma once + +/// @file alloc_helpers.h +/// @brief Heap-allocating helper functions. +/// +/// These functions return std::string and allocate heap memory on every call. +/// On long-running embedded devices, repeated heap allocations fragment memory +/// over time, eventually causing crashes even with free memory available. +/// +/// Prefer the stack-based alternatives documented on each function instead. +/// New code should avoid using these functions. + +#include +#include +#include +#include +#include + +namespace esphome { + +// --- String helpers (allocating) --- + +/// Truncate a string to a specific length. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_truncate(const std::string &str, size_t length); + +/// Extract the part of the string until either the first occurrence of the specified character, or the end +/// (requires str to be null-terminated). +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_until(const char *str, char ch); +/// Extract the part of the string until either the first occurrence of the specified character, or the end. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_until(const std::string &str, char ch); + +/// Convert the string to lower case. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_lower_case(const std::string &str); + +/// Convert the string to upper case. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_upper_case(const std::string &str); + +/// Convert the string to snake case (lowercase with underscores). +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +std::string str_snake_case(const std::string &str); + +/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. +/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead. +std::string str_sanitize(const std::string &str); + +/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator). +/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. +std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...); + +/// sprintf-like function returning std::string. +/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. +std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); + +// --- Hex/binary formatting helpers (allocating) --- + +/// Format the six-byte array \p mac into a MAC address string. +/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead. +std::string format_mac_address_pretty(const uint8_t mac[6]); + +/// Format the byte array \p data of length \p len in lowercased hex. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +std::string format_hex(const uint8_t *data, size_t length); + +/// Format the vector \p data in lowercased hex. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +std::string format_hex(const std::vector &data); + +/// Format a byte array in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true); + +/// Format a 16-bit word array in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true); + +/// Format a byte vector in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true); + +/// Format a 16-bit word vector in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true); + +/// Format a string's bytes in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. +std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true); + +/// Format the byte array \p data of length \p len in binary. +/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. +std::string format_bin(const uint8_t *data, size_t length); + +// --- Value formatting helpers (allocating) --- + +/// Format a float value with accuracy decimals to a string. +/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0. +__attribute__((deprecated("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0."))) +std::string +value_accuracy_to_string(float value, int8_t accuracy_decimals); + +// --- Base64 helpers (allocating) --- + +/// Encode a byte buffer to base64 string. +/// @warning Allocates heap memory. +std::string base64_encode(const uint8_t *buf, size_t buf_len); +/// Encode a byte vector to base64 string. +/// @warning Allocates heap memory. +std::string base64_encode(const std::vector &buf); + +/// Decode a base64 string to a byte vector. +/// @warning Allocates heap memory. Use base64_decode(data, len, buf, buf_len) with a pre-allocated buffer instead. +std::vector base64_decode(const std::string &encoded_string); + +// --- MAC address helpers (allocating) --- + +/// Get the device MAC address as a string, in lowercase hex notation. +/// @warning Allocates heap memory. Use get_mac_address_into_buffer() instead. +std::string get_mac_address(); + +/// Get the device MAC address as a string, in colon-separated uppercase hex notation. +/// @warning Allocates heap memory. Use get_mac_address_pretty_into_buffer() instead. +std::string get_mac_address_pretty(); + +} // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 34ecaf137f0..1d0efd01ce5 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -221,31 +221,7 @@ bool str_endswith_ignore_case(const char *str, size_t str_len, const char *suffi return strncasecmp(str + str_len - suffix_len, suffix, suffix_len) == 0; } -std::string str_truncate(const std::string &str, size_t length) { - return str.length() > length ? str.substr(0, length) : str; -} -std::string str_until(const char *str, char ch) { - const char *pos = strchr(str, ch); - return pos == nullptr ? std::string(str) : std::string(str, pos - str); -} -std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); } -// wrapper around std::transform to run safely on functions from the ctype.h header -// see https://en.cppreference.com/w/cpp/string/byte/toupper#Notes -template std::string str_ctype_transform(const std::string &str) { - std::string result; - result.resize(str.length()); - std::transform(str.begin(), str.end(), result.begin(), [](unsigned char ch) { return fn(ch); }); - return result; -} -std::string str_lower_case(const std::string &str) { return str_ctype_transform(str); } -std::string str_upper_case(const std::string &str) { return str_ctype_transform(str); } -std::string str_snake_case(const std::string &str) { - std::string result = str; - for (char &c : result) { - c = to_snake_case_char(c); - } - return result; -} +// 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) { if (buffer_size == 0) { return buffer; @@ -258,41 +234,7 @@ char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { return buffer; } -std::string str_sanitize(const std::string &str) { - std::string result; - result.resize(str.size()); - str_sanitize_to(&result[0], str.size() + 1, str.c_str()); - return result; -} -std::string str_snprintf(const char *fmt, size_t len, ...) { - std::string str; - va_list args; - - str.resize(len); - va_start(args, len); - size_t out_length = vsnprintf(&str[0], len + 1, fmt, args); - va_end(args); - - if (out_length < len) - str.resize(out_length); - - return str; -} -std::string str_sprintf(const char *fmt, ...) { - std::string str; - va_list args; - - va_start(args, fmt); - size_t length = vsnprintf(nullptr, 0, fmt, args); - va_end(args); - - str.resize(length); - va_start(args, fmt); - vsnprintf(&str[0], length + 1, fmt, args); - va_end(args); - - return str; -} +// str_sanitize, str_snprintf, str_sprintf moved to alloc_helpers.cpp // Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; @@ -341,11 +283,7 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { return chars; } -std::string format_mac_address_pretty(const uint8_t *mac) { - char buf[18]; - format_mac_addr_upper(mac, buf); - return std::string(buf); -} +// format_mac_address_pretty moved to alloc_helpers.cpp // Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase. // When separator is set, it is written unconditionally after each byte and the last @@ -398,13 +336,7 @@ char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_ return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); } -std::string format_hex(const uint8_t *data, size_t length) { - std::string ret; - ret.resize(length * 2); - format_hex_to(&ret[0], length * 2 + 1, data, length); - return ret; -} -std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } +// format_hex (std::string returning overloads) moved to alloc_helpers.cpp char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); @@ -441,43 +373,7 @@ char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint16_t *dat return buffer; } -// Shared implementation for uint8_t and string hex formatting -static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { - if (data == nullptr || length == 0) - return ""; - std::string ret; - size_t hex_len = separator ? (length * 3 - 1) : (length * 2); - ret.resize(hex_len); - format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); - if (show_length && length > 4) - return ret + " (" + std::to_string(length) + ")"; - return ret; -} - -std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) { - return format_hex_pretty_uint8(data, length, separator, show_length); -} -std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) { - return format_hex_pretty(data.data(), data.size(), separator, show_length); -} - -std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) { - if (data == nullptr || length == 0) - return ""; - std::string ret; - size_t hex_len = separator ? (length * 5 - 1) : (length * 4); - ret.resize(hex_len); - format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator); - if (show_length && length > 4) - return ret + " (" + std::to_string(length) + ")"; - return ret; -} -std::string format_hex_pretty(const std::vector &data, char separator, bool show_length) { - return format_hex_pretty(data.data(), data.size(), separator, show_length); -} -std::string format_hex_pretty(const std::string &data, char separator, bool show_length) { - return format_hex_pretty_uint8(reinterpret_cast(data.data()), data.length(), separator, show_length); -} +// format_hex_pretty (all std::string returning overloads) moved to alloc_helpers.cpp char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { if (buffer_size == 0) { @@ -500,12 +396,7 @@ char *format_bin_to(char *buffer, size_t buffer_size, const uint8_t *data, size_ return buffer; } -std::string format_bin(const uint8_t *data, size_t length) { - std::string result; - result.resize(length * 8); - format_bin_to(&result[0], length * 8 + 1, data, length); - return result; -} +// format_bin moved to alloc_helpers.cpp ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { if (on == nullptr && ESPHOME_strcasecmp_P(str, ESPHOME_PSTR("on")) == 0) @@ -537,11 +428,7 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de } } -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { - char buf[VALUE_ACCURACY_MAX_LEN]; - value_accuracy_to_buf(buf, value, accuracy_decimals); - return std::string(buf); -} +// value_accuracy_to_string moved to alloc_helpers.cpp size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals) { normalize_accuracy_decimals(value, accuracy_decimals); @@ -606,45 +493,7 @@ static inline uint8_t base64_find_char(char c) { // Check if character is valid base64 or base64url static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/') || (c == '-') || (c == '_')); } -std::string base64_encode(const std::vector &buf) { return base64_encode(buf.data(), buf.size()); } - -// Encode 3 input bytes to 4 base64 characters, append 'count' to ret. -static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) { - char char_array_4[4]; - char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; - char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); - char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); - char_array_4[3] = char_array_3[2] & 0x3f; - - for (int j = 0; j < count; j++) - ret += BASE64_CHARS[static_cast(char_array_4[j])]; -} - -std::string base64_encode(const uint8_t *buf, size_t buf_len) { - std::string ret; - int i = 0; - char char_array_3[3]; - - while (buf_len--) { - char_array_3[i++] = *(buf++); - if (i == 3) { - base64_encode_triple(char_array_3, 4, ret); - i = 0; - } - } - - if (i) { - for (int j = i; j < 3; j++) - char_array_3[j] = '\0'; - - base64_encode_triple(char_array_3, i + 1, ret); - - while ((i++ < 3)) - ret += '='; - } - - return ret; -} +// base64_encode (both overloads) moved to alloc_helpers.cpp size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len) { return base64_decode(reinterpret_cast(encoded_string.data()), encoded_string.size(), buf, buf_len); @@ -705,14 +554,7 @@ size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *b return out; } -std::vector base64_decode(const std::string &encoded_string) { - // Calculate maximum decoded size: every 4 base64 chars = 3 bytes - size_t max_len = ((encoded_string.size() + 3) / 4) * 3; - std::vector ret(max_len); - size_t actual_len = base64_decode(encoded_string, ret.data(), max_len); - ret.resize(actual_len); - return ret; -} +// base64_decode (vector-returning overload) moved to alloc_helpers.cpp /// Decode base64/base64url string directly into vector of little-endian int32 values /// @param base64 Base64 or base64url encoded string (both +/ and -_ accepted) @@ -851,18 +693,7 @@ void HighFrequencyLoopRequester::stop() { this->started_ = false; } -std::string get_mac_address() { - uint8_t mac[6]; - get_mac_address_raw(mac); - char buf[13]; - format_mac_addr_lower_no_sep(mac, buf); - return std::string(buf); -} - -std::string get_mac_address_pretty() { - char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - return std::string(get_mac_address_pretty_into_buffer(buf)); -} +// get_mac_address, get_mac_address_pretty moved to alloc_helpers.cpp void get_mac_address_into_buffer(std::span buf) { uint8_t mac[6]; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 54bc32a5a58..bb164f5034f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -21,6 +21,12 @@ #include "esphome/core/optional.h" +// Backward compatibility re-export of heap-allocating helpers. +// These functions have moved to alloc_helpers.h. External components should +// update their includes to use #include "esphome/core/alloc_helpers.h" directly. +// This re-export will be removed in 2026.11.0. +#include "esphome/core/alloc_helpers.h" + #ifdef USE_ESP8266 #include #include @@ -979,27 +985,13 @@ inline bool str_endswith_ignore_case(const std::string &str, const char *suffix) return str_endswith_ignore_case(str.c_str(), str.size(), suffix, strlen(suffix)); } -/// Truncate a string to a specific length. -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -std::string str_truncate(const std::string &str, size_t length); +// str_truncate moved to alloc_helpers.h - remove this include before 2026.11.0 -/// Extract the part of the string until either the first occurrence of the specified character, or the end -/// (requires str to be null-terminated). -std::string str_until(const char *str, char ch); -/// Extract the part of the string until either the first occurrence of the specified character, or the end. -std::string str_until(const std::string &str, char ch); - -/// Convert the string to lower case. -std::string str_lower_case(const std::string &str); -/// Convert the string to upper case. -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -std::string str_upper_case(const std::string &str); +// str_until, str_lower_case, str_upper_case moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Convert a single char to snake_case: lowercase and space to underscore. constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } -/// Convert the string to snake case (lowercase with underscores). -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -std::string str_snake_case(const std::string &str); +// str_snake_case moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore. constexpr char to_sanitized_char(char c) { @@ -1022,9 +1014,7 @@ template inline char *str_sanitize_to(char (&buffer)[N], const char *s return str_sanitize_to(buffer, N, str); } -/// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. -/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead. -std::string str_sanitize(const std::string &str); +// str_sanitize moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations. /// This computes object_id hashes directly from names without creating an intermediate buffer. @@ -1040,13 +1030,7 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) { return hash; } -/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator). -/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. -std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...); - -/// sprintf-like function returning std::string. -/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead. -std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...); +// str_snprintf, str_sprintf moved to alloc_helpers.h - remove this comment before 2026.11.0 #ifdef USE_ESP8266 // ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM) @@ -1441,189 +1425,26 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE); } -/// Format the six-byte array \p mac into a MAC address. -/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. -std::string format_mac_address_pretty(const uint8_t mac[6]); -/// Format the byte array \p data of length \p len in lowercased hex. -/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. -std::string format_hex(const uint8_t *data, size_t length); -/// Format the vector \p data in lowercased hex. -/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. -std::string format_hex(const std::vector &data); +// format_mac_address_pretty, format_hex (all overloads) moved to alloc_helpers.h +// Remove this comment and the template overloads below before 2026.11.0 + /// Format an unsigned integer in lowercased hex, starting with the most significant byte. /// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. template::value, int> = 0> std::string format_hex(T val) { val = convert_big_endian(val); return format_hex(reinterpret_cast(&val), sizeof(T)); } /// Format the std::array \p data in lowercased hex. /// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. template std::string format_hex(const std::array &data) { return format_hex(data.data(), data.size()); } -/** Format a byte array in pretty-printed, human-readable hex format. - * - * Converts binary data to a hexadecimal string representation with customizable formatting. - * Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator. - * Optionally includes the total byte count in parentheses at the end. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data Pointer to the byte array to format. - * @param length Number of bytes in the array. - * @param separator Character to use between hex bytes (default: '.'). - * @param show_length Whether to append the byte count in parentheses (default: true). - * @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters. - * - * @note Returns empty string if data is nullptr or length is 0. - * @note The length will only be appended if show_length is true AND the length is greater than 4. - * - * Example: - * @code - * uint8_t data[] = {0xA1, 0xB2, 0xC3}; - * format_hex_pretty(data, 3); // Returns "A1.B2.C3" (no length shown for <= 4 parts) - * uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5}; - * format_hex_pretty(data2, 5); // Returns "A1.B2.C3.D4.E5 (5)" - * format_hex_pretty(data2, 5, ':'); // Returns "A1:B2:C3:D4:E5 (5)" - * format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5" - * @endcode - */ -std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true); +// format_hex_pretty (all overloads) moved to alloc_helpers.h +// Remove this comment and the template overload below before 2026.11.0 -/** Format a 16-bit word array in pretty-printed, human-readable hex format. - * - * Similar to the byte array version, but formats 16-bit words as 4-digit hex values. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data Pointer to the 16-bit word array to format. - * @param length Number of 16-bit words in the array. - * @param separator Character to use between hex words (default: '.'). - * @param show_length Whether to append the word count in parentheses (default: true). - * @return Formatted hex string with 4-digit hex values per word. - * - * @note The length will only be appended if show_length is true AND the length is greater than 4. - * - * Example: - * @code - * uint16_t data[] = {0xA1B2, 0xC3D4}; - * format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts) - * uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6}; - * format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)" - * @endcode - */ -std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true); - -/** Format a byte vector in pretty-printed, human-readable hex format. - * - * Convenience overload for std::vector. Formats each byte as a two-digit - * uppercase hex value with customizable separator. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data Vector of bytes to format. - * @param separator Character to use between hex bytes (default: '.'). - * @param show_length Whether to append the byte count in parentheses (default: true). - * @return Formatted hex string representation of the vector contents. - * - * @note The length will only be appended if show_length is true AND the vector size is greater than 4. - * - * Example: - * @code - * std::vector data = {0xDE, 0xAD, 0xBE, 0xEF}; - * format_hex_pretty(data); // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts) - * std::vector data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA}; - * format_hex_pretty(data2); // Returns "DE.AD.BE.EF.CA (5)" - * format_hex_pretty(data2, '-'); // Returns "DE-AD-BE-EF-CA (5)" - * @endcode - */ -std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true); - -/** Format a 16-bit word vector in pretty-printed, human-readable hex format. - * - * Convenience overload for std::vector. Each 16-bit word is formatted - * as a 4-digit uppercase hex value in big-endian order. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data Vector of 16-bit words to format. - * @param separator Character to use between hex words (default: '.'). - * @param show_length Whether to append the word count in parentheses (default: true). - * @return Formatted hex string representation of the vector contents. - * - * @note The length will only be appended if show_length is true AND the vector size is greater than 4. - * - * Example: - * @code - * std::vector data = {0x1234, 0x5678}; - * format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts) - * std::vector data2 = {0x1234, 0x5678, 0x9ABC}; - * format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)" - * @endcode - */ -std::string format_hex_pretty(const std::vector &data, char separator = '.', bool show_length = true); - -/** Format a string's bytes in pretty-printed, human-readable hex format. - * - * Treats each character in the string as a byte and formats it in hex. - * Useful for debugging binary data stored in std::string containers. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @param data String whose bytes should be formatted as hex. - * @param separator Character to use between hex bytes (default: '.'). - * @param show_length Whether to append the byte count in parentheses (default: true). - * @return Formatted hex string representation of the string's byte contents. - * - * @note The length will only be appended if show_length is true AND the string length is greater than 4. - * - * Example: - * @code - * std::string data = "ABC"; // ASCII: 0x41, 0x42, 0x43 - * format_hex_pretty(data); // Returns "41.42.43" (no length shown for <= 4 parts) - * std::string data2 = "ABCDE"; - * format_hex_pretty(data2); // Returns "41.42.43.44.45 (5)" - * @endcode - */ -std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true); - -/** Format an unsigned integer in pretty-printed, human-readable hex format. - * - * Converts the integer to big-endian byte order and formats each byte as hex. - * The most significant byte appears first in the output string. - * - * @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. - * Causes heap fragmentation on long-running devices. - * - * @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.). - * @param val The unsigned integer value to format. - * @param separator Character to use between hex bytes (default: '.'). - * @param show_length Whether to append the byte count in parentheses (default: true). - * @return Formatted hex string with most significant byte first. - * - * @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4. - * - * Example: - * @code - * uint32_t value = 0x12345678; - * format_hex_pretty(value); // Returns "12.34.56.78" (no length shown for <= 4 parts) - * uint64_t value2 = 0x123456789ABCDEF0; - * format_hex_pretty(value2); // Returns "12.34.56.78.9A.BC.DE.F0 (8)" - * format_hex_pretty(value2, ':'); // Returns "12:34:56:78:9A:BC:DE:F0 (8)" - * format_hex_pretty(0x1234); // Returns "12.34" - * @endcode - */ +/// Format an unsigned integer in pretty-printed, human-readable hex format. +/// @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead. template::value, int> = 0> std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) { val = convert_big_endian(val); @@ -1683,13 +1504,10 @@ inline char *format_bin_to(char (&buffer)[N], T val) { return format_bin_to(buffer, reinterpret_cast(&val), sizeof(T)); } -/// Format the byte array \p data of length \p len in binary. -/// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. -std::string format_bin(const uint8_t *data, size_t length); +// format_bin moved to alloc_helpers.h - remove this comment and template overload before 2026.11.0 + /// Format an unsigned integer in binary, starting with the most significant byte. /// @warning Allocates heap memory. Use format_bin_to() with a stack buffer instead. -/// Causes heap fragmentation on long-running devices. template::value, int> = 0> std::string format_bin(T val) { val = convert_big_endian(val); return format_bin(reinterpret_cast(&val), sizeof(T)); @@ -1705,9 +1523,7 @@ enum ParseOnOffState : uint8_t { /// Parse a string that contains either on, off or toggle. ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); -/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0. -ESPDEPRECATED("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.", "2026.1.0") -std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); +// value_accuracy_to_string moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null) static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64; @@ -1721,10 +1537,8 @@ size_t value_accuracy_with_uom_to_buf(std::span bu /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step); -std::string base64_encode(const uint8_t *buf, size_t buf_len); -std::string base64_encode(const std::vector &buf); - -std::vector base64_decode(const std::string &encoded_string); +// base64_encode (both overloads), base64_decode (vector overload) moved to alloc_helpers.h +// Remove this comment before 2026.11.0 size_t base64_decode(std::string const &encoded_string, uint8_t *buf, size_t buf_len); size_t base64_decode(const uint8_t *encoded_data, size_t encoded_len, uint8_t *buf, size_t buf_len); @@ -2160,15 +1974,7 @@ class HighFrequencyLoopRequester { /// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes). void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter) -/// Get the device MAC address as a string, in lowercase hex notation. -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -/// Use get_mac_address_into_buffer() instead. -std::string get_mac_address(); - -/// Get the device MAC address as a string, in colon-separated uppercase hex notation. -/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. -/// Use get_mac_address_pretty_into_buffer() instead. -std::string get_mac_address_pretty(); +// get_mac_address, get_mac_address_pretty moved to alloc_helpers.h - remove this comment before 2026.11.0 /// Get the device MAC address into the given buffer, in lowercase hex notation. /// Assumes buffer length is MAC_ADDRESS_BUFFER_SIZE (12 digits for hexadecimal representation followed by null diff --git a/script/ci-custom.py b/script/ci-custom.py index 6dce86924e0..02ec08bc318 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -722,18 +722,22 @@ def lint_trailing_whitespace(fname, match): # Heap-allocating helpers that cause fragmentation on long-running embedded devices. # These return std::string and should be replaced with stack-based alternatives. HEAP_ALLOCATING_HELPERS = { + "base64_encode": "base64_encode_to() with a pre-allocated buffer", "format_bin": "format_bin_to() with a stack buffer", "format_hex": "format_hex_to() with a stack buffer", "format_hex_pretty": "format_hex_pretty_to() with a stack buffer", "format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer", "get_mac_address": "get_mac_address_into_buffer() with a stack buffer", "get_mac_address_pretty": "get_mac_address_pretty_into_buffer() with a stack buffer", + "str_lower_case": "manual tolower() with a stack buffer", "str_sanitize": "str_sanitize_to() with a stack buffer", "str_truncate": "removal (function is unused)", + "str_until": "manual strchr()/find() with a StringRef or stack buffer", "str_upper_case": "removal (function is unused)", "str_snake_case": "removal (function is unused)", "str_sprintf": "snprintf() with a stack buffer", "str_snprintf": "snprintf() with a stack buffer", + "value_accuracy_to_string": "value_accuracy_to_buf() with a stack buffer", } @@ -743,24 +747,33 @@ HEAP_ALLOCATING_HELPERS = { # get_mac_address(?!_) ensures we don't match get_mac_address_into_buffer, etc. # CPP_RE_EOL captures rest of line so NOLINT comments are detected r"[^\w](" + r"base64_encode(?!_)|" r"format_bin(?!_)|" r"format_hex(?!_)|" r"format_hex_pretty(?!_)|" r"format_mac_address_pretty|" r"get_mac_address_pretty(?!_)|" r"get_mac_address(?!_)|" + r"str_lower_case|" r"str_sanitize(?!_)|" r"str_truncate|" + r"str_until|" r"str_upper_case|" r"str_snake_case|" r"str_sprintf|" - r"str_snprintf" + r"str_snprintf|" + r"value_accuracy_to_string" r")\s*\(" + CPP_RE_EOL, include=cpp_include, exclude=[ # The definitions themselves + "esphome/core/alloc_helpers.h", + "esphome/core/alloc_helpers.cpp", + # Backward compatibility re-exports (remove before 2026.11.0) "esphome/core/helpers.h", "esphome/core/helpers.cpp", + # Vendored third-party library + "esphome/components/http_request/httplib.h", ], ) def lint_no_heap_allocating_helpers(fname, match): @@ -812,6 +825,7 @@ def lint_no_sprintf(fname, match): "esphome/components/http_request/httplib.h", # Deprecated helpers that return std::string "esphome/core/helpers.cpp", + "esphome/core/alloc_helpers.cpp", # The using declaration itself "esphome/core/helpers.h", # Test fixtures - not production embedded code