DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
stomp.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 stomp.cpp
6 * @brief STOMP 1.2 frame builder + parser (pure, host-tested).
7 */
8
10
11#if DETWS_ENABLE_STOMP
12
13#include <string.h>
14
15// Append one octet's escaped form to buf[pos..cap); advance pos. Returns false on overflow.
16static bool emit_escaped(char *buf, size_t cap, size_t *pos, char c)
17{
18 char e0 = '\\';
19 char e1;
20 switch (c)
21 {
22 case '\r':
23 e1 = 'r';
24 break;
25 case '\n':
26 e1 = 'n';
27 break;
28 case ':':
29 e1 = 'c';
30 break;
31 case '\\':
32 e1 = '\\';
33 break;
34 default: // not special: one raw octet
35 if (*pos + 1 > cap)
36 return false;
37 buf[(*pos)++] = c;
38 return true;
39 }
40 if (*pos + 2 > cap)
41 return false;
42 buf[(*pos)++] = e0;
43 buf[(*pos)++] = e1;
44 return true;
45}
46
47// Append the escaped form of a NUL-terminated string. Returns false on overflow.
48static bool emit_escaped_str(char *buf, size_t cap, size_t *pos, const char *s)
49{
50 for (; *s; s++)
51 if (!emit_escaped(buf, cap, pos, *s))
52 return false;
53 return true;
54}
55
56size_t stomp_build_frame(char *buf, size_t cap, const char *command, const char *const *header_keys,
57 const char *const *header_vals, size_t nheaders, const char *body, size_t body_len)
58{
59 if (!buf || cap == 0 || !command || (nheaders && (!header_keys || !header_vals)))
60 return 0;
61
62 size_t pos = 0;
63
64 // Command line (a command verb has no special octets, but escape defensively).
65 if (!emit_escaped_str(buf, cap, &pos, command))
66 return 0;
67 if (pos + 1 > cap)
68 return 0;
69 buf[pos++] = '\n';
70
71 // Header lines: key:value\n, both escaped.
72 for (size_t i = 0; i < nheaders; i++)
73 {
74 if (!header_keys[i] || !header_vals[i])
75 return 0;
76 if (!emit_escaped_str(buf, cap, &pos, header_keys[i]))
77 return 0;
78 if (pos + 1 > cap)
79 return 0;
80 buf[pos++] = ':';
81 if (!emit_escaped_str(buf, cap, &pos, header_vals[i]))
82 return 0;
83 if (pos + 1 > cap)
84 return 0;
85 buf[pos++] = '\n';
86 }
87
88 // Blank line, raw body, terminating NUL.
89 if (pos + 1 > cap)
90 return 0;
91 buf[pos++] = '\n';
92 if (body_len)
93 {
94 if (!body || pos + body_len > cap)
95 return 0;
96 memcpy(buf + pos, body, body_len);
97 pos += body_len;
98 }
99 if (pos + 1 > cap)
100 return 0;
101 buf[pos++] = '\0';
102 return pos;
103}
104
105// Parse an unsigned base-10 length from [s, s+len). Returns false on empty / non-digit /
106// overflow (a content-length larger than size_t can never be satisfied by the buffer).
107static bool parse_len(const char *s, size_t len, size_t *out)
108{
109 if (len == 0)
110 return false;
111 size_t v = 0;
112 for (size_t i = 0; i < len; i++)
113 {
114 if (s[i] < '0' || s[i] > '9')
115 return false;
116 if (v > (SIZE_MAX - 9) / 10) // would overflow on the next digit
117 return false;
118 v = v * 10 + (size_t)(s[i] - '0');
119 }
120 *out = v;
121 return true;
122}
123
124// Length of a header line ending at the '\n' at index nl, trimming a trailing '\r'.
125static size_t line_len(const char *buf, size_t start, size_t nl)
126{
127 size_t end = nl;
128 if (end > start && buf[end - 1] == '\r')
129 end--;
130 return end - start;
131}
132
133bool stomp_parse_frame(const char *buf, size_t len, StompFrame *out, size_t *consumed)
134{
135 if (!buf || !out || !consumed)
136 return false;
137
138 // Skip leading EOL octets (heart-beats / inter-frame newlines).
139 size_t i = 0;
140 while (i < len && (buf[i] == '\r' || buf[i] == '\n'))
141 i++;
142 if (i >= len)
143 return false; // nothing but newlines so far
144
145 out->command = nullptr;
146 out->command_len = 0;
147 out->header_count = 0;
148 out->body = nullptr;
149 out->body_len = 0;
150
151 // Command line.
152 size_t nl = i;
153 while (nl < len && buf[nl] != '\n')
154 nl++;
155 if (nl >= len)
156 return false; // command line incomplete
157 out->command = buf + i;
158 out->command_len = line_len(buf, i, nl);
159 if (out->command_len == 0)
160 return false; // GCOVR_EXCL_LINE unreachable: leading EOLs are skipped so buf[i] is non-newline =>
161 // command_len>=1
162 size_t cur = nl + 1;
163
164 // Header lines until a blank line.
165 size_t content_length = 0;
166 bool have_content_length = false;
167 while (cur < len)
168 {
169 nl = cur;
170 while (nl < len && buf[nl] != '\n')
171 nl++;
172 if (nl >= len)
173 return false; // header line incomplete
174 size_t ll = line_len(buf, cur, nl);
175 if (ll == 0)
176 {
177 cur = nl + 1; // blank line: body starts here
178 break;
179 }
180 // Split at the first ':'.
181 size_t colon = cur;
182 size_t line_end = cur + ll;
183 while (colon < line_end && buf[colon] != ':')
184 colon++;
185 if (colon >= line_end)
186 return false; // header without a colon
187 if (out->header_count < DETWS_STOMP_MAX_HEADERS)
188 {
189 StompHeader *h = &out->headers[out->header_count++];
190 h->key = buf + cur;
191 h->key_len = colon - cur;
192 h->val = buf + colon + 1;
193 h->val_len = line_end - (colon + 1);
194 // content-length drives the body length (only the first occurrence is used). A
195 // present-but-unparseable / overflowing value is a malformed frame, not a fall-back
196 // to NUL-delimited parsing.
197 if (!have_content_length && h->key_len == 14 && memcmp(h->key, "content-length", 14) == 0)
198 {
199 if (!parse_len(h->val, h->val_len, &content_length))
200 return false;
201 have_content_length = true;
202 }
203 }
204 cur = nl + 1;
205 if (cur > len)
206 return false; // GCOVR_EXCL_LINE unreachable: the nl>=len check above guarantees nl<len, so cur=nl+1<=len
207 }
208
209 // Body.
210 if (have_content_length)
211 {
212 if (cur + content_length >= len) // need body + the terminating NUL
213 return false;
214 if (buf[cur + content_length] != '\0')
215 return false; // declared length does not land on the NUL terminator
216 out->body = buf + cur;
217 out->body_len = content_length;
218 *consumed = cur + content_length + 1;
219 return true;
220 }
221 // No content-length: body runs to the first NUL.
222 size_t b = cur;
223 while (b < len && buf[b] != '\0')
224 b++;
225 if (b >= len)
226 return false; // NUL terminator not yet buffered
227 out->body = buf + cur;
228 out->body_len = b - cur;
229 *consumed = b + 1;
230 return true;
231}
232
233bool stomp_header(const StompFrame *f, const char *name, const char **val, size_t *val_len)
234{
235 if (!f || !name)
236 return false;
237 constexpr size_t name_max = 128; // STOMP header names are short; bound the needle defensively
238 size_t nlen = strnlen(name, name_max);
239 for (size_t i = 0; i < f->header_count; i++)
240 if (f->headers[i].key_len == nlen && memcmp(f->headers[i].key, name, nlen) == 0)
241 {
242 if (val)
243 *val = f->headers[i].val;
244 if (val_len)
245 *val_len = f->headers[i].val_len;
246 return true;
247 }
248 return false;
249}
250
251size_t stomp_unescape(char *dst, size_t cap, const char *src, size_t src_len)
252{
253 if (!dst || !src)
254 return 0;
255 size_t pos = 0;
256 for (size_t i = 0; i < src_len; i++)
257 {
258 char c = src[i];
259 if (c == '\\')
260 {
261 if (i + 1 >= src_len)
262 return 0; // dangling escape
263 char n = src[++i];
264 switch (n)
265 {
266 case 'r':
267 c = '\r';
268 break;
269 case 'n':
270 c = '\n';
271 break;
272 case 'c':
273 c = ':';
274 break;
275 case '\\':
276 c = '\\';
277 break;
278 default:
279 return 0; // invalid escape sequence
280 }
281 }
282 if (pos + 1 > cap)
283 return 0; // overflow
284 dst[pos++] = c;
285 }
286 return pos;
287}
288
289#endif // DETWS_ENABLE_STOMP
#define DETWS_STOMP_MAX_HEADERS
Max header lines parsed per STOMP frame (extras beyond this are ignored).
STOMP 1.2 frame codec (DETWS_ENABLE_STOMP) - zero-heap frame builder + parser, so a device can talk t...