ESPHome 2026.2.3
Loading...
Searching...
No Matches
multipart.cpp
Go to the documentation of this file.
2#if defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
3#include "multipart.h"
4#include "utils.h"
5#include "esphome/core/log.h"
6#include <cstring>
7#include "multipart_parser.h"
8
10
11static const char *const TAG = "multipart";
12
13// ========== MultipartReader Implementation ==========
14
15MultipartReader::MultipartReader(const std::string &boundary) {
16 // Initialize settings with callbacks
17 memset(&settings_, 0, sizeof(settings_));
18 settings_.on_header_field = on_header_field;
19 settings_.on_header_value = on_header_value;
20 settings_.on_part_data = on_part_data;
21 settings_.on_part_data_end = on_part_data_end;
22
23 ESP_LOGV(TAG, "Initializing multipart parser with boundary: '%s' (len: %zu)", boundary.c_str(), boundary.length());
24
25 // Create parser with boundary
26 parser_ = multipart_parser_init(boundary.c_str(), &settings_);
27 if (parser_) {
28 multipart_parser_set_data(parser_, this);
29 } else {
30 ESP_LOGE(TAG, "Failed to initialize multipart parser");
31 }
32}
33
35 if (parser_) {
36 multipart_parser_free(parser_);
37 }
38}
39
40size_t MultipartReader::parse(const char *data, size_t len) {
41 if (!parser_) {
42 ESP_LOGE(TAG, "Parser not initialized");
43 return 0;
44 }
45
46 size_t parsed = multipart_parser_execute(parser_, data, len);
47
48 if (parsed != len) {
49 ESP_LOGW(TAG, "Parser consumed %zu of %zu bytes - possible error", parsed, len);
50 }
51
52 return parsed;
53}
54
55void MultipartReader::process_header_(const char *value, size_t length) {
56 // Process the completed header (field + value pair)
57 const char *field = current_header_field_.c_str();
58 size_t field_len = current_header_field_.length();
59
60 if (str_startswith_case_insensitive(field, field_len, "content-disposition")) {
61 // Parse name and filename from Content-Disposition
62 extract_header_param(value, length, "name", current_part_.name);
63 extract_header_param(value, length, "filename", current_part_.filename);
64 } else if (str_startswith_case_insensitive(field, field_len, "content-type")) {
65 str_trim(value, length, current_part_.content_type);
66 }
67
68 // Clear field for next header
69 current_header_field_.clear();
70}
71
72int MultipartReader::on_header_field(multipart_parser *parser, const char *at, size_t length) {
73 MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser));
74 reader->current_header_field_.assign(at, length);
75 return 0;
76}
77
78int MultipartReader::on_header_value(multipart_parser *parser, const char *at, size_t length) {
79 MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser));
80 reader->process_header_(at, length);
81 return 0;
82}
83
84int MultipartReader::on_part_data(multipart_parser *parser, const char *at, size_t length) {
85 MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser));
86 // Only process file uploads
87 if (reader->has_file() && reader->data_callback_) {
88 // IMPORTANT: The 'at' pointer points to data within the parser's input buffer.
89 // This data is only valid during this callback. The callback handler MUST
90 // process or copy the data immediately - it cannot store the pointer for
91 // later use as the buffer will be overwritten.
92 reader->data_callback_(reinterpret_cast<const uint8_t *>(at), length);
93 }
94 return 0;
95}
96
97int MultipartReader::on_part_data_end(multipart_parser *parser) {
98 MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser));
99 ESP_LOGV(TAG, "Part data end");
100 if (reader->part_complete_callback_) {
101 reader->part_complete_callback_();
102 }
103 // Clear part info for next part
104 reader->current_part_ = Part{};
105 return 0;
106}
107
108// ========== Utility Functions ==========
109
110// Case-insensitive string prefix check
111bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix) {
112 size_t prefix_len = strlen(prefix);
113 if (str_len < prefix_len) {
114 return false;
115 }
116 return str_ncmp_ci(str, prefix, prefix_len);
117}
118
119// Extract a parameter value from a header line
120// Handles both quoted and unquoted values
121// Assigns to out if found, clears out otherwise
122void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out) {
123 size_t param_len = strlen(param);
124 size_t search_pos = 0;
125
126 while (search_pos < header_len) {
127 // Look for param name
128 const char *found = strcasestr_n(header + search_pos, header_len - search_pos, param);
129 if (!found) {
130 out.clear();
131 return;
132 }
133 size_t pos = found - header;
134
135 // Check if this is a word boundary (not part of another parameter)
136 if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') {
137 search_pos = pos + 1;
138 continue;
139 }
140
141 // Move past param name
142 pos += param_len;
143
144 // Skip whitespace and find '='
145 while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
146 pos++;
147 }
148
149 if (pos >= header_len || header[pos] != '=') {
150 search_pos = pos;
151 continue;
152 }
153
154 pos++; // Skip '='
155
156 // Skip whitespace after '='
157 while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
158 pos++;
159 }
160
161 if (pos >= header_len) {
162 out.clear();
163 return;
164 }
165
166 // Check if value is quoted
167 if (header[pos] == '"') {
168 pos++;
169 const char *end = static_cast<const char *>(memchr(header + pos, '"', header_len - pos));
170 if (end) {
171 out.assign(header + pos, end - (header + pos));
172 return;
173 }
174 // Malformed - no closing quote
175 out.clear();
176 return;
177 }
178
179 // Unquoted value - find the end (semicolon, comma, or end of string)
180 size_t end = pos;
181 while (end < header_len && header[end] != ';' && header[end] != ',' && header[end] != ' ' && header[end] != '\t') {
182 end++;
183 }
184
185 out.assign(header + pos, end - pos);
186 return;
187 }
188
189 out.clear();
190}
191
192// Parse boundary from Content-Type header
193// Returns true if boundary found, false otherwise
194// boundary_start and boundary_len will point to the boundary value
195bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len) {
196 if (!content_type) {
197 return false;
198 }
199
200 size_t content_type_len = strlen(content_type);
201
202 // Check for multipart/form-data (case-insensitive)
203 if (!strcasestr_n(content_type, content_type_len, "multipart/form-data")) {
204 return false;
205 }
206
207 // Look for boundary parameter
208 const char *b = strcasestr_n(content_type, content_type_len, "boundary=");
209 if (!b) {
210 return false;
211 }
212
213 const char *start = b + 9; // Skip "boundary="
214
215 // Skip whitespace
216 while (*start == ' ' || *start == '\t') {
217 start++;
218 }
219
220 if (!*start) {
221 return false;
222 }
223
224 // Find end of boundary
225 const char *end = start;
226 if (*end == '"') {
227 // Quoted boundary
228 start++;
229 end++;
230 while (*end && *end != '"') {
231 end++;
232 }
233 *boundary_len = end - start;
234 } else {
235 // Unquoted boundary
236 while (*end && *end != ' ' && *end != ';' && *end != '\r' && *end != '\n' && *end != '\t') {
237 end++;
238 }
239 *boundary_len = end - start;
240 }
241
242 if (*boundary_len == 0) {
243 return false;
244 }
245
246 *boundary_start = start;
247
248 return true;
249}
250
251// Trim whitespace from both ends, assign result to out
252void str_trim(const char *str, size_t len, std::string &out) {
253 const char *start = str;
254 const char *end = str + len;
255 while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n'))
256 start++;
257 while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n'))
258 end--;
259 out.assign(start, end - start);
260}
261
262} // namespace esphome::web_server_idf
263#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)
MultipartReader(const std::string &boundary)
Definition multipart.cpp:15
size_t parse(const char *data, size_t len)
Definition multipart.cpp:40
const char *const TAG
Definition spi.cpp:7
void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out)
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len)
const char * strcasestr_n(const char *haystack, size_t haystack_len, const char *needle)
Definition utils.cpp:102
void str_trim(const char *str, size_t len, std::string &out)
bool str_ncmp_ci(const char *s1, const char *s2, size_t n)
Definition utils.cpp:92
bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix)
std::string size_t len
Definition helpers.h:692
size_t size_t pos
Definition helpers.h:729
uint8_t end[39]
Definition sun_gtil2.cpp:17
uint16_t length
Definition tt21100.cpp:0