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