ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 PC_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 {
37 return false;
38 }
39 buf[(*pos)++] = c;
40 return true;
41 }
42 if (*pos + 2 > cap)
43 {
44 return false;
45 }
46 buf[(*pos)++] = e0;
47 buf[(*pos)++] = e1;
48 return true;
49}
50
51// Append the escaped form of a NUL-terminated string. Returns false on overflow.
52static bool emit_escaped_str(char *buf, size_t cap, size_t *pos, const char *s)
53{
54 for (; *s; s++)
55 {
56 if (!emit_escaped(buf, cap, pos, *s))
57 {
58 return false;
59 }
60 }
61 return true;
62}
63
64size_t pc_stomp_build_frame(char *buf, size_t cap, const char *command, const char *const *header_keys,
65 const char *const *header_vals, size_t nheaders, const char *body, size_t body_len)
66{
67 if (!buf || cap == 0 || !command || (nheaders && (!header_keys || !header_vals)))
68 {
69 return 0;
70 }
71
72 size_t pos = 0;
73
74 // Command line (a command verb has no special octets, but escape defensively).
75 if (!emit_escaped_str(buf, cap, &pos, command))
76 {
77 return 0;
78 }
79 if (pos + 1 > cap)
80 {
81 return 0;
82 }
83 buf[pos++] = '\n';
84
85 // Header lines: key:value\n, both escaped.
86 for (size_t i = 0; i < nheaders; i++)
87 {
88 if (!header_keys[i] || !header_vals[i])
89 {
90 return 0;
91 }
92 if (!emit_escaped_str(buf, cap, &pos, header_keys[i]))
93 {
94 return 0;
95 }
96 if (pos + 1 > cap)
97 {
98 return 0;
99 }
100 buf[pos++] = ':';
101 if (!emit_escaped_str(buf, cap, &pos, header_vals[i]))
102 {
103 return 0;
104 }
105 if (pos + 1 > cap)
106 {
107 return 0;
108 }
109 buf[pos++] = '\n';
110 }
111
112 // Blank line, raw body, terminating NUL.
113 if (pos + 1 > cap)
114 {
115 return 0;
116 }
117 buf[pos++] = '\n';
118 if (body_len)
119 {
120 if (!body || pos + body_len > cap)
121 {
122 return 0;
123 }
124 memcpy(buf + pos, body, body_len);
125 pos += body_len;
126 }
127 if (pos + 1 > cap)
128 {
129 return 0;
130 }
131 buf[pos++] = '\0';
132 return pos;
133}
134
135// Parse an unsigned base-10 length from [s, s+len). Returns false on empty / non-digit /
136// overflow (a content-length larger than size_t can never be satisfied by the buffer).
137static bool parse_len(const char *s, size_t len, size_t *out)
138{
139 if (len == 0)
140 {
141 return false;
142 }
143 size_t v = 0;
144 for (size_t i = 0; i < len; i++)
145 {
146 if (s[i] < '0' || s[i] > '9')
147 {
148 return false;
149 }
150 if (v > (SIZE_MAX - 9) / 10) // would overflow on the next digit
151 {
152 return false;
153 }
154 v = v * 10 + (size_t)(s[i] - '0');
155 }
156 *out = v;
157 return true;
158}
159
160// Length of a header line ending at the '\n' at index nl, trimming a trailing '\r'.
161static size_t line_len(const char *buf, size_t start, size_t nl)
162{
163 size_t end = nl;
164 if (end > start && buf[end - 1] == '\r')
165 {
166 end--;
167 }
168 return end - start;
169}
170
171bool pc_stomp_parse_frame(const char *buf, size_t len, StompFrame *out, size_t *consumed)
172{
173 if (!buf || !out || !consumed)
174 {
175 return false;
176 }
177
178 // Skip leading EOL octets (heart-beats / inter-frame newlines).
179 size_t i = 0;
180 while (i < len && (buf[i] == '\r' || buf[i] == '\n'))
181 {
182 i++;
183 }
184 if (i >= len)
185 {
186 return false; // nothing but newlines so far
187 }
188
189 out->command = nullptr;
190 out->command_len = 0;
191 out->header_count = 0;
192 out->body = nullptr;
193 out->body_len = 0;
194
195 // Command line.
196 size_t nl = i;
197 while (nl < len && buf[nl] != '\n')
198 {
199 nl++;
200 }
201 if (nl >= len)
202 {
203 return false; // command line incomplete
204 }
205 out->command = buf + i;
206 out->command_len = line_len(buf, i, nl);
207 if (out->command_len == 0) // GCOVR_EXCL_BR_LINE the true arm is unreachable: the skip loop above
208 // guarantees buf[i] is neither '\r' nor '\n', so the search for nl (the
209 // next '\n' at or after i) always advances past i, giving nl>=i+1. If
210 // line_len trims a trailing '\r' the trimmed length is only 0 when that
211 // '\r' is buf[i] itself (nl==i+1), which cannot happen since buf[i]!='\r'
212 // is already established => command_len>=1 in every case
213 {
214 return false; // GCOVR_EXCL_LINE unreachable for the same reason as the branch above
215 }
216 size_t cur = nl + 1;
217
218 // Header lines until a blank line.
219 size_t content_length = 0;
220 bool have_content_length = false;
221 while (cur < len)
222 {
223 nl = cur;
224 while (nl < len && buf[nl] != '\n')
225 {
226 nl++;
227 }
228 if (nl >= len)
229 {
230 return false; // header line incomplete
231 }
232 size_t ll = line_len(buf, cur, nl);
233 if (ll == 0)
234 {
235 cur = nl + 1; // blank line: body starts here
236 break;
237 }
238 // Split at the first ':'.
239 size_t colon = cur;
240 size_t line_end = cur + ll;
241 while (colon < line_end && buf[colon] != ':')
242 {
243 colon++;
244 }
245 if (colon >= line_end)
246 {
247 return false; // header without a colon
248 }
249 if (out->header_count < PC_STOMP_MAX_HEADERS)
250 {
251 StompHeader *h = &out->headers[out->header_count++];
252 h->key = buf + cur;
253 h->key_len = colon - cur;
254 h->val = buf + colon + 1;
255 h->val_len = line_end - (colon + 1);
256 // content-length drives the body length (only the first occurrence is used). A
257 // present-but-unparseable / overflowing value is a malformed frame, not a fall-back
258 // to NUL-delimited parsing.
259 if (!have_content_length && h->key_len == 14 && memcmp(h->key, "content-length", 14) == 0)
260 {
261 if (!parse_len(h->val, h->val_len, &content_length))
262 {
263 return false;
264 }
265 have_content_length = true;
266 }
267 }
268 cur = nl + 1;
269 if (cur > len) // GCOVR_EXCL_BR_LINE the true arm is unreachable: the nl>=len check above guarantees
270 // nl<len, so cur=nl+1<=len
271 {
272 return false; // GCOVR_EXCL_LINE unreachable for the same reason as the branch above
273 }
274 }
275
276 // Body.
277 if (have_content_length)
278 {
279 if (cur + content_length >= len) // need body + the terminating NUL
280 {
281 return false;
282 }
283 if (buf[cur + content_length] != '\0')
284 {
285 return false; // declared length does not land on the NUL terminator
286 }
287 out->body = buf + cur;
288 out->body_len = content_length;
289 *consumed = cur + content_length + 1;
290 return true;
291 }
292 // No content-length: body runs to the first NUL.
293 size_t b = cur;
294 while (b < len && buf[b] != '\0')
295 {
296 b++;
297 }
298 if (b >= len)
299 {
300 return false; // NUL terminator not yet buffered
301 }
302 out->body = buf + cur;
303 out->body_len = b - cur;
304 *consumed = b + 1;
305 return true;
306}
307
308bool pc_stomp_header(const StompFrame *f, const char *name, const char **val, size_t *val_len)
309{
310 if (!f || !name)
311 {
312 return false;
313 }
314#define PC_name_max 128 // STOMP header names are short; bound the needle defensively
315 size_t nlen = strnlen(name, PC_name_max);
316 for (size_t i = 0; i < f->header_count; i++)
317 {
318 if (f->headers[i].key_len == nlen && memcmp(f->headers[i].key, name, nlen) == 0)
319 {
320 if (val)
321 {
322 *val = f->headers[i].val;
323 }
324 if (val_len)
325 {
326 *val_len = f->headers[i].val_len;
327 }
328 return true;
329 }
330 }
331 return false;
332}
333
334size_t pc_stomp_unescape(char *dst, size_t cap, const char *src, size_t src_len)
335{
336 if (!dst || !src)
337 {
338 return 0;
339 }
340 size_t pos = 0;
341 for (size_t i = 0; i < src_len; i++)
342 {
343 char c = src[i];
344 if (c == '\\')
345 {
346 if (i + 1 >= src_len)
347 {
348 return 0; // dangling escape
349 }
350 char n = src[++i];
351 switch (n)
352 {
353 case 'r':
354 c = '\r';
355 break;
356 case 'n':
357 c = '\n';
358 break;
359 case 'c':
360 c = ':';
361 break;
362 case '\\':
363 c = '\\';
364 break;
365 default:
366 return 0; // invalid escape sequence
367 }
368 }
369 if (pos + 1 > cap)
370 {
371 return 0; // overflow
372 }
373 dst[pos++] = c;
374 }
375 return pos;
376}
377
378#endif // PC_ENABLE_STOMP
#define PC_STOMP_MAX_HEADERS
Max header lines parsed per STOMP frame (extras beyond this are ignored).
STOMP 1.2 frame codec (PC_ENABLE_STOMP) - zero-heap frame builder + parser, so a device can talk to a...