ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
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 PC_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 {
24 tmp[t++] = '0';
25 }
26 while (n)
27 {
28 tmp[t++] = (char)('0' + (n % 10));
29 n /= 10;
30 }
31 if (*pos + 1u + (size_t)t + 2u >= cap)
32 {
33 return false;
34 }
35 buf[(*pos)++] = tag;
36 while (t)
37 {
38 buf[(*pos)++] = tmp[--t]; // reverse the digits
39 }
40 buf[(*pos)++] = '\r';
41 buf[(*pos)++] = '\n';
42 return true;
43}
44
45size_t pc_resp_encode_command(char *buf, size_t cap, const char *const *args, const size_t *arg_lens, size_t argc)
46{
47 if (!buf || cap == 0 || !args || argc == 0)
48 {
49 return 0;
50 }
51 size_t pos = 0;
52 if (!put_len_prefix(buf, cap, &pos, '*', argc))
53 {
54 return 0;
55 }
56
57 for (size_t i = 0; i < argc; i++)
58 {
59 if (!args[i])
60 {
61 return 0;
62 }
63 size_t alen = arg_lens ? arg_lens[i] : strnlen(args[i], cap);
64 if (!put_len_prefix(buf, cap, &pos, '$', alen))
65 {
66 return 0;
67 }
68 if (pos + alen + 2 >= cap) // arg body + trailing CRLF (reserve NUL room)
69 {
70 return 0;
71 }
72 memcpy(buf + pos, args[i], alen);
73 pos += alen;
74 buf[pos++] = '\r';
75 buf[pos++] = '\n';
76 }
77 buf[pos] = '\0';
78 return pos;
79}
80
81// Find the CRLF that ends the line starting at buf[from]; returns the index of the
82// '\r', or len if not found (incomplete line).
83static size_t find_crlf(const uint8_t *buf, size_t len, size_t from)
84{
85 for (size_t i = from; i + 1 < len; i++)
86 {
87 if (buf[i] == '\r' && buf[i + 1] == '\n')
88 {
89 return i;
90 }
91 }
92 return len;
93}
94
95// Parse a base-10 (optionally negative) integer from [buf+from, buf+end).
96static bool parse_int(const uint8_t *buf, size_t from, size_t end, int64_t *out)
97{
98 if (from >= end)
99 {
100 return false;
101 }
102 bool neg = false;
103 size_t i = from;
104 if (buf[i] == '-')
105 {
106 neg = true;
107 i++;
108 }
109 if (i >= end)
110 {
111 return false;
112 }
113 uint64_t v = 0; // accumulate unsigned: signed overflow (a huge digit run) is UB
114 for (; i < end; i++)
115 {
116 if (buf[i] < '0' || buf[i] > '9')
117 {
118 return false;
119 }
120 v = v * 10u + (uint64_t)(buf[i] - '0');
121 }
122 *out = neg ? (int64_t)(0ULL - v) : (int64_t)v; // two's-complement reinterpret, no negation UB
123 return true;
124}
125
126// Case-insensitively compare the slice [buf+from, buf+end) to the C string s.
127static bool slice_ieq(const uint8_t *buf, size_t from, size_t end, const char *s)
128{
129 for (size_t i = from; i < end; i++, s++)
130 {
131 if (*s == '\0')
132 {
133 return false;
134 }
135 uint8_t a = buf[i];
136 if (a >= 'A' && a <= 'Z')
137 {
138 a = (uint8_t)(a + 32);
139 }
140 char b = *s;
141 if (b >= 'A' && b <= 'Z') // GCOVR_EXCL_BR_LINE b is the pattern char; every caller (parse_double_special)
142 {
143 b = (char)(b + 32); // GCOVR_EXCL_LINE passes a lowercase literal ("inf"/"+inf"/"-inf"/"nan"), so
144 // this upper->lower fold of the pattern side never fires; slice_ieq is static
145 // (file-local) with no other caller, so no host test can reach it either.
146 }
147 if (a != (uint8_t)b)
148 {
149 return false;
150 }
151 }
152 return *s == '\0';
153}
154
155// RESP3 double special forms: inf / +inf / -inf / nan. Returns true (setting *out) if [from,end) is one,
156// else false with *out untouched.
157static bool parse_double_special(const uint8_t *buf, size_t from, size_t end, double *out)
158{
159 if (slice_ieq(buf, from, end, "inf") || slice_ieq(buf, from, end, "+inf"))
160 {
161 *out = 1e308 * 10.0;
162 return true;
163 }
164 if (slice_ieq(buf, from, end, "-inf"))
165 {
166 *out = -1e308 * 10.0;
167 return true;
168 }
169 if (slice_ieq(buf, from, end, "nan"))
170 {
171 double inf = 1e308 * 10.0; // +infinity by overflow (this file avoids <math.h>, cf. -inf above)
172 *out = inf * 0.0; // infinity * 0 = NaN, without an identical-operand division
173 return true;
174 }
175 return false;
176}
177
178// Parse an optional [ (e|E) [sign] digits ] exponent at *i (advancing it). A missing exponent leaves
179// *exp = 0 and returns true; an 'e' with no following digits returns false.
180static bool parse_exponent(const uint8_t *buf, size_t *i, size_t end, int *exp)
181{
182 *exp = 0;
183 if (!(*i < end && (buf[*i] == 'e' || buf[*i] == 'E')))
184 {
185 return true;
186 }
187 (*i)++;
188 bool eneg = false;
189 if (*i < end && (buf[*i] == '+' || buf[*i] == '-'))
190 {
191 eneg = (buf[*i] == '-');
192 (*i)++;
193 }
194 bool edig = false;
195 while (*i < end && buf[*i] >= '0' && buf[*i] <= '9')
196 {
197 if (*exp < 1000000) // clamp: a larger exponent saturates the double to inf/0 anyway
198 {
199 *exp = *exp * 10 + (buf[*i] - '0');
200 }
201 edig = true;
202 (*i)++;
203 }
204 if (!edig)
205 {
206 return false;
207 }
208 if (eneg)
209 {
210 *exp = -*exp;
211 }
212 return true;
213}
214
215// Best-effort decimal-to-double for a RESP3 double line: inf / -inf / nan, or
216// [sign] int [.frac] [(e|E)[sign]exp]. The raw text stays authoritative in str.
217static bool parse_double(const uint8_t *buf, size_t from, size_t end, double *out)
218{
219 if (parse_double_special(buf, from, end, out))
220 {
221 return true;
222 }
223
224 size_t i = from;
225 bool neg = false;
226 if (i < end && (buf[i] == '+' || buf[i] == '-'))
227 {
228 neg = (buf[i] == '-');
229 i++;
230 }
231 double mant = 0.0;
232 bool any = false;
233 for (; i < end && buf[i] >= '0' && buf[i] <= '9'; i++)
234 {
235 mant = mant * 10.0 + (buf[i] - '0');
236 any = true;
237 }
238 if (i < end && buf[i] == '.')
239 {
240 i++;
241 double scale = 0.1;
242 for (; i < end && buf[i] >= '0' && buf[i] <= '9'; i++)
243 {
244 mant += (buf[i] - '0') * scale;
245 scale *= 0.1;
246 any = true;
247 }
248 }
249 if (!any)
250 {
251 return false;
252 }
253 int exp = 0;
254 if (!parse_exponent(buf, &i, end, &exp))
255 {
256 return false;
257 }
258 if (i != end)
259 {
260 return false; // trailing garbage
261 }
262 double scale = 1.0;
263 int e = exp < 0 ? -exp : exp;
264 for (int k = 0; k < e; k++)
265 {
266 scale *= 10.0;
267 }
268 mant = (exp < 0) ? (mant / scale) : (mant * scale);
269 *out = neg ? -mant : mant;
270 return true;
271}
272
273// Length-prefixed body: bulk string ($), bulk error (!), verbatim string (=). Validates the declared
274// length against the buffer and its trailing CRLF; $-1 decodes to nil. type is buf[0].
275static bool parse_bulk_body(const uint8_t *buf, size_t len, uint8_t type, size_t header_from, size_t header_to,
276 size_t after_header, RespReply *out, size_t *consumed)
277{
278 int64_t blen;
279 if (!parse_int(buf, header_from, header_to, &blen))
280 {
281 return false;
282 }
283 if (type == '$' && blen < 0) // $-1 = nil
284 {
285 out->type = RespType::RESP_NIL;
286 *consumed = after_header;
287 return true;
288 }
289 if (blen < 0)
290 {
291 return false;
292 }
293 // Bound the length against the remaining capacity without adding it (a 32-bit
294 // size_t would wrap if we computed after_header + blen + 2 first).
295 if (after_header + 2 > len || (uint64_t)blen > (uint64_t)(len - after_header - 2))
296 {
297 return false; // body + trailing CRLF not fully buffered
298 }
299 size_t need = after_header + (size_t)blen + 2; // body + trailing CRLF
300 if (buf[after_header + (size_t)blen] != '\r' || buf[after_header + (size_t)blen + 1] != '\n')
301 {
302 return false; // malformed terminator
303 }
304 if (type == '$')
305 {
306 out->type = RespType::RESP_BULK;
307 }
308 else if (type == '!')
309 {
310 out->type = RespType::RESP_BULK_ERROR;
311 }
312 else
313 {
314 out->type = RespType::RESP_VERBATIM;
315 }
316 out->str = (const char *)(buf + after_header);
317 out->str_len = (size_t)blen;
318 *consumed = need;
319 return true;
320}
321
322// Aggregate header whose children follow: array (*), set (~), push (>). Only the count is read here; the
323// caller parses each element next. *-1 decodes to nil. type is buf[0].
324static bool parse_aggregate(const uint8_t *buf, uint8_t type, size_t header_from, size_t header_to, size_t after_header,
325 RespReply *out, size_t *consumed)
326{
327 int64_t n;
328 if (!parse_int(buf, header_from, header_to, &n))
329 {
330 return false;
331 }
332 if (type == '*' && n < 0) // *-1 = nil array
333 {
334 out->type = RespType::RESP_NIL;
335 *consumed = after_header;
336 return true;
337 }
338 if (n < 0)
339 {
340 return false;
341 }
342 if (type == '*')
343 {
344 out->type = RespType::RESP_ARRAY;
345 }
346 else if (type == '~')
347 {
348 out->type = RespType::RESP_SET;
349 }
350 else
351 {
352 out->type = RespType::RESP_PUSH;
353 }
354 out->ival = n;
355 out->count = n;
356 *consumed = after_header; // header only; the caller parses each element next
357 return true;
358}
359
360bool pc_resp_parse(const uint8_t *buf, size_t len, RespReply *out, size_t *consumed)
361{
362 if (!buf || len < 3 || !out || !consumed) // shortest value is "+\r\n" style; need a type + CRLF
363 {
364 return false;
365 }
366
367 size_t crlf = find_crlf(buf, len, 1);
368 if (crlf == len)
369 {
370 return false; // header line incomplete
371 }
372 size_t header_from = 1;
373 size_t header_to = crlf;
374 size_t after_header = crlf + 2; // past \r\n
375
376 out->str = nullptr;
377 out->str_len = 0;
378 out->ival = 0;
379 out->dval = 0;
380 out->count = 0;
381
382 switch (buf[0])
383 {
384 case '+':
385 case '-':
386 out->type = (buf[0] == '+') ? RespType::RESP_SIMPLE : RespType::RESP_ERROR;
387 out->str = (const char *)(buf + header_from);
388 out->str_len = header_to - header_from;
389 *consumed = after_header;
390 return true;
391
392 case ':': {
393 int64_t v;
394 if (!parse_int(buf, header_from, header_to, &v))
395 {
396 return false;
397 }
398 out->type = RespType::RESP_INTEGER;
399 out->ival = v;
400 *consumed = after_header;
401 return true;
402 }
403
404 // Length-prefixed bodies: bulk string ($), bulk error (!), verbatim string (=).
405 case '$':
406 case '!':
407 case '=':
408 return parse_bulk_body(buf, len, buf[0], header_from, header_to, after_header, out, consumed);
409
410 // Aggregates whose children follow: array (*), set (~), push (>).
411 case '*':
412 case '~':
413 case '>':
414 return parse_aggregate(buf, buf[0], header_from, header_to, after_header, out, consumed);
415
416 case '%': { // map: N pairs -> 2N following child values
417 int64_t n;
418 if (!parse_int(buf, header_from, header_to, &n) || n < 0)
419 {
420 return false;
421 }
422 out->type = RespType::RESP_MAP;
423 out->ival = n * 2;
424 out->count = n * 2;
425 *consumed = after_header;
426 return true;
427 }
428
429 case '_': // RESP3 null
430 out->type = RespType::RESP_NIL;
431 *consumed = after_header;
432 return true;
433
434 case '#': { // boolean
435 if (header_to - header_from != 1 || (buf[header_from] != 't' && buf[header_from] != 'f'))
436 {
437 return false;
438 }
439 out->type = RespType::RESP_BOOL;
440 out->ival = (buf[header_from] == 't') ? 1 : 0;
441 *consumed = after_header;
442 return true;
443 }
444
445 case ',': // double (text authoritative; dval best-effort)
446 out->type = RespType::RESP_DOUBLE;
447 out->str = (const char *)(buf + header_from);
448 out->str_len = header_to - header_from;
449 parse_double(buf, header_from, header_to, &out->dval);
450 *consumed = after_header;
451 return true;
452
453 case '(': // big number (digits kept as text)
454 out->type = RespType::RESP_BIG_NUMBER;
455 out->str = (const char *)(buf + header_from);
456 out->str_len = header_to - header_from;
457 *consumed = after_header;
458 return true;
459
460 default:
461 return false; // unknown type byte
462 }
463}
464
465#endif // PC_ENABLE_REDIS
Redis RESP2/RESP3 wire codec (PC_ENABLE_REDIS) - zero-heap command encoder + reply parser,...