ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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// Length-bounded, binary-safe forward search for needle[0..nlen) within hay[0..hlen).
13// Unlike strstr, it does not stop at a NUL, so a body containing NUL bytes scans correctly.
14static char *mem_find(char *hay, size_t hlen, const char *needle, size_t nlen)
15{
16 if (nlen == 0 || nlen > hlen) // GCOVR_EXCL_BR_LINE nlen==0 never true: every caller in this file passes
17 {
18 return nullptr; // a fixed literal (2) or a length derived from an already-checked nonzero blen
19 }
20 for (size_t i = 0; i + nlen <= hlen; i++)
21 {
22 if (memcmp(hay + i, needle, nlen) == 0)
23 {
24 return hay + i;
25 }
26 }
27 return nullptr;
28}
29
30// Extract parameter value: search for `key="<value>"` inside `src`.
31// If found, null-terminates in-place and returns pointer to the value.
32// Returns nullptr if not found.
33static char *extract_quoted_param(char *src, const char *key)
34{
35 char *p = strstr(src, key);
36 if (!p)
37 {
38 return nullptr;
39 }
40#define PC_key_max 32 // param keys are short literals ("name=", "filename=")
41 p += strnlen(key, PC_key_max);
42 if (*p != '"')
43 {
44 return nullptr;
45 }
46 p++; // skip opening quote
47 char *end = strchr(p, '"');
48 if (!end)
49 {
50 return nullptr;
51 }
52 *end = '\0';
53 return p;
54}
55
57{
58 mp->part_count = 0;
59
60 const char *ct = http_get_header(req, "Content-Type");
61 if (!ct)
62 {
63 return false;
64 }
65
66 // Extract boundary value (may be quoted or unquoted)
67 const char *bsearch = strstr(ct, "boundary=");
68 if (!bsearch)
69 {
70 return false;
71 }
72 bsearch += 9;
73 if (*bsearch == '"')
74 {
75 bsearch++;
76 }
77
78 char bval[MAX_BOUNDARY_LEN + 1];
79 size_t blen = 0;
80 // blen reaching MAX_BOUNDARY_LEN (72) never terminates this loop in practice: bsearch points into
81 // the stored Content-Type value, which http_parser caps at MAX_VAL_LEN-1 (47) bytes total, and
82 // "boundary=" alone consumes 9+ of those - so the NUL (or a '"'/';'/' ' delimiter) is always hit first.
83 while (*bsearch && *bsearch != '"' && *bsearch != ';' && *bsearch != ' ' && // GCOVR_EXCL_BR_LINE
84 blen < MAX_BOUNDARY_LEN)
85 {
86 bval[blen++] = *bsearch++;
87 }
88 bval[blen] = '\0';
89
90 if (blen == 0)
91 {
92 return false;
93 }
94
95 // Delimiter is "--" + boundary
96 char delim[MAX_BOUNDARY_LEN + 3];
97 delim[0] = '-';
98 delim[1] = '-';
99 memcpy(delim + 2, bval, blen + 1); // includes null
100 size_t dlen = blen + 2;
101
102 char *body = (char *)req->body;
103 char *end = body + req->body_len; // length-bounded scanning: NUL bytes in a binary part are fine
104
105 // A part's data ends at the full "\r\n--boundary" delimiter (RFC 2046): matching only the
106 // "--boundary" bytes would false-truncate a binary part that happens to contain them.
107 char ddelim[MAX_BOUNDARY_LEN + 5];
108 ddelim[0] = '\r';
109 ddelim[1] = '\n';
110 memcpy(ddelim + 2, delim, dlen); // "--boundary" (dlen bytes, no NUL)
111 size_t ddlen = dlen + 2;
112
113 // Find the first delimiter ("--boundary"; a leading CRLF / preamble is optional here).
114 char *pos = mem_find(body, (size_t)(end - body), delim, dlen);
115 if (!pos)
116 {
117 return false;
118 }
119 pos += dlen;
120 if (pos + 2 <= end && pos[0] == '\r' && pos[1] == '\n')
121 {
122 pos += 2;
123 }
124
125 while (mp->part_count < MAX_MULTIPART_PARTS)
126 {
127 // "--" immediately after the delimiter marks the terminating boundary.
128 if (pos + 2 <= end && pos[0] == '-' && pos[1] == '-')
129 {
130 break;
131 }
132
133 MultipartPart *part = &mp->parts[mp->part_count];
134 part->name = nullptr;
135 part->filename = nullptr;
136 part->type = nullptr;
137 part->data = nullptr;
138 part->data_len = 0;
139
140 // Parse the per-part headers (text) until the blank line.
141 for (;;)
142 {
143 if (pos + 2 <= end && pos[0] == '\r' && pos[1] == '\n')
144 {
145 pos += 2; // blank line → start of data
146 break;
147 }
148
149 char *line_end = mem_find(pos, (size_t)(end - pos), "\r\n", 2);
150 if (!line_end)
151 {
152 return false;
153 }
154
155 *line_end = '\0'; // null-terminate header line
156
157 if (strncasecmp(pos, "Content-Disposition:", 20) == 0)
158 {
159 char *v = pos + 20;
160 while (*v == ' ')
161 {
162 v++;
163 }
164 // Extract filename before name: filename= appears after name= in the
165 // header, so extracting it first avoids corrupting name='s search
166 // when extract_quoted_param null-terminates the value in-place.
167 part->filename = extract_quoted_param(v, "filename=");
168 part->name = extract_quoted_param(v, "name=");
169 }
170 else if (strncasecmp(pos, "Content-Type:", 13) == 0)
171 {
172 char *v = pos + 13;
173 while (*v == ' ')
174 {
175 v++;
176 }
177 part->type = v;
178 }
179
180 pos = line_end + 2; // next line (skip '\0' + '\n')
181 }
182
183 // Data runs from pos until the next "\r\n--boundary" (binary-safe, length-bounded).
184 char *next = mem_find(pos, (size_t)(end - pos), ddelim, ddlen);
185 if (!next)
186 {
187 return false;
188 }
189
190 part->data = pos;
191 part->data_len = (size_t)(next - pos);
192 *next = '\0'; // terminate at the CRLF so a text part is still usable as a C-string
193
194 mp->part_count++;
195
196 pos = next + ddlen; // past "\r\n--boundary"
197 if (pos + 2 <= end && pos[0] == '\r' && pos[1] == '\n')
198 {
199 pos += 2;
200 }
201 }
202
203 return mp->part_count > 0;
204}
205
206const char *pc_multipart_get_field(const Multipart *mp, const char *field)
207{
208 for (int i = 0; i < mp->part_count; i++)
209 {
210 if (mp->parts[i].name && strcmp(mp->parts[i].name, field) == 0)
211 {
212 return mp->parts[i].data;
213 }
214 }
215 return nullptr;
216}
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
#define PC_key_max
const char * pc_multipart_get_field(const Multipart *mp, const char *field)
Look up a field value across all parsed parts by name.
bool pc_multipart_parse(HttpReq *req, Multipart *mp)
Parse the body of req as multipart/form-data.
Definition multipart.cpp:56
In-place multipart/form-data parser (RFC 7578).
#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.
Fully-parsed HTTP/1.1 request.
uint8_t body[BODY_BUF_SIZE+1]
Stored body bytes, always null-terminated.
size_t body_len
Bytes stored in body[] (≤ BODY_BUF_SIZE).
One parsed part from a multipart body.
Definition multipart.h:47
const char * data
Part body (null-terminated in-place).
Definition multipart.h:51
const char * type
Content-Type of this part, or nullptr.
Definition multipart.h:50
size_t data_len
Part body length in bytes (not counting the null).
Definition multipart.h:52
const char * name
Form field name from Content-Disposition, or nullptr.
Definition multipart.h:48
const char * filename
Upload filename from Content-Disposition, or nullptr.
Definition multipart.h:49
Container for all parsed parts of a multipart body.
Definition multipart.h:59
MultipartPart parts[MAX_MULTIPART_PARTS]
Parsed parts.
Definition multipart.h:60
int part_count
Number of valid entries in parts[].
Definition multipart.h:61