From c03e38d688012524c0829f3d40e717cb6b145b85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 11 Feb 2026 16:49:51 -0600 Subject: [PATCH] Revert "[web_server_idf] Remove std::string temporaries from multipart header parsing" This reverts commit f6bb54486d0efacedb70dc12431e617344a68cc6. --- .../components/web_server_idf/multipart.cpp | 87 ++++++++----------- esphome/components/web_server_idf/multipart.h | 9 +- esphome/components/web_server_idf/utils.cpp | 11 +-- esphome/components/web_server_idf/utils.h | 4 +- .../web_server_idf/web_server_idf.cpp | 5 +- 5 files changed, 49 insertions(+), 67 deletions(-) diff --git a/esphome/components/web_server_idf/multipart.cpp b/esphome/components/web_server_idf/multipart.cpp index 7744272a5c..52dafeb997 100644 --- a/esphome/components/web_server_idf/multipart.cpp +++ b/esphome/components/web_server_idf/multipart.cpp @@ -54,15 +54,14 @@ size_t MultipartReader::parse(const char *data, size_t len) { void MultipartReader::process_header_(const char *value, size_t length) { // Process the completed header (field + value pair) - const char *field = current_header_field_.c_str(); - size_t field_len = current_header_field_.length(); + std::string value_str(value, length); - if (str_startswith_case_insensitive(field, field_len, "content-disposition")) { + if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) { // Parse name and filename from Content-Disposition - extract_header_param(value, length, "name", current_part_.name); - extract_header_param(value, length, "filename", current_part_.filename); - } else if (str_startswith_case_insensitive(field, field_len, "content-type")) { - str_trim(value, length, current_part_.content_type); + current_part_.name = extract_header_param(value_str, "name"); + current_part_.filename = extract_header_param(value_str, "filename"); + } else if (str_startswith_case_insensitive(current_header_field_, "content-type")) { + current_part_.content_type = str_trim(value_str); } // Clear field for next header @@ -108,29 +107,25 @@ int MultipartReader::on_part_data_end(multipart_parser *parser) { // ========== Utility Functions ========== // Case-insensitive string prefix check -bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix) { - size_t prefix_len = strlen(prefix); - if (str_len < prefix_len) { +bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) { + if (str.length() < prefix.length()) { return false; } - return str_ncmp_ci(str, prefix, prefix_len); + return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length()); } // Extract a parameter value from a header line // Handles both quoted and unquoted values -// Assigns to out if found, clears out otherwise -void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out) { - size_t param_len = strlen(param); +std::string extract_header_param(const std::string &header, const std::string ¶m) { size_t search_pos = 0; - while (search_pos < header_len) { + while (search_pos < header.length()) { // Look for param name - const char *found = strcasestr_n(header + search_pos, header_len - search_pos, param); + const char *found = stristr(header.c_str() + search_pos, param.c_str()); if (!found) { - out.clear(); - return; + return ""; } - size_t pos = found - header; + size_t pos = found - header.c_str(); // Check if this is a word boundary (not part of another parameter) if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') { @@ -139,14 +134,14 @@ void extract_header_param(const char *header, size_t header_len, const char *par } // Move past param name - pos += param_len; + pos += param.length(); // Skip whitespace and find '=' - while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) { + while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { pos++; } - if (pos >= header_len || header[pos] != '=') { + if (pos >= header.length() || header[pos] != '=') { search_pos = pos; continue; } @@ -154,39 +149,36 @@ void extract_header_param(const char *header, size_t header_len, const char *par pos++; // Skip '=' // Skip whitespace after '=' - while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) { + while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { pos++; } - if (pos >= header_len) { - out.clear(); - return; + if (pos >= header.length()) { + return ""; } // Check if value is quoted if (header[pos] == '"') { pos++; - const char *end = static_cast(memchr(header + pos, '"', header_len - pos)); - if (end) { - out.assign(header + pos, end - (header + pos)); - return; + size_t end = header.find('"', pos); + if (end != std::string::npos) { + return header.substr(pos, end - pos); } // Malformed - no closing quote - out.clear(); - return; + return ""; } // Unquoted value - find the end (semicolon, comma, or end of string) size_t end = pos; - while (end < header_len && header[end] != ';' && header[end] != ',' && header[end] != ' ' && header[end] != '\t') { + while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' && + header[end] != '\t') { end++; } - out.assign(header + pos, end - pos); - return; + return header.substr(pos, end - pos); } - out.clear(); + return ""; } // Parse boundary from Content-Type header @@ -197,15 +189,13 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st return false; } - size_t content_type_len = strlen(content_type); - // Check for multipart/form-data (case-insensitive) - if (!strcasestr_n(content_type, content_type_len, "multipart/form-data")) { + if (!stristr(content_type, "multipart/form-data")) { return false; } // Look for boundary parameter - const char *b = strcasestr_n(content_type, content_type_len, "boundary="); + const char *b = stristr(content_type, "boundary="); if (!b) { return false; } @@ -248,15 +238,14 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st return true; } -// Trim whitespace from both ends, assign result to out -void str_trim(const char *str, size_t len, std::string &out) { - const char *start = str; - const char *end = str + len; - while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n')) - start++; - while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n')) - end--; - out.assign(start, end - start); +// Trim whitespace from both ends of a string +std::string str_trim(const std::string &str) { + size_t start = str.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) { + return ""; + } + size_t end = str.find_last_not_of(" \t\r\n"); + return str.substr(start, end - start + 1); } } // namespace esphome::web_server_idf diff --git a/esphome/components/web_server_idf/multipart.h b/esphome/components/web_server_idf/multipart.h index cb1e0ecd1d..9008be6459 100644 --- a/esphome/components/web_server_idf/multipart.h +++ b/esphome/components/web_server_idf/multipart.h @@ -66,20 +66,19 @@ class MultipartReader { // ========== Utility Functions ========== // Case-insensitive string prefix check -bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix); +bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix); // Extract a parameter value from a header line // Handles both quoted and unquoted values -// Assigns to out if found, clears out otherwise -void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out); +std::string extract_header_param(const std::string &header, const std::string ¶m); // Parse boundary from Content-Type header // Returns true if boundary found, false otherwise // boundary_start and boundary_len will point to the boundary value bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len); -// Trim whitespace from both ends, assign result to out -void str_trim(const char *str, size_t len, std::string &out); +// Trim whitespace from both ends of a string +std::string str_trim(const std::string &str); } // namespace esphome::web_server_idf #endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA) diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index 81ae626277..a1a2793b4a 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -98,8 +98,8 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n) { return true; } -// Bounded case-insensitive string search (like strcasestr but length-bounded) -const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle) { +// Case-insensitive string search (like strstr but case-insensitive) +const char *stristr(const char *haystack, const char *needle) { if (!haystack) { return nullptr; } @@ -109,12 +109,7 @@ const char *strcasestr_n(const char *haystack, size_t haystack_len, const char * return haystack; } - if (haystack_len < needle_len) { - return nullptr; - } - - const char *end = haystack + haystack_len - needle_len + 1; - for (const char *p = haystack; p < end; p++) { + for (const char *p = haystack; *p; p++) { if (str_ncmp_ci(p, needle, needle_len)) { return p; } diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 87635c0458..bb0610dac0 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -25,8 +25,8 @@ inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b) // Helper function for case-insensitive string region comparison bool str_ncmp_ci(const char *s1, const char *s2, size_t n); -// Bounded case-insensitive string search (like strcasestr but length-bounded) -const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle); +// Case-insensitive string search (like strstr but case-insensitive) +const char *stristr(const char *haystack, const char *needle); } // namespace esphome::web_server_idf #endif // USE_ESP32 diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 5a7957fff4..225d6f85cb 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -171,11 +171,10 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) { const char *content_type_char = content_type.value().c_str(); // Check most common case first - size_t content_type_len = strlen(content_type_char); - if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) { + if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) { // Normal form data - proceed with regular handling #ifdef USE_WEBSERVER_OTA - } else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) { + } else if (stristr(content_type_char, "multipart/form-data") != nullptr) { auto *server = static_cast(r->user_ctx); return server->handle_multipart_upload_(r, content_type_char); #endif