DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
nats.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 nats.cpp
6 * @brief NATS client protocol builder + parser (pure, host-tested).
7 */
8
10
11#if DETWS_ENABLE_NATS
12
13#include <string.h>
14
15// A tiny bounded append cursor; sets ok=false on overflow and stops.
16struct Buf
17{
18 char *p;
19 size_t cap;
20 size_t pos;
21 bool ok;
22};
23
24static void put_str(Buf *b, const char *s)
25{
26 if (!b->ok || !s)
27 {
28 if (!s)
29 b->ok = false; // GCOVR_EXCL_LINE unreachable: every put_str arg is a literal or a checked/guarded non-null
30 // string
31 return;
32 }
33 size_t n = strnlen(s, b->cap + 1);
34 if (b->pos + n > b->cap)
35 {
36 b->ok = false;
37 return;
38 }
39 memcpy(b->p + b->pos, s, n);
40 b->pos += n;
41}
42
43static void put_bytes(Buf *b, const uint8_t *d, size_t n)
44{
45 if (!b->ok)
46 return;
47 if (b->pos + n > b->cap)
48 {
49 b->ok = false;
50 return;
51 }
52 if (n)
53 memcpy(b->p + b->pos, d, n);
54 b->pos += n;
55}
56
57static void put_ch(Buf *b, char c)
58{
59 if (!b->ok)
60 return;
61 if (b->pos + 1 > b->cap)
62 {
63 b->ok = false;
64 return;
65 }
66 b->p[b->pos++] = c;
67}
68
69static void put_uint(Buf *b, uint64_t v)
70{
71 char tmp[20];
72 size_t n = 0;
73 char rev[20];
74 size_t r = 0;
75 if (v == 0)
76 rev[r++] = '0';
77 while (v)
78 {
79 rev[r++] = (char)('0' + (int)(v % 10));
80 v /= 10;
81 }
82 while (r)
83 tmp[n++] = rev[--r];
84 put_bytes(b, (const uint8_t *)tmp, n);
85}
86
87static size_t finish(Buf *b)
88{
89 if (!b->ok)
90 return 0;
91 if (b->pos < b->cap) // NUL-terminate when there's room (the returned length excludes it)
92 b->p[b->pos] = '\0';
93 return b->pos;
94}
95
96size_t nats_build_connect(char *buf, size_t cap, const char *options_json)
97{
98 if (!buf || !options_json)
99 return 0;
100 Buf b = {buf, cap, 0, true};
101 put_str(&b, "CONNECT ");
102 put_str(&b, options_json);
103 put_str(&b, "\r\n");
104 return finish(&b);
105}
106
107size_t nats_build_pub(char *buf, size_t cap, const char *subject, const char *reply_to, const uint8_t *payload,
108 size_t payload_len)
109{
110 if (!buf || !subject || (payload_len && !payload))
111 return 0;
112 Buf b = {buf, cap, 0, true};
113 put_str(&b, "PUB ");
114 put_str(&b, subject);
115 if (reply_to)
116 {
117 put_ch(&b, ' ');
118 put_str(&b, reply_to);
119 }
120 put_ch(&b, ' ');
121 put_uint(&b, payload_len);
122 put_str(&b, "\r\n");
123 put_bytes(&b, payload, payload_len);
124 put_str(&b, "\r\n");
125 return finish(&b);
126}
127
128size_t nats_build_sub(char *buf, size_t cap, const char *subject, const char *queue, const char *sid)
129{
130 if (!buf || !subject || !sid)
131 return 0;
132 Buf b = {buf, cap, 0, true};
133 put_str(&b, "SUB ");
134 put_str(&b, subject);
135 if (queue)
136 {
137 put_ch(&b, ' ');
138 put_str(&b, queue);
139 }
140 put_ch(&b, ' ');
141 put_str(&b, sid);
142 put_str(&b, "\r\n");
143 return finish(&b);
144}
145
146size_t nats_build_unsub(char *buf, size_t cap, const char *sid, uint32_t max_msgs, bool with_max)
147{
148 if (!buf || !sid)
149 return 0;
150 Buf b = {buf, cap, 0, true};
151 put_str(&b, "UNSUB ");
152 put_str(&b, sid);
153 if (with_max)
154 {
155 put_ch(&b, ' ');
156 put_uint(&b, max_msgs);
157 }
158 put_str(&b, "\r\n");
159 return finish(&b);
160}
161
162size_t nats_build_ping(char *buf, size_t cap)
163{
164 Buf b = {buf, cap, 0, true};
165 put_str(&b, "PING\r\n");
166 return finish(&b);
167}
168
169size_t nats_build_pong(char *buf, size_t cap)
170{
171 Buf b = {buf, cap, 0, true};
172 put_str(&b, "PONG\r\n");
173 return finish(&b);
174}
175
176// Find the CRLF that ends the control line; returns the index of '\r', or len if absent.
177static size_t find_crlf(const char *buf, size_t len)
178{
179 for (size_t i = 0; i + 1 < len; i++)
180 if (buf[i] == '\r' && buf[i + 1] == '\n')
181 return i;
182 return len;
183}
184
185// Decimal parse of [s, s+n); false on a non-digit.
186static bool parse_uint(const char *s, size_t n, size_t *out)
187{
188 if (n == 0)
189 return false; // GCOVR_EXCL_LINE unreachable: the sole caller passes a space-delimited MSG token, always
190 // length>=1
191 size_t v = 0;
192 for (size_t i = 0; i < n; i++)
193 {
194 if (s[i] < '0' || s[i] > '9')
195 return false;
196 v = v * 10 + (size_t)(s[i] - '0');
197 }
198 *out = v;
199 return true;
200}
201
202// True when the verb token at buf matches op (and is followed by a space or end-of-token).
203static bool verb_is(const char *buf, size_t line_len, const char *op)
204{
205 size_t n = strnlen(op, line_len + 1);
206 if (line_len < n)
207 return false;
208 if (memcmp(buf, op, n) != 0)
209 return false;
210 return line_len == n || buf[n] == ' ' || buf[n] == '\t';
211}
212
213bool nats_parse(const char *buf, size_t len, NatsMsg *out, size_t *consumed)
214{
215 if (!buf || !out || !consumed)
216 return false;
217 size_t crlf = find_crlf(buf, len);
218 if (crlf == len)
219 return false; // control line not fully buffered
220 size_t line_len = crlf;
221 size_t after_line = crlf + 2;
222
223 out->subject = out->sid = out->reply = out->arg = nullptr;
224 out->subject_len = out->sid_len = out->reply_len = out->arg_len = 0;
225 out->payload = nullptr;
226 out->payload_len = 0;
227
228 if (verb_is(buf, line_len, "PING"))
229 {
230 out->type = NatsMsgType::NATS_PING;
231 *consumed = after_line;
232 return true;
233 }
234 if (verb_is(buf, line_len, "PONG"))
235 {
236 out->type = NatsMsgType::NATS_PONG;
237 *consumed = after_line;
238 return true;
239 }
240 if (verb_is(buf, line_len, "+OK"))
241 {
242 out->type = NatsMsgType::NATS_OK;
243 *consumed = after_line;
244 return true;
245 }
246 if (verb_is(buf, line_len, "-ERR") || verb_is(buf, line_len, "INFO"))
247 {
248 out->type = (buf[0] == '-') ? NatsMsgType::NATS_ERR : NatsMsgType::NATS_INFO;
249 size_t a = 4; // skip the verb: both "-ERR" and "INFO" are 4 chars
250 while (a < line_len && (buf[a] == ' ' || buf[a] == '\t'))
251 a++;
252 out->arg = buf + a;
253 out->arg_len = line_len - a;
254 *consumed = after_line;
255 return true;
256 }
257 if (verb_is(buf, line_len, "MSG"))
258 {
259 // MSG <subject> <sid> [reply-to] <#bytes>
260 const char *tok[4];
261 size_t tlen[4];
262 size_t ntok = 0;
263 size_t i = 3; // past "MSG"
264 while (i < line_len && ntok < 4)
265 {
266 while (i < line_len && (buf[i] == ' ' || buf[i] == '\t'))
267 i++;
268 if (i >= line_len)
269 break;
270 size_t start = i;
271 while (i < line_len && buf[i] != ' ' && buf[i] != '\t')
272 i++;
273 tok[ntok] = buf + start;
274 tlen[ntok] = i - start;
275 ntok++;
276 }
277 if (ntok != 3 && ntok != 4) // subject sid [reply] size
278 return false;
279 size_t size;
280 if (!parse_uint(tok[ntok - 1], tlen[ntok - 1], &size)) // the last token is the byte count
281 return false;
282 // Bound the byte count against the remaining capacity without adding it (a 32-bit
283 // size_t would wrap if we computed after_line + size + 2 first).
284 if (after_line + 2 > len || size > len - after_line - 2)
285 return false; // payload + trailing CRLF not fully buffered
286 size_t total = after_line + size + 2;
287 out->type = NatsMsgType::NATS_MSG;
288 out->subject = tok[0];
289 out->subject_len = tlen[0];
290 out->sid = tok[1];
291 out->sid_len = tlen[1];
292 if (ntok == 4)
293 {
294 out->reply = tok[2];
295 out->reply_len = tlen[2];
296 }
297 out->payload = (const uint8_t *)(buf + after_line);
298 out->payload_len = size;
299 *consumed = total;
300 return true;
301 }
302
303 out->type = NatsMsgType::NATS_UNKNOWN;
304 *consumed = after_line;
305 return true;
306}
307
308#endif // DETWS_ENABLE_NATS
NATS client protocol codec (DETWS_ENABLE_NATS) - zero-heap builder + parser for the text-based NATS p...