DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
json.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 json.cpp
6 * @brief Implementation of the zero-heap JSON writer and top-level reader.
7 */
8
9#include "json.h"
11#include <stdio.h>
12#include <string.h>
13
14// ---------------------------------------------------------------------------
15// JsonWriter
16// ---------------------------------------------------------------------------
17
18JsonWriter::JsonWriter(char *buf, size_t cap)
19 : _buf(buf), _cap(cap), _len(0), _ok(buf != nullptr && cap >= 1), _after_key(false), _depth(0)
20{
21 if (_ok)
22 _buf[0] = '\0';
23}
24
25void JsonWriter::put(char c)
26{
27 if (!_ok)
28 return;
29 if (_len + 1 >= _cap) // leave room for the NUL
30 {
31 _ok = false;
32 return;
33 }
34 _buf[_len++] = c;
35 _buf[_len] = '\0';
36}
37
38void JsonWriter::put_raw(const char *s)
39{
40 if (!s)
41 return;
42 for (; *s; s++)
43 put(*s);
44}
45
46void JsonWriter::put_escaped(const char *s)
47{
48 if (!s)
49 return;
50 for (; *s; s++)
51 {
52 unsigned char c = (unsigned char)*s;
53 switch (c)
54 {
55 case '"':
56 put('\\');
57 put('"');
58 break;
59 case '\\':
60 put('\\');
61 put('\\');
62 break;
63 case '\n':
64 put('\\');
65 put('n');
66 break;
67 case '\r':
68 put('\\');
69 put('r');
70 break;
71 case '\t':
72 put('\\');
73 put('t');
74 break;
75 case '\b':
76 put('\\');
77 put('b');
78 break;
79 case '\f':
80 put('\\');
81 put('f');
82 break;
83 default:
84 if (c < 0x20)
85 {
86 // Control char -> \u00XX
87 static const char hexd[] = "0123456789abcdef";
88 put('\\');
89 put('u');
90 put('0');
91 put('0');
92 put(hexd[(c >> 4) & 0x0f]);
93 put(hexd[c & 0x0f]);
94 }
95 else
96 {
97 put((char)c);
98 }
99 break;
100 }
101 }
102}
103
104void JsonWriter::value_prefix()
105{
106 if (_after_key)
107 {
108 _after_key = false; // a value right after key(): the comma was its own
109 return;
110 }
111 if (_depth > 0)
112 {
113 size_t lvl = (size_t)(_depth - 1);
114 if (_need_comma[lvl])
115 put(',');
116 _need_comma[lvl] = true;
117 }
118}
119
120void JsonWriter::push(char open)
121{
122 value_prefix();
123 put(open);
124 if (_depth < JSON_MAX_DEPTH)
125 {
126 _need_comma[_depth] = false;
127 _depth++;
128 }
129 else
130 {
131 _ok = false; // nesting too deep
132 }
133}
134
135void JsonWriter::pop(char close)
136{
137 put(close);
138 if (_depth > 0)
139 _depth--;
140 else
141 _ok = false; // unbalanced close
142}
143
145{
146 push('{');
147}
149{
150 pop('}');
151}
153{
154 push('[');
155}
157{
158 pop(']');
159}
160
161void JsonWriter::key(const char *k)
162{
163 value_prefix();
164 put('"');
165 put_escaped(k);
166 put('"');
167 put(':');
168 _after_key = true; // suppress the following value's own comma
169}
170
171void JsonWriter::str(const char *v)
172{
173 value_prefix();
174 put('"');
175 put_escaped(v);
176 put('"');
177}
178
180{
181 char tmp[24];
182 snprintf(tmp, sizeof(tmp), "%ld", v);
183 value_prefix();
184 put_raw(tmp);
185}
186
187void JsonWriter::uinteger(unsigned long v)
188{
189 char tmp[24];
190 snprintf(tmp, sizeof(tmp), "%lu", v);
191 value_prefix();
192 put_raw(tmp);
193}
194
196{
197 value_prefix();
198 put_raw(v ? "true" : "false");
199}
200
202{
203 value_prefix();
204 put_raw("null");
205}
206
207void JsonWriter::raw(const char *literal)
208{
209 value_prefix();
210 put_raw(literal);
211}
212
213void JsonWriter::kv_str(const char *k, const char *v)
214{
215 key(k);
216 str(v);
217}
218void JsonWriter::kv_int(const char *k, long v)
219{
220 key(k);
221 integer(v);
222}
223void JsonWriter::kv_uint(const char *k, unsigned long v)
224{
225 key(k);
226 uinteger(v);
227}
228void JsonWriter::kv_bool(const char *k, bool v)
229{
230 key(k);
231 boolean(v);
232}
233void JsonWriter::kv_null(const char *k)
234{
235 key(k);
236 null_value();
237}
238void JsonWriter::kv_raw(const char *k, const char *literal)
239{
240 key(k);
241 raw(literal);
242}
243
244// ---------------------------------------------------------------------------
245// Reader (top-level object members)
246// ---------------------------------------------------------------------------
247
248static bool is_ws(char c)
249{
250 return c == ' ' || c == '\t' || c == '\n' || c == '\r';
251}
252
253static const char *skip_ws(const char *p)
254{
255 while (*p && is_ws(*p))
256 p++;
257 return p;
258}
259
260// p points at the opening quote; returns the pointer just past the closing quote
261// (or at the terminating NUL if unterminated). Honors backslash escapes.
262static const char *skip_string(const char *p)
263{
264 p++; // opening quote
265 while (*p)
266 {
267 if (*p == '\\' && p[1])
268 {
269 p += 2;
270 continue;
271 }
272 if (*p == '"')
273 return p + 1;
274 p++;
275 }
276 return p;
277}
278
279// Skip one JSON value starting at p (ws already consumed). Returns the pointer
280// just past the value.
281static const char *skip_value(const char *p)
282{
283 if (*p == '"')
284 return skip_string(p);
285 if (*p == '{' || *p == '[')
286 {
287 char open = *p;
288 char close = (open == '{') ? '}' : ']';
289 int depth = 0;
290 while (*p)
291 {
292 if (*p == '"')
293 {
294 p = skip_string(p);
295 continue;
296 }
297 if (*p == open)
298 depth++;
299 else if (*p == close)
300 {
301 depth--;
302 if (depth == 0)
303 return p + 1;
304 }
305 p++;
306 }
307 return p;
308 }
309 // primitive: number / true / false / null
310 while (*p && *p != ',' && *p != '}' && *p != ']' && !is_ws(*p))
311 p++;
312 return p;
313}
314
315// Locate the value of a top-level @p key in object @p json. Returns a pointer to
316// the first character of the value (ws-skipped), or nullptr if not found.
317static const char *json_find_value(const char *json, const char *key)
318{
319 if (!json || !key)
320 return nullptr;
321 const char *p = skip_ws(json);
322 if (*p != '{')
323 return nullptr;
324 p++; // into the object
325
326 constexpr size_t key_max = 256; // member names the server looks up are short; bound the needle defensively
327 size_t keylen = strnlen(key, key_max);
328 while (true)
329 {
330 p = skip_ws(p);
331 if (*p == '}' || *p == '\0')
332 return nullptr;
333 if (*p != '"')
334 return nullptr; // expected a member name
335
336 const char *kstart = p + 1;
337 const char *kend = skip_string(p); // just past closing quote
338 size_t klen = (kend > kstart) ? (size_t)((kend - 1) - kstart) : 0;
339 bool match = (klen == keylen) && (strncmp(kstart, key, klen) == 0);
340
341 p = skip_ws(kend);
342 if (*p != ':')
343 return nullptr;
344 p = skip_ws(p + 1);
345
346 if (match)
347 return p;
348
349 p = skip_value(p);
350 p = skip_ws(p);
351 if (*p == ',')
352 {
353 p++;
354 continue;
355 }
356 return nullptr; // '}' or malformed
357 }
358}
359
360static int hex_val(char c)
361{
362 if (c >= '0' && c <= '9')
363 return c - '0';
364 if (c >= 'a' && c <= 'f')
365 return c - 'a' + 10;
366 if (c >= 'A' && c <= 'F')
367 return c - 'A' + 10;
368 return -1;
369}
370
371enum class JsonEsc
372{
373 LITERAL_C, // *c_out holds one char for the caller's common write path
374 EMITTED, // UTF-8 bytes already written to out; caller advances p and continues
375 TRUNCATED // sequence would overflow out_cap; caller returns
376};
377
378// Decode a \uXXXX sequence with p at the 'u'. A high surrogate (0xD800..0xDBFF) followed by a low
379// surrogate combines into one code point (0x10000..0x10FFFF); an unpaired/lone surrogate becomes
380// U+FFFD. On success returns the code point and leaves p on the last consumed byte. Returns -1 for
381// malformed / short hex, leaving p unchanged so the caller emits a literal '?'.
382// Read the four hex digits at src[0..3] into a 16-bit value; -1 if any is absent or non-hex.
383static int json_hex4(const char *src)
384{
385 int h0 = src[0] ? hex_val(src[0]) : -1;
386 int h1 = (h0 >= 0 && src[1]) ? hex_val(src[1]) : -1;
387 int h2 = (h1 >= 0 && src[2]) ? hex_val(src[2]) : -1;
388 int h3 = (h2 >= 0 && src[3]) ? hex_val(src[3]) : -1;
389 if (h3 < 0)
390 return -1;
391 return (h0 << 12) | (h1 << 8) | (h2 << 4) | h3;
392}
393
394static long json_decode_u(const char *&p)
395{
396 int v = json_hex4(p + 1); // p at 'u'; the four hex digits are p[1..4]
397 if (v < 0)
398 return -1;
399 unsigned cp = (unsigned)v;
400 p += 4; // consume the four hex digits (p now at the last one)
401 if (cp >= 0xD800 && cp <= 0xDBFF)
402 {
403 // A high surrogate pairs with a following \uXXXX low surrogate (0xDC00..0xDFFF).
404 int lo = (p[1] == '\\' && p[2] == 'u') ? json_hex4(p + 3) : -1;
405 if (lo >= 0xDC00 && lo <= 0xDFFF)
406 {
407 cp = 0x10000u + ((cp - 0xD800u) << 10) + ((unsigned)lo - 0xDC00u);
408 p += 6; // consume the low surrogate's \uXXXX too
409 }
410 else
411 cp = 0xFFFDu; // unpaired high surrogate
412 }
413 else if (cp >= 0xDC00 && cp <= 0xDFFF)
414 {
415 cp = 0xFFFDu; // lone low surrogate
416 }
417 return (long)cp;
418}
419
420// Encode code point cp as UTF-8 into out at *i (bounded by out_cap): <= 0x7F one byte, then 2/3/4
421// bytes. Returns EMITTED on success, or TRUNCATED (writing a NUL) if the whole sequence will not fit.
422static JsonEsc json_emit_utf8(unsigned cp, char *out, size_t &i, size_t out_cap)
423{
424 unsigned char u8[4];
425 int un;
426 if (cp < 0x80u)
427 {
428 u8[0] = (unsigned char)cp;
429 un = 1;
430 }
431 else if (cp < 0x800u)
432 {
433 u8[0] = (unsigned char)(0xC0u | (cp >> 6));
434 u8[1] = (unsigned char)(0x80u | (cp & 0x3Fu));
435 un = 2;
436 }
437 else if (cp < 0x10000u)
438 {
439 u8[0] = (unsigned char)(0xE0u | (cp >> 12));
440 u8[1] = (unsigned char)(0x80u | ((cp >> 6) & 0x3Fu));
441 u8[2] = (unsigned char)(0x80u | (cp & 0x3Fu));
442 un = 3;
443 }
444 else
445 {
446 u8[0] = (unsigned char)(0xF0u | (cp >> 18));
447 u8[1] = (unsigned char)(0x80u | ((cp >> 12) & 0x3Fu));
448 u8[2] = (unsigned char)(0x80u | ((cp >> 6) & 0x3Fu));
449 u8[3] = (unsigned char)(0x80u | (cp & 0x3Fu));
450 un = 4;
451 }
452 if (i + (size_t)un >= out_cap)
453 {
454 out[i] = '\0'; // the whole UTF-8 sequence must fit; truncate cleanly
455 return JsonEsc::TRUNCATED;
456 }
457 for (int k = 0; k < un; k++)
458 out[i++] = (char)u8[k];
459 return JsonEsc::EMITTED;
460}
461
462// Decode one JSON string escape. On entry p points at the escape char (just past the '\'). Simple
463// escapes and malformed \u yield LITERAL_C (resolved char in *c_out); a valid \uXXXX emits its UTF-8
464// bytes directly (EMITTED) or reports TRUNCATED when the whole sequence will not fit. p is left on
465// the last consumed byte so the caller can advance past it uniformly.
466static JsonEsc json_decode_escape(const char *&p, char *out, size_t &i, size_t out_cap, char *c_out)
467{
468 switch (*p)
469 {
470 case 'n':
471 *c_out = '\n';
472 return JsonEsc::LITERAL_C;
473 case 't':
474 *c_out = '\t';
475 return JsonEsc::LITERAL_C;
476 case 'r':
477 *c_out = '\r';
478 return JsonEsc::LITERAL_C;
479 case 'b':
480 *c_out = '\b';
481 return JsonEsc::LITERAL_C;
482 case 'f':
483 *c_out = '\f';
484 return JsonEsc::LITERAL_C;
485 case '"':
486 *c_out = '"';
487 return JsonEsc::LITERAL_C;
488 case '\\':
489 *c_out = '\\';
490 return JsonEsc::LITERAL_C;
491 case '/':
492 *c_out = '/';
493 return JsonEsc::LITERAL_C;
494 case 'u':
495 break; // \uXXXX handled below
496 default:
497 *c_out = *p;
498 return JsonEsc::LITERAL_C;
499 }
500
501 // \uXXXX -> UTF-8. <= 0x7F stays one byte; 0x80..0x7FF / 0x800..0xFFFF and (via a surrogate pair)
502 // 0x10000..0x10FFFF emit 2/3/4 bytes. Malformed / short hex -> '?', rescanned as literals.
503 long cp = json_decode_u(p);
504 if (cp < 0)
505 {
506 *c_out = '?';
507 return JsonEsc::LITERAL_C;
508 }
509 return json_emit_utf8((unsigned)cp, out, i, out_cap);
510}
511
512bool json_get_str(const char *json, const char *key, char *out, size_t out_cap)
513{
514 if (!out || out_cap == 0)
515 return false;
516 const char *v = json_find_value(json, key);
517 if (!v || *v != '"')
518 return false;
519
520 const char *p = v + 1;
521 size_t i = 0;
522 while (*p && *p != '"')
523 {
524 char c = *p;
525 if (c == '\\' && p[1])
526 {
527 p++;
528 JsonEsc r = json_decode_escape(p, out, i, out_cap, &c);
529 if (r == JsonEsc::TRUNCATED)
530 return true;
531 if (r == JsonEsc::EMITTED)
532 {
533 p++; // past the last consumed hex digit
534 continue;
535 }
536 }
537 if (i + 1 < out_cap)
538 {
539 out[i++] = c;
540 }
541 else
542 {
543 out[i] = '\0'; // truncate to capacity
544 return true;
545 }
546 p++;
547 }
548 out[i] = '\0';
549 return true;
550}
551
552bool json_get_int(const char *json, const char *key, long *out)
553{
554 if (!out)
555 return false;
556 const char *v = json_find_value(json, key);
557 if (!v || *v == '"') // must be a bare number, not a string
558 return false;
559 const char *end = nullptr;
560 long val = det_strtol(v, &end);
561 if (end == v)
562 return false; // no digits parsed
563 *out = val;
564 return true;
565}
566
567bool json_get_bool(const char *json, const char *key, bool *out)
568{
569 if (!out)
570 return false;
571 const char *v = json_find_value(json, key);
572 if (!v)
573 return false;
574 if (strncmp(v, "true", 4) == 0 && (v[4] == '\0' || v[4] == ',' || v[4] == '}' || v[4] == ']' || is_ws(v[4])))
575 {
576 *out = true;
577 return true;
578 }
579 if (strncmp(v, "false", 5) == 0 && (v[5] == '\0' || v[5] == ',' || v[5] == '}' || v[5] == ']' || is_ws(v[5])))
580 {
581 *out = false;
582 return true;
583 }
584 return false;
585}
#define JSON_MAX_DEPTH
Maximum object/array nesting depth for the JsonWriter (see json.h).
void str(const char *v)
Emit a quoted, escaped string value.
Definition json.cpp:171
void begin_object()
Open { (as a value/element where applicable).
Definition json.cpp:144
void key(const char *k)
Emit an object member name ("k":); follow with one value.
Definition json.cpp:161
void raw(const char *literal)
Emit a pre-formatted literal verbatim.
Definition json.cpp:207
JsonWriter(char *buf, size_t cap)
Construct over a caller buffer.
Definition json.cpp:18
void kv_bool(const char *k, bool v)
"k":true|false.
Definition json.cpp:228
void end_array()
Close ].
Definition json.cpp:156
void kv_str(const char *k, const char *v)
"k":"v" (escaped).
Definition json.cpp:213
void boolean(bool v)
Emit true/false.
Definition json.cpp:195
void integer(long v)
Emit a signed integer value.
Definition json.cpp:179
void kv_uint(const char *k, unsigned long v)
"k":<uint>.
Definition json.cpp:223
void kv_int(const char *k, long v)
"k":<int>.
Definition json.cpp:218
void uinteger(unsigned long v)
Emit an unsigned integer value.
Definition json.cpp:187
void end_object()
Close }.
Definition json.cpp:148
void kv_raw(const char *k, const char *literal)
"k":<literal>.
Definition json.cpp:238
void null_value()
Emit null.
Definition json.cpp:201
void kv_null(const char *k)
"k":null.
Definition json.cpp:233
void begin_array()
Open [.
Definition json.cpp:152
bool json_get_str(const char *json, const char *key, char *out, size_t out_cap)
Read a top-level string member from a JSON object body.
Definition json.cpp:512
JsonEsc
Definition json.cpp:372
bool json_get_bool(const char *json, const char *key, bool *out)
Read a top-level boolean member (true/false) from a JSON object body.
Definition json.cpp:567
bool json_get_int(const char *json, const char *key, long *out)
Read a top-level integer member from a JSON object body.
Definition json.cpp:552
Layer 6 (Presentation) - zero-heap JSON: a bounded writer and top-level reader.
Tiny no-stdlib base-10 number parsers (strtol/strtoul/strtof replacements).
long det_strtol(const char *s, const char **end)
Parse a base-10 long; sets end past the digits (or to s if none).
Definition numparse.h:32