mirror of
https://github.com/esphome/esphome.git
synced 2026-03-01 02:14:19 -07:00
[web_server_idf] Remove std::string temporaries from multipart header parsing
Refactor multipart utility functions to work with const char* + length instead of std::string to eliminate temporary heap allocations during header parsing. The original implementations used std::string for convenience when the OTA multipart support was first added, but these can be avoided since the multipart parser already provides raw pointers and lengths in its callbacks. - extract_header_param: takes (const char*, size_t, const char*, std::string&) instead of (const std::string&, const std::string&) -> std::string. Assigns directly to destination, avoiding intermediate string construction. - str_startswith_case_insensitive: takes (const char*, size_t, const char*) instead of (const std::string&, const std::string&) - str_trim: takes (const char*, size_t, std::string&) instead of (const std::string&) -> std::string - Rename stristr to strcasestr_n with explicit haystack length parameter to make the relationship to POSIX strcasestr clear and fix a latent buffer over-read risk (stristr relied on null-termination which the multipart parser does not guarantee for its callback data) - process_header_ no longer creates a std::string copy of the raw parser buffer before calling utility functions Saves ~350 bytes of flash.
This commit is contained in:
@@ -54,14 +54,15 @@ 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)
|
||||
std::string value_str(value, length);
|
||||
const char *field = current_header_field_.c_str();
|
||||
size_t field_len = current_header_field_.length();
|
||||
|
||||
if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) {
|
||||
if (str_startswith_case_insensitive(field, field_len, "content-disposition")) {
|
||||
// Parse name and filename from Content-Disposition
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
// Clear field for next header
|
||||
@@ -107,25 +108,29 @@ int MultipartReader::on_part_data_end(multipart_parser *parser) {
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
// Case-insensitive string prefix check
|
||||
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) {
|
||||
if (str.length() < prefix.length()) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length());
|
||||
return str_ncmp_ci(str, prefix, prefix_len);
|
||||
}
|
||||
|
||||
// Extract a parameter value from a header line
|
||||
// Handles both quoted and unquoted values
|
||||
std::string extract_header_param(const std::string &header, const std::string ¶m) {
|
||||
// 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);
|
||||
size_t search_pos = 0;
|
||||
|
||||
while (search_pos < header.length()) {
|
||||
while (search_pos < header_len) {
|
||||
// Look for param name
|
||||
const char *found = stristr(header.c_str() + search_pos, param.c_str());
|
||||
const char *found = strcasestr_n(header + search_pos, header_len - search_pos, param);
|
||||
if (!found) {
|
||||
return "";
|
||||
out.clear();
|
||||
return;
|
||||
}
|
||||
size_t pos = found - header.c_str();
|
||||
size_t pos = found - header;
|
||||
|
||||
// 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') {
|
||||
@@ -134,14 +139,14 @@ std::string extract_header_param(const std::string &header, const std::string &p
|
||||
}
|
||||
|
||||
// Move past param name
|
||||
pos += param.length();
|
||||
pos += param_len;
|
||||
|
||||
// Skip whitespace and find '='
|
||||
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||
while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos >= header.length() || header[pos] != '=') {
|
||||
if (pos >= header_len || header[pos] != '=') {
|
||||
search_pos = pos;
|
||||
continue;
|
||||
}
|
||||
@@ -149,36 +154,39 @@ std::string extract_header_param(const std::string &header, const std::string &p
|
||||
pos++; // Skip '='
|
||||
|
||||
// Skip whitespace after '='
|
||||
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||
while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (pos >= header.length()) {
|
||||
return "";
|
||||
if (pos >= header_len) {
|
||||
out.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if value is quoted
|
||||
if (header[pos] == '"') {
|
||||
pos++;
|
||||
size_t end = header.find('"', pos);
|
||||
if (end != std::string::npos) {
|
||||
return header.substr(pos, end - pos);
|
||||
const char *end = static_cast<const char *>(memchr(header + pos, '"', header_len - pos));
|
||||
if (end) {
|
||||
out.assign(header + pos, end - (header + pos));
|
||||
return;
|
||||
}
|
||||
// Malformed - no closing quote
|
||||
return "";
|
||||
out.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unquoted value - find the end (semicolon, comma, or end of string)
|
||||
size_t end = pos;
|
||||
while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' &&
|
||||
header[end] != '\t') {
|
||||
while (end < header_len && header[end] != ';' && header[end] != ',' && header[end] != ' ' && header[end] != '\t') {
|
||||
end++;
|
||||
}
|
||||
|
||||
return header.substr(pos, end - pos);
|
||||
out.assign(header + pos, end - pos);
|
||||
return;
|
||||
}
|
||||
|
||||
return "";
|
||||
out.clear();
|
||||
}
|
||||
|
||||
// Parse boundary from Content-Type header
|
||||
@@ -189,13 +197,15 @@ 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 (!stristr(content_type, "multipart/form-data")) {
|
||||
if (!strcasestr_n(content_type, content_type_len, "multipart/form-data")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look for boundary parameter
|
||||
const char *b = stristr(content_type, "boundary=");
|
||||
const char *b = strcasestr_n(content_type, content_type_len, "boundary=");
|
||||
if (!b) {
|
||||
return false;
|
||||
}
|
||||
@@ -238,14 +248,15 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
}
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
|
||||
@@ -66,19 +66,20 @@ class MultipartReader {
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
// Case-insensitive string prefix check
|
||||
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix);
|
||||
bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix);
|
||||
|
||||
// Extract a parameter value from a header line
|
||||
// Handles both quoted and unquoted values
|
||||
std::string extract_header_param(const std::string &header, const std::string ¶m);
|
||||
// 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);
|
||||
|
||||
// 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 of a string
|
||||
std::string str_trim(const std::string &str);
|
||||
// Trim whitespace from both ends, assign result to out
|
||||
void str_trim(const char *str, size_t len, std::string &out);
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
|
||||
|
||||
@@ -98,8 +98,8 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Case-insensitive string search (like strstr but case-insensitive)
|
||||
const char *stristr(const char *haystack, const char *needle) {
|
||||
// Bounded case-insensitive string search (like strcasestr but length-bounded)
|
||||
const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle) {
|
||||
if (!haystack) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -109,7 +109,12 @@ const char *stristr(const char *haystack, const char *needle) {
|
||||
return haystack;
|
||||
}
|
||||
|
||||
for (const char *p = haystack; *p; p++) {
|
||||
if (haystack_len < needle_len) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *end = haystack + haystack_len - needle_len + 1;
|
||||
for (const char *p = haystack; p < end; p++) {
|
||||
if (str_ncmp_ci(p, needle, needle_len)) {
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
// Case-insensitive string search (like strstr but case-insensitive)
|
||||
const char *stristr(const char *haystack, const char *needle);
|
||||
// Bounded case-insensitive string search (like strcasestr but length-bounded)
|
||||
const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle);
|
||||
|
||||
} // namespace esphome::web_server_idf
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -171,10 +171,11 @@ 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
|
||||
if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) {
|
||||
size_t content_type_len = strlen(content_type_char);
|
||||
if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
|
||||
// Normal form data - proceed with regular handling
|
||||
#ifdef USE_WEBSERVER_OTA
|
||||
} else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
|
||||
} else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
|
||||
auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
|
||||
return server->handle_multipart_upload_(r, content_type_char);
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user