DeterministicESPAsyncWebServer 1.2.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
multipart.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file multipart.cpp
6 * @brief In-place multipart/form-data parser implementation.
7 */
8
9#include "multipart.h"
10#include <string.h>
11
12// Skip past a CRLF pair; returns p+2 if CRLF found, else p unchanged.
13static char *skip_crlf(char *p)
14{
15 if (p[0] == '\r' && p[1] == '\n')
16 return p + 2;
17 return p;
18}
19
20// Extract parameter value: search for `key="<value>"` inside `src`.
21// If found, null-terminates in-place and returns pointer to the value.
22// Returns nullptr if not found.
23static char *extract_quoted_param(char *src, const char *key)
24{
25 char *p = strstr(src, key);
26 if (!p)
27 return nullptr;
28 p += strlen(key);
29 if (*p != '"')
30 return nullptr;
31 p++; // skip opening quote
32 char *end = strchr(p, '"');
33 if (!end)
34 return nullptr;
35 *end = '\0';
36 return p;
37}
38
40{
41 mp->part_count = 0;
42
43 const char *ct = http_get_header(req, "Content-Type");
44 if (!ct)
45 return false;
46
47 // Extract boundary value (may be quoted or unquoted)
48 const char *bsearch = strstr(ct, "boundary=");
49 if (!bsearch)
50 return false;
51 bsearch += 9;
52 if (*bsearch == '"')
53 bsearch++;
54
55 char bval[MAX_BOUNDARY_LEN + 1];
56 size_t blen = 0;
57 while (*bsearch && *bsearch != '"' && *bsearch != ';' && *bsearch != ' '
58 && blen < MAX_BOUNDARY_LEN)
59 bval[blen++] = *bsearch++;
60 bval[blen] = '\0';
61
62 if (blen == 0)
63 return false;
64
65 // Delimiter is "--" + boundary
66 char delim[MAX_BOUNDARY_LEN + 3];
67 delim[0] = '-';
68 delim[1] = '-';
69 memcpy(delim + 2, bval, blen + 1); // includes null
70 size_t dlen = blen + 2;
71
72 char *body = (char *)req->body;
73
74 // Find the first delimiter
75 char *pos = strstr(body, delim);
76 if (!pos)
77 return false;
78
79 pos += (int)dlen;
80 pos = skip_crlf(pos);
81
83 {
84 // End boundary is "--" immediately after delimiter
85 if (pos[0] == '-' && pos[1] == '-')
86 break;
87
88 MultipartPart *part = &mp->parts[mp->part_count];
89 part->name = nullptr;
90 part->filename = nullptr;
91 part->type = nullptr;
92 part->data = nullptr;
93 part->data_len = 0;
94
95 // Parse per-part headers until the blank line
96 for (;;)
97 {
98 if (pos[0] == '\r' && pos[1] == '\n')
99 {
100 pos += 2; // blank line → start of data
101 break;
102 }
103
104 char *line_end = strstr(pos, "\r\n");
105 if (!line_end)
106 return false;
107
108 *line_end = '\0'; // null-terminate header line
109
110 if (strncasecmp(pos, "Content-Disposition:", 20) == 0)
111 {
112 char *v = pos + 20;
113 while (*v == ' ')
114 v++;
115 // Extract name and filename (in-place, with null termination)
116 // Extract filename before name: filename= appears after name= in the
117 // header, so extracting it first avoids corrupting name='s search
118 // when extract_quoted_param null-terminates the value in-place.
119 part->filename = extract_quoted_param(v, "filename=");
120 part->name = extract_quoted_param(v, "name=");
121 }
122 else if (strncasecmp(pos, "Content-Type:", 13) == 0)
123 {
124 char *v = pos + 13;
125 while (*v == ' ')
126 v++;
127 part->type = v;
128 }
129
130 pos = line_end + 2; // next line (skip '\0' + '\n')
131 }
132
133 // Data runs from pos until the next delimiter (preceded by \r\n)
134 part->data = pos;
135 char *next = strstr(pos, delim);
136 if (!next)
137 return false;
138
139 char *data_end = next - 2; // \r\n before the delimiter
140 part->data_len = (size_t)(data_end - pos);
141 *data_end = '\0'; // null-terminate data
142
143 mp->part_count++;
144
145 pos = next + (int)dlen;
146 pos = skip_crlf(pos);
147 }
148
149 return mp->part_count > 0;
150}
151
152const char *multipart_get_field(const Multipart *mp, const char *field)
153{
154 for (int i = 0; i < mp->part_count; i++)
155 {
156 if (mp->parts[i].name && strcmp(mp->parts[i].name, field) == 0)
157 return mp->parts[i].data;
158 }
159 return nullptr;
160}
#define MAX_BOUNDARY_LEN
Maximum MIME boundary length (RFC 2046 allows up to 70 characters).
#define MAX_MULTIPART_PARTS
Maximum simultaneously parsed multipart parts per request.
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
const char * multipart_get_field(const Multipart *mp, const char *field)
Look up a field value across all parsed parts by name.
bool multipart_parse(HttpReq *req, Multipart *mp)
Parse the body of req as multipart/form-data.
Definition multipart.cpp:39
In-place multipart/form-data parser (RFC 7578).
Fully-parsed HTTP/1.1 request.
uint8_t body[BODY_BUF_SIZE+1]
Stored body bytes, always null-terminated.
One parsed part from a multipart body.
Definition multipart.h:41
const char * data
Part body (null-terminated in-place).
Definition multipart.h:45
const char * type
Content-Type of this part, or nullptr.
Definition multipart.h:44
size_t data_len
Part body length in bytes (not counting the null).
Definition multipart.h:46
const char * name
Form field name from Content-Disposition, or nullptr.
Definition multipart.h:42
const char * filename
Upload filename from Content-Disposition, or nullptr.
Definition multipart.h:43
Container for all parsed parts of a multipart body.
Definition multipart.h:53
MultipartPart parts[MAX_MULTIPART_PARTS]
Parsed parts.
Definition multipart.h:54
int part_count
Number of valid entries in parts[].
Definition multipart.h:55