Branch data Line data Source code
1 : : /**
2 : : * @file json_encode.c
3 : : * @brief Encode a JSON string into a compressed .trp buffer.
4 : : *
5 : : * Minimal recursive-descent parser that flattens JSON into dot-path keys.
6 : : * Objects: {"a":{"b":1}} -> key "a.b", value int(1)
7 : : * Arrays: {"x":[10,20]} -> keys "x[0]", "x[1]"
8 : : *
9 : : * Copyright (c) 2026 M. A. Chatterjee <deftio at deftio dot com>
10 : : * BSD-2-Clause — see LICENSE.txt
11 : : */
12 : :
13 : : #include "json_internal.h"
14 : :
15 : : #include <ctype.h>
16 : : #include <math.h>
17 : : #include <stdio.h>
18 : : #include <stdlib.h>
19 : : #include <string.h>
20 : :
21 : : /* ── Parser state ────────────────────────────────────────────────────── */
22 : :
23 : : typedef struct {
24 : : const char *src;
25 : : size_t len;
26 : : size_t pos;
27 : : int depth;
28 : : tp_encoder *enc;
29 : : char path[4096]; /* current dot-path buffer */
30 : : size_t path_len;
31 : : } json_parser;
32 : :
33 : : /* ── Whitespace / peek helpers ───────────────────────────────────────── */
34 : :
35 : 2161 : static void skip_ws(json_parser *p)
36 : : {
37 [ + + ]: 2175 : while (p->pos < p->len) {
38 : 2166 : char c = p->src[p->pos];
39 [ + + + - : 2166 : if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
+ - - + ]
40 : 14 : p->pos++;
41 : : else
42 : : break;
43 : : }
44 : 2161 : }
45 : :
46 : 445 : static char peek(json_parser *p)
47 : : {
48 : 445 : skip_ws(p);
49 [ + + ]: 445 : if (p->pos >= p->len)
50 : 2 : return '\0';
51 : 443 : return p->src[p->pos];
52 : : }
53 : :
54 : 832 : static bool expect(json_parser *p, char c)
55 : : {
56 : 832 : skip_ws(p);
57 [ + + + + ]: 832 : if (p->pos < p->len && p->src[p->pos] == c) {
58 : 828 : p->pos++;
59 : 828 : return true;
60 : : }
61 : 4 : return false;
62 : : }
63 : :
64 : : /* ── Path manipulation ───────────────────────────────────────────────── */
65 : :
66 : 219 : static void path_push_key(json_parser *p, const char *key, size_t key_len, size_t *saved_len)
67 : : {
68 : 219 : *saved_len = p->path_len;
69 [ + + ]: 219 : if (p->path_len > 0) {
70 : 76 : p->path[p->path_len++] = '.';
71 : : }
72 [ + - ]: 219 : if (p->path_len + key_len < sizeof(p->path)) {
73 : 219 : memcpy(p->path + p->path_len, key, key_len);
74 : 219 : p->path_len += key_len;
75 : : }
76 : 219 : p->path[p->path_len] = '\0';
77 : 219 : }
78 : :
79 : 105 : static void path_push_index(json_parser *p, uint32_t idx, size_t *saved_len)
80 : : {
81 : 105 : *saved_len = p->path_len;
82 : : char tmp[16];
83 : 105 : int n = snprintf(tmp, sizeof(tmp), "[%u]", (unsigned)idx);
84 [ + - + - ]: 105 : if (n > 0 && p->path_len + (size_t)n < sizeof(p->path)) {
85 : 105 : memcpy(p->path + p->path_len, tmp, (size_t)n);
86 : 105 : p->path_len += (size_t)n;
87 : : }
88 : 105 : p->path[p->path_len] = '\0';
89 : 105 : }
90 : :
91 : 245 : static void path_restore(json_parser *p, size_t saved_len)
92 : : {
93 : 245 : p->path_len = saved_len;
94 : 245 : p->path[p->path_len] = '\0';
95 : 245 : }
96 : :
97 : : /* ── Forward declaration ─────────────────────────────────────────────── */
98 : :
99 : : static tp_result parse_value(json_parser *p);
100 : :
101 : : /* ── String parsing ──────────────────────────────────────────────────── */
102 : :
103 : 277 : static tp_result parse_string_into(json_parser *p, char *out, size_t out_cap, size_t *out_len)
104 : : {
105 [ + + ]: 277 : if (!expect(p, '"'))
106 : 1 : return TP_ERR_JSON_SYNTAX;
107 : :
108 : 276 : size_t w = 0;
109 [ + + ]: 1286 : while (p->pos < p->len) {
110 : 1285 : char c = p->src[p->pos++];
111 [ + + ]: 1285 : if (c == '"') {
112 [ + - + - ]: 267 : if (out && w < out_cap)
113 : 267 : out[w] = '\0';
114 : 267 : *out_len = w;
115 : 267 : return TP_OK;
116 : : }
117 [ + + ]: 1018 : if (c == '\\') {
118 [ + + ]: 29 : if (p->pos >= p->len)
119 : 1 : return TP_ERR_JSON_SYNTAX;
120 : 28 : char esc = p->src[p->pos++];
121 [ + + + + : 28 : switch (esc) {
+ + + + ]
122 : 4 : case '"':
123 : : case '\\':
124 : : case '/':
125 : 4 : c = esc;
126 : 4 : break;
127 : 2 : case 'b':
128 : 2 : c = '\b';
129 : 2 : break;
130 : 2 : case 'f':
131 : 2 : c = '\f';
132 : 2 : break;
133 : 2 : case 'n':
134 : 2 : c = '\n';
135 : 2 : break;
136 : 2 : case 'r':
137 : 2 : c = '\r';
138 : 2 : break;
139 : 2 : case 't':
140 : 2 : c = '\t';
141 : 2 : break;
142 : 20 : case 'u': {
143 : : /* Parse 4 hex digits -> UTF-8 */
144 [ + + ]: 13 : if (p->pos + 4 > p->len)
145 : 1 : return TP_ERR_JSON_SYNTAX;
146 : 12 : uint32_t cp = 0;
147 [ + + ]: 56 : for (int i = 0; i < 4; i++) {
148 : 46 : char h = p->src[p->pos++];
149 : 46 : cp <<= 4;
150 [ + + + + ]: 46 : if (h >= '0' && h <= '9')
151 : 32 : cp |= (uint32_t)(h - '0');
152 [ + + + + ]: 14 : else if (h >= 'a' && h <= 'f')
153 : 4 : cp |= (uint32_t)(h - 'a' + 10);
154 [ + + + + ]: 10 : else if (h >= 'A' && h <= 'F')
155 : 8 : cp |= (uint32_t)(h - 'A' + 10);
156 : : else
157 : 2 : return TP_ERR_JSON_SYNTAX;
158 : : }
159 : : /* Handle surrogate pairs */
160 [ + + + - ]: 10 : if (cp >= 0xD800 && cp <= 0xDBFF) {
161 [ + + + - : 5 : if (p->pos + 6 > p->len || p->src[p->pos] != '\\' || p->src[p->pos + 1] != 'u')
- + ]
162 : 1 : return TP_ERR_JSON_SYNTAX;
163 : 4 : p->pos += 2;
164 : 4 : uint32_t lo = 0;
165 [ + + ]: 17 : for (int i = 0; i < 4; i++) {
166 : 14 : char h = p->src[p->pos++];
167 : 14 : lo <<= 4;
168 [ + - + + ]: 14 : if (h >= '0' && h <= '9')
169 : 8 : lo |= (uint32_t)(h - '0');
170 [ + + + + ]: 6 : else if (h >= 'a' && h <= 'f')
171 : 2 : lo |= (uint32_t)(h - 'a' + 10);
172 [ + - + + ]: 4 : else if (h >= 'A' && h <= 'F')
173 : 3 : lo |= (uint32_t)(h - 'A' + 10);
174 : : else
175 : 1 : return TP_ERR_JSON_SYNTAX;
176 : : }
177 [ + + - + ]: 3 : if (lo < 0xDC00 || lo > 0xDFFF)
178 : 1 : return TP_ERR_JSON_SYNTAX;
179 : 2 : cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
180 : : }
181 : : /* Encode codepoint as UTF-8 */
182 [ + + ]: 7 : if (cp < 0x80) {
183 [ + - + - ]: 1 : if (out && w < out_cap)
184 : 1 : out[w] = (char)cp;
185 : 1 : w++;
186 [ + + ]: 6 : } else if (cp < 0x800) {
187 [ + - + - ]: 3 : if (out && w + 1 < out_cap) {
188 : 3 : out[w] = (char)(0xC0 | (cp >> 6));
189 : 3 : out[w + 1] = (char)(0x80 | (cp & 0x3F));
190 : : }
191 : 3 : w += 2;
192 [ + + ]: 3 : } else if (cp < 0x10000) {
193 [ + - + - ]: 1 : if (out && w + 2 < out_cap) {
194 : 1 : out[w] = (char)(0xE0 | (cp >> 12));
195 : 1 : out[w + 1] = (char)(0x80 | ((cp >> 6) & 0x3F));
196 : 1 : out[w + 2] = (char)(0x80 | (cp & 0x3F));
197 : : }
198 : 1 : w += 3;
199 [ + - ]: 2 : } else if (cp < 0x110000) {
200 [ + - + - ]: 2 : if (out && w + 3 < out_cap) {
201 : 2 : out[w] = (char)(0xF0 | (cp >> 18));
202 : 2 : out[w + 1] = (char)(0x80 | ((cp >> 12) & 0x3F));
203 : 2 : out[w + 2] = (char)(0x80 | ((cp >> 6) & 0x3F));
204 : 2 : out[w + 3] = (char)(0x80 | (cp & 0x3F));
205 : : }
206 : 2 : w += 4;
207 : : }
208 : 7 : continue; /* already handled, skip the default append */
209 : : }
210 : 1 : default:
211 : 1 : return TP_ERR_JSON_SYNTAX;
212 : : }
213 : : }
214 [ + - + - ]: 1003 : if (out && w < out_cap)
215 : 1003 : out[w] = c;
216 : 1003 : w++;
217 : : }
218 : 1 : return TP_ERR_JSON_SYNTAX; /* unterminated string */
219 : : }
220 : :
221 : : /* ── Number parsing ──────────────────────────────────────────────────── */
222 : :
223 : 132 : static tp_result parse_number(json_parser *p, tp_value *val)
224 : : {
225 : 132 : size_t start = p->pos;
226 : 132 : bool is_float = false;
227 : 132 : bool is_neg = false;
228 : :
229 [ + - + + ]: 132 : if (p->pos < p->len && p->src[p->pos] == '-') {
230 : 4 : is_neg = true;
231 : 4 : p->pos++;
232 : : }
233 : :
234 : : /* Integer part */
235 [ + - + + ]: 132 : if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
236 : 1 : return TP_ERR_JSON_SYNTAX;
237 [ + + + + ]: 427 : while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos]))
238 : 296 : p->pos++;
239 : :
240 : : /* Fractional part */
241 [ + + + + ]: 131 : if (p->pos < p->len && p->src[p->pos] == '.') {
242 : 13 : is_float = true;
243 : 13 : p->pos++;
244 [ + - + + ]: 13 : if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
245 : 1 : return TP_ERR_JSON_SYNTAX;
246 [ + - + + ]: 31 : while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos]))
247 : 19 : p->pos++;
248 : : }
249 : :
250 : : /* Exponent part */
251 [ + + + + : 130 : if (p->pos < p->len && (p->src[p->pos] == 'e' || p->src[p->pos] == 'E')) {
+ + ]
252 : 5 : is_float = true;
253 : 5 : p->pos++;
254 [ + - + + : 5 : if (p->pos < p->len && (p->src[p->pos] == '+' || p->src[p->pos] == '-'))
+ + ]
255 : 3 : p->pos++;
256 [ + - + + ]: 5 : if (p->pos >= p->len || !isdigit((unsigned char)p->src[p->pos]))
257 : 2 : return TP_ERR_JSON_SYNTAX;
258 [ + - + + ]: 7 : while (p->pos < p->len && isdigit((unsigned char)p->src[p->pos]))
259 : 4 : p->pos++;
260 : : }
261 : :
262 : : /* Convert */
263 : 128 : size_t numlen = p->pos - start;
264 : : char tmp[64];
265 [ + + ]: 128 : if (numlen >= sizeof(tmp))
266 : 1 : numlen = sizeof(tmp) - 1;
267 : 128 : memcpy(tmp, p->src + start, numlen);
268 : 128 : tmp[numlen] = '\0';
269 : :
270 [ + + ]: 128 : if (is_float) {
271 : 14 : double d = strtod(tmp, NULL);
272 : 14 : *val = tp_value_float64(d);
273 [ + + ]: 114 : } else if (is_neg) {
274 : 3 : long long ll = strtoll(tmp, NULL, 10);
275 : 3 : *val = tp_value_int((int64_t)ll);
276 : : } else {
277 : 111 : unsigned long long ull = strtoull(tmp, NULL, 10);
278 [ + + ]: 111 : if (ull > (unsigned long long)INT64_MAX) {
279 : 2 : *val = tp_value_uint((uint64_t)ull);
280 : : } else {
281 : 109 : *val = tp_value_int((int64_t)ull);
282 : : }
283 : : }
284 : 128 : return TP_OK;
285 : : }
286 : :
287 : : /* ── Value parsing (recursive) ───────────────────────────────────────── */
288 : :
289 : 139 : static tp_result parse_object(json_parser *p)
290 : : {
291 : 139 : p->depth++;
292 [ + + ]: 139 : if (p->depth > TP_MAX_NESTING_DEPTH)
293 : 1 : return TP_ERR_JSON_DEPTH;
294 : :
295 : 138 : if (!expect(p, '{')) /* LCOV_EXCL_BR_LINE */
296 : : return TP_ERR_JSON_SYNTAX; /* LCOV_EXCL_LINE */
297 : :
298 [ + + ]: 138 : if (peek(p) == '}') {
299 : 4 : p->pos++;
300 : 4 : p->depth--;
301 : 4 : return TP_OK;
302 : : }
303 : :
304 : 87 : while (true) {
305 : : /* Parse key */
306 : : char key[1024];
307 : 221 : size_t key_len = 0;
308 : 221 : tp_result rc = parse_string_into(p, key, sizeof(key) - 1, &key_len);
309 [ + + ]: 221 : if (rc != TP_OK)
310 : 50 : return rc;
311 : 220 : key[key_len] = '\0';
312 : :
313 : 220 : skip_ws(p);
314 [ + + ]: 220 : if (!expect(p, ':'))
315 : 1 : return TP_ERR_JSON_SYNTAX;
316 : :
317 : : /* Push key onto path */
318 : : size_t saved;
319 : 219 : path_push_key(p, key, key_len, &saved);
320 : :
321 : : /* Parse value */
322 : 219 : rc = parse_value(p);
323 [ + + ]: 219 : if (rc != TP_OK)
324 : 47 : return rc;
325 : :
326 : 172 : path_restore(p, saved);
327 : :
328 : 172 : skip_ws(p);
329 [ + + ]: 172 : if (peek(p) == '}') {
330 : 84 : p->pos++;
331 : 84 : break;
332 : : }
333 [ + + ]: 88 : if (!expect(p, ','))
334 : 1 : return TP_ERR_JSON_SYNTAX;
335 : : }
336 : :
337 : 84 : p->depth--;
338 : 84 : return TP_OK;
339 : : }
340 : :
341 : 63 : static tp_result parse_array(json_parser *p)
342 : : {
343 : 63 : p->depth++;
344 [ + + ]: 63 : if (p->depth > TP_MAX_NESTING_DEPTH)
345 : 1 : return TP_ERR_JSON_DEPTH;
346 : :
347 : 62 : if (!expect(p, '[')) /* LCOV_EXCL_BR_LINE */
348 : : return TP_ERR_JSON_SYNTAX; /* LCOV_EXCL_LINE */
349 : :
350 [ + + ]: 62 : if (peek(p) == ']') {
351 : 3 : p->pos++;
352 : 3 : p->depth--;
353 : 3 : return TP_OK;
354 : : }
355 : :
356 : 59 : uint32_t idx = 0;
357 : 46 : while (true) {
358 : : size_t saved;
359 : 105 : path_push_index(p, idx, &saved);
360 : :
361 : 105 : tp_result rc = parse_value(p);
362 [ + + ]: 105 : if (rc != TP_OK)
363 : 33 : return rc;
364 : :
365 : 73 : path_restore(p, saved);
366 : 73 : idx++;
367 : :
368 : 73 : skip_ws(p);
369 [ + + ]: 73 : if (peek(p) == ']') {
370 : 26 : p->pos++;
371 : 26 : break;
372 : : }
373 [ + + ]: 47 : if (!expect(p, ','))
374 : 1 : return TP_ERR_JSON_SYNTAX;
375 : : }
376 : :
377 : 26 : p->depth--;
378 : 26 : return TP_OK;
379 : : }
380 : :
381 : 324 : static tp_result parse_value(json_parser *p)
382 : : {
383 : 324 : skip_ws(p);
384 [ + + ]: 324 : if (p->pos >= p->len)
385 : 1 : return TP_ERR_JSON_SYNTAX;
386 : :
387 : 323 : char c = p->src[p->pos];
388 : :
389 [ + + ]: 323 : if (c == '{')
390 : 55 : return parse_object(p);
391 : :
392 [ + + ]: 268 : if (c == '[')
393 : 55 : return parse_array(p);
394 : :
395 [ + + ]: 213 : if (c == '"') {
396 : : /* String value — parse then store at current path */
397 : : char str[4096];
398 : 56 : size_t slen = 0;
399 : 56 : tp_result rc = parse_string_into(p, str, sizeof(str) - 1, &slen);
400 [ + + ]: 56 : if (rc != TP_OK)
401 : 9 : return rc;
402 : 47 : str[slen] = '\0';
403 : 47 : tp_value val = tp_value_string_n(str, slen);
404 : 47 : return tp_encoder_add_n(p->enc, p->path, p->path_len, &val);
405 : : }
406 : :
407 [ + + + + ]: 157 : if (c == '-' || isdigit((unsigned char)c)) {
408 : : tp_value val;
409 : 132 : tp_result rc = parse_number(p, &val);
410 [ + + ]: 132 : if (rc != TP_OK)
411 : 4 : return rc;
412 : 128 : return tp_encoder_add_n(p->enc, p->path, p->path_len, &val);
413 : : }
414 : :
415 [ + + + + ]: 25 : if (p->pos + 4 <= p->len && memcmp(p->src + p->pos, "true", 4) == 0) {
416 : 11 : p->pos += 4;
417 : 11 : tp_value val = tp_value_bool(true);
418 : 11 : return tp_encoder_add_n(p->enc, p->path, p->path_len, &val);
419 : : }
420 : :
421 [ + + + + ]: 14 : if (p->pos + 5 <= p->len && memcmp(p->src + p->pos, "false", 5) == 0) {
422 : 6 : p->pos += 5;
423 : 6 : tp_value val = tp_value_bool(false);
424 : 6 : return tp_encoder_add_n(p->enc, p->path, p->path_len, &val);
425 : : }
426 : :
427 [ + + + - ]: 8 : if (p->pos + 4 <= p->len && memcmp(p->src + p->pos, "null", 4) == 0) {
428 : 7 : p->pos += 4;
429 : 7 : tp_value val = tp_value_null();
430 : 7 : return tp_encoder_add_n(p->enc, p->path, p->path_len, &val);
431 : : }
432 : :
433 : 1 : return TP_ERR_JSON_SYNTAX;
434 : : }
435 : :
436 : : /* ── One-shot encode ─────────────────────────────────────────────────── */
437 : :
438 : 98 : tp_result tp_json_encode(const char *json_str, size_t json_len, uint8_t **buf, size_t *buf_len)
439 : : {
440 [ + + + + : 98 : if (!json_str || !buf || !buf_len)
+ + ]
441 : 3 : return TP_ERR_INVALID_PARAM;
442 : :
443 : : /* Allocation failure paths are excluded from coverage (LCOV_EXCL). */
444 : 95 : tp_encoder *enc = NULL;
445 : 95 : tp_result rc = tp_encoder_create(&enc);
446 : 95 : if (rc != TP_OK) /* LCOV_EXCL_BR_LINE */
447 : : return rc; /* LCOV_EXCL_LINE */
448 : :
449 : : json_parser parser;
450 : 95 : memset(&parser, 0, sizeof(parser));
451 : 95 : parser.src = json_str;
452 : 95 : parser.len = json_len;
453 : 95 : parser.pos = 0;
454 : 95 : parser.depth = 0;
455 : 95 : parser.enc = enc;
456 : 95 : parser.path_len = 0;
457 : 95 : parser.path[0] = '\0';
458 : :
459 : : /* Determine root type and parse */
460 : 95 : skip_ws(&parser);
461 [ + + ]: 95 : if (parser.pos >= parser.len) {
462 : 2 : tp_encoder_destroy(&enc);
463 : 2 : return TP_ERR_JSON_SYNTAX;
464 : : }
465 : :
466 : : uint32_t root_type;
467 : 93 : char root_c = parser.src[parser.pos];
468 [ + + ]: 93 : if (root_c == '{') {
469 : 84 : root_type = TP_JSON_ROOT_OBJECT;
470 : 84 : rc = parse_object(&parser);
471 [ + + ]: 9 : } else if (root_c == '[') {
472 : 8 : root_type = TP_JSON_ROOT_ARRAY;
473 : 8 : rc = parse_array(&parser);
474 : : } else {
475 : 1 : tp_encoder_destroy(&enc);
476 : 1 : return TP_ERR_JSON_SYNTAX;
477 : : }
478 : :
479 [ + + ]: 92 : if (rc != TP_OK) {
480 : : /* LCOV_EXCL_START */
481 : : tp_encoder_destroy(&enc);
482 : : return rc;
483 : : /* LCOV_EXCL_STOP */
484 : : }
485 : :
486 : : /* Store root type metadata */
487 : 71 : tp_value root_val = tp_value_uint(root_type);
488 : 71 : rc = tp_encoder_add(enc, TP_JSON_META_ROOT, &root_val);
489 : 71 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
490 : : /* LCOV_EXCL_START */
491 : : tp_encoder_destroy(&enc);
492 : : return rc;
493 : : /* LCOV_EXCL_STOP */
494 : : }
495 : :
496 : 71 : rc = tp_encoder_build(enc, buf, buf_len);
497 : 71 : tp_encoder_destroy(&enc);
498 : 71 : return rc;
499 : : }
|