DeterministicESPAsyncWebServer v6.28.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
redis_resp.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 redis_resp.cpp
6 * @brief Redis RESP2 command encoder + reply parser (pure, host-tested).
7 */
8
10
11#if DETWS_ENABLE_REDIS
12
13#include <string.h>
14
15// Write a "<tag><decimal>\r\n" length prefix into buf at *pos, advancing it. A hand-rolled decimal is
16// several times faster than snprintf on the ESP32-S3, where this was the dominant cost of encoding a
17// command (docs/FEATURE_PERFORMANCE.md section 4). Reserves a trailing byte for the final NUL, as before.
18static bool put_len_prefix(char *buf, size_t cap, size_t *pos, char tag, size_t n)
19{
20 char tmp[20];
21 int t = 0;
22 if (n == 0)
23 tmp[t++] = '0';
24 while (n)
25 {
26 tmp[t++] = (char)('0' + (n % 10));
27 n /= 10;
28 }
29 if (*pos + 1u + (size_t)t + 2u >= cap)
30 return false;
31 buf[(*pos)++] = tag;
32 while (t)
33 buf[(*pos)++] = tmp[--t]; // reverse the digits
34 buf[(*pos)++] = '\r';
35 buf[(*pos)++] = '\n';
36 return true;
37}
38
39size_t resp_encode_command(char *buf, size_t cap, const char *const *args, const size_t *arg_lens, size_t argc)
40{
41 if (!buf || cap == 0 || !args || argc == 0)
42 return 0;
43 size_t pos = 0;
44 if (!put_len_prefix(buf, cap, &pos, '*', argc))
45 return 0;
46
47 for (size_t i = 0; i < argc; i++)
48 {
49 if (!args[i])
50 return 0;
51 size_t alen = arg_lens ? arg_lens[i] : strnlen(args[i], cap);
52 if (!put_len_prefix(buf, cap, &pos, '$', alen))
53 return 0;
54 if (pos + alen + 2 >= cap) // arg body + trailing CRLF (reserve NUL room)
55 return 0;
56 memcpy(buf + pos, args[i], alen);
57 pos += alen;
58 buf[pos++] = '\r';
59 buf[pos++] = '\n';
60 }
61 buf[pos] = '\0';
62 return pos;
63}
64
65// Find the CRLF that ends the line starting at buf[from]; returns the index of the
66// '\r', or len if not found (incomplete line).
67static size_t find_crlf(const uint8_t *buf, size_t len, size_t from)
68{
69 for (size_t i = from; i + 1 < len; i++)
70 if (buf[i] == '\r' && buf[i + 1] == '\n')
71 return i;
72 return len;
73}
74
75// Parse a base-10 (optionally negative) integer from [buf+from, buf+end).
76static bool parse_int(const uint8_t *buf, size_t from, size_t end, int64_t *out)
77{
78 if (from >= end)
79 return false;
80 bool neg = false;
81 size_t i = from;
82 if (buf[i] == '-')
83 {
84 neg = true;
85 i++;
86 }
87 if (i >= end)
88 return false;
89 uint64_t v = 0; // accumulate unsigned: signed overflow (a huge digit run) is UB
90 for (; i < end; i++)
91 {
92 if (buf[i] < '0' || buf[i] > '9')
93 return false;
94 v = v * 10u + (uint64_t)(buf[i] - '0');
95 }
96 *out = neg ? (int64_t)(0ULL - v) : (int64_t)v; // two's-complement reinterpret, no negation UB
97 return true;
98}
99
100// Case-insensitively compare the slice [buf+from, buf+end) to the C string s.
101static bool slice_ieq(const uint8_t *buf, size_t from, size_t end, const char *s)
102{
103 for (size_t i = from; i < end; i++, s++)
104 {
105 if (*s == '\0')
106 return false;
107 uint8_t a = buf[i];
108 if (a >= 'A' && a <= 'Z')
109 a = (uint8_t)(a + 32);
110 char b = *s;
111 if (b >= 'A' && b <= 'Z')
112 b = (char)(b + 32);
113 if (a != (uint8_t)b)
114 return false;
115 }
116 return *s == '\0';
117}
118
119// RESP3 double special forms: inf / +inf / -inf / nan. Returns true (setting *out) if [from,end) is one,
120// else false with *out untouched.
121static bool parse_double_special(const uint8_t *buf, size_t from, size_t end, double *out)
122{
123 if (slice_ieq(buf, from, end, "inf") || slice_ieq(buf, from, end, "+inf"))
124 {
125 *out = 1e308 * 10.0;
126 return true;
127 }
128 if (slice_ieq(buf, from, end, "-inf"))
129 {
130 *out = -1e308 * 10.0;
131 return true;
132 }
133 if (slice_ieq(buf, from, end, "nan"))
134 {
135 double inf = 1e308 * 10.0; // +infinity by overflow (this file avoids <math.h>, cf. -inf above)
136 *out = inf * 0.0; // infinity * 0 = NaN, without an identical-operand division
137 return true;
138 }
139 return false;
140}
141
142// Parse an optional [ (e|E) [sign] digits ] exponent at *i (advancing it). A missing exponent leaves
143// *exp = 0 and returns true; an 'e' with no following digits returns false.
144static bool parse_exponent(const uint8_t *buf, size_t *i, size_t end, int *exp)
145{
146 *exp = 0;
147 if (!(*i < end && (buf[*i] == 'e' || buf[*i] == 'E')))
148 return true;
149 (*i)++;
150 bool eneg = false;
151 if (*i < end && (buf[*i] == '+' || buf[*i] == '-'))
152 {
153 eneg = (buf[*i] == '-');
154 (*i)++;
155 }
156 bool edig = false;
157 while (*i < end && buf[*i] >= '0' && buf[*i] <= '9')
158 {
159 if (*exp < 1000000) // clamp: a larger exponent saturates the double to inf/0 anyway
160 *exp = *exp * 10 + (buf[*i] - '0');
161 edig = true;
162 (*i)++;
163 }
164 if (!edig)
165 return false;
166 if (eneg)
167 *exp = -*exp;
168 return true;
169}
170
171// Best-effort decimal-to-double for a RESP3 double line: inf / -inf / nan, or
172// [sign] int [.frac] [(e|E)[sign]exp]. The raw text stays authoritative in str.
173static bool parse_double(const uint8_t *buf, size_t from, size_t end, double *out)
174{
175 if (parse_double_special(buf, from, end, out))
176 return true;
177
178 size_t i = from;
179 bool neg = false;
180 if (i < end && (buf[i] == '+' || buf[i] == '-'))
181 {
182 neg = (buf[i] == '-');
183 i++;
184 }
185 double mant = 0.0;
186 bool any = false;
187 for (; i < end && buf[i] >= '0' && buf[i] <= '9'; i++)
188 {
189 mant = mant * 10.0 + (buf[i] - '0');
190 any = true;
191 }
192 if (i < end && buf[i] == '.')
193 {
194 i++;
195 double scale = 0.1;
196 for (; i < end && buf[i] >= '0' && buf[i] <= '9'; i++)
197 {
198 mant += (buf[i] - '0') * scale;
199 scale *= 0.1;
200 any = true;
201 }
202 }
203 if (!any)
204 return false;
205 int exp = 0;
206 if (!parse_exponent(buf, &i, end, &exp))
207 return false;
208 if (i != end)
209 return false; // trailing garbage
210 double scale = 1.0;
211 int e = exp < 0 ? -exp : exp;
212 for (int k = 0; k < e; k++)
213 scale *= 10.0;
214 mant = (exp < 0) ? (mant / scale) : (mant * scale);
215 *out = neg ? -mant : mant;
216 return true;
217}
218
219// Length-prefixed body: bulk string ($), bulk error (!), verbatim string (=). Validates the declared
220// length against the buffer and its trailing CRLF; $-1 decodes to nil. type is buf[0].
221static bool parse_bulk_body(const uint8_t *buf, size_t len, uint8_t type, size_t header_from, size_t header_to,
222 size_t after_header, RespReply *out, size_t *consumed)
223{
224 int64_t blen;
225 if (!parse_int(buf, header_from, header_to, &blen))
226 return false;
227 if (type == '$' && blen < 0) // $-1 = nil
228 {
229 out->type = RespType::RESP_NIL;
230 *consumed = after_header;
231 return true;
232 }
233 if (blen < 0)
234 return false;
235 // Bound the length against the remaining capacity without adding it (a 32-bit
236 // size_t would wrap if we computed after_header + blen + 2 first).
237 if (after_header + 2 > len || (uint64_t)blen > (uint64_t)(len - after_header - 2))
238 return false; // body + trailing CRLF not fully buffered
239 size_t need = after_header + (size_t)blen + 2; // body + trailing CRLF
240 if (buf[after_header + (size_t)blen] != '\r' || buf[after_header + (size_t)blen + 1] != '\n')
241 return false; // malformed terminator
242 if (type == '$')
243 out->type = RespType::RESP_BULK;
244 else if (type == '!')
245 out->type = RespType::RESP_BULK_ERROR;
246 else
247 out->type = RespType::RESP_VERBATIM;
248 out->str = (const char *)(buf + after_header);
249 out->str_len = (size_t)blen;
250 *consumed = need;
251 return true;
252}
253
254// Aggregate header whose children follow: array (*), set (~), push (>). Only the count is read here; the
255// caller parses each element next. *-1 decodes to nil. type is buf[0].
256static bool parse_aggregate(const uint8_t *buf, uint8_t type, size_t header_from, size_t header_to, size_t after_header,
257 RespReply *out, size_t *consumed)
258{
259 int64_t n;
260 if (!parse_int(buf, header_from, header_to, &n))
261 return false;
262 if (type == '*' && n < 0) // *-1 = nil array
263 {
264 out->type = RespType::RESP_NIL;
265 *consumed = after_header;
266 return true;
267 }
268 if (n < 0)
269 return false;
270 if (type == '*')
271 out->type = RespType::RESP_ARRAY;
272 else if (type == '~')
273 out->type = RespType::RESP_SET;
274 else
275 out->type = RespType::RESP_PUSH;
276 out->ival = n;
277 out->count = n;
278 *consumed = after_header; // header only; the caller parses each element next
279 return true;
280}
281
282bool resp_parse(const uint8_t *buf, size_t len, RespReply *out, size_t *consumed)
283{
284 if (!buf || len < 3 || !out || !consumed) // shortest value is "+\r\n" style; need a type + CRLF
285 return false;
286
287 size_t crlf = find_crlf(buf, len, 1);
288 if (crlf == len)
289 return false; // header line incomplete
290 size_t header_from = 1;
291 size_t header_to = crlf;
292 size_t after_header = crlf + 2; // past \r\n
293
294 out->str = nullptr;
295 out->str_len = 0;
296 out->ival = 0;
297 out->dval = 0;
298 out->count = 0;
299
300 switch (buf[0])
301 {
302 case '+':
303 case '-':
304 out->type = (buf[0] == '+') ? RespType::RESP_SIMPLE : RespType::RESP_ERROR;
305 out->str = (const char *)(buf + header_from);
306 out->str_len = header_to - header_from;
307 *consumed = after_header;
308 return true;
309
310 case ':': {
311 int64_t v;
312 if (!parse_int(buf, header_from, header_to, &v))
313 return false;
314 out->type = RespType::RESP_INTEGER;
315 out->ival = v;
316 *consumed = after_header;
317 return true;
318 }
319
320 // Length-prefixed bodies: bulk string ($), bulk error (!), verbatim string (=).
321 case '$':
322 case '!':
323 case '=':
324 return parse_bulk_body(buf, len, buf[0], header_from, header_to, after_header, out, consumed);
325
326 // Aggregates whose children follow: array (*), set (~), push (>).
327 case '*':
328 case '~':
329 case '>':
330 return parse_aggregate(buf, buf[0], header_from, header_to, after_header, out, consumed);
331
332 case '%': { // map: N pairs -> 2N following child values
333 int64_t n;
334 if (!parse_int(buf, header_from, header_to, &n) || n < 0)
335 return false;
336 out->type = RespType::RESP_MAP;
337 out->ival = n * 2;
338 out->count = n * 2;
339 *consumed = after_header;
340 return true;
341 }
342
343 case '_': // RESP3 null
344 out->type = RespType::RESP_NIL;
345 *consumed = after_header;
346 return true;
347
348 case '#': { // boolean
349 if (header_to - header_from != 1 || (buf[header_from] != 't' && buf[header_from] != 'f'))
350 return false;
351 out->type = RespType::RESP_BOOL;
352 out->ival = (buf[header_from] == 't') ? 1 : 0;
353 *consumed = after_header;
354 return true;
355 }
356
357 case ',': // double (text authoritative; dval best-effort)
358 out->type = RespType::RESP_DOUBLE;
359 out->str = (const char *)(buf + header_from);
360 out->str_len = header_to - header_from;
361 parse_double(buf, header_from, header_to, &out->dval);
362 *consumed = after_header;
363 return true;
364
365 case '(': // big number (digits kept as text)
366 out->type = RespType::RESP_BIG_NUMBER;
367 out->str = (const char *)(buf + header_from);
368 out->str_len = header_to - header_from;
369 *consumed = after_header;
370 return true;
371
372 default:
373 return false; // unknown type byte
374 }
375}
376
377#endif // DETWS_ENABLE_REDIS
Redis RESP2/RESP3 wire codec (DETWS_ENABLE_REDIS) - zero-heap command encoder + reply parser,...