Branch data Line data Source code
1 : : /**
2 : : * @file json_decode.c
3 : : * @brief Decode a .trp buffer back to JSON text.
4 : : *
5 : : * Reads all key-value pairs from the trie, unflattens dot-path keys
6 : : * back into nested JSON objects and arrays, and outputs JSON text.
7 : : *
8 : : * Copyright (c) 2026 M. A. Chatterjee <deftio at deftio dot com>
9 : : * BSD-2-Clause — see LICENSE.txt
10 : : */
11 : :
12 : : #include "core_internal.h"
13 : : #include "json_internal.h"
14 : :
15 : : #include <stdio.h>
16 : : #include <stdlib.h>
17 : : #include <string.h>
18 : :
19 : : /* ── Dynamic string buffer ───────────────────────────────────────────── */
20 : :
21 : : typedef struct {
22 : : char *data;
23 : : size_t len;
24 : : size_t cap;
25 : : } strbuf;
26 : :
27 : 233 : static void sb_init(strbuf *sb)
28 : : {
29 : 233 : sb->data = NULL;
30 : 233 : sb->len = 0;
31 : 233 : sb->cap = 0;
32 : 233 : }
33 : :
34 : : /* LCOV_EXCL_START — only called from allocation failure error paths */
35 : : static void sb_free(strbuf *sb)
36 : : {
37 : : free(sb->data);
38 : : sb->data = NULL;
39 : : sb->len = 0;
40 : : sb->cap = 0;
41 : : }
42 : : /* LCOV_EXCL_STOP */
43 : :
44 : 10734 : static tp_result sb_grow(strbuf *sb, size_t need)
45 : : {
46 [ + + ]: 10734 : if (sb->len + need <= sb->cap)
47 : 10496 : return TP_OK;
48 [ + + ]: 238 : size_t new_cap = sb->cap == 0 ? 256 : sb->cap;
49 [ + + ]: 243 : while (new_cap < sb->len + need)
50 : 5 : new_cap *= 2;
51 : : /* Allocation failure paths are excluded from coverage (LCOV_EXCL). */
52 : 238 : char *p = realloc(sb->data, new_cap);
53 [ - + ]: 238 : if (!p)
54 : : return TP_ERR_ALLOC; /* LCOV_EXCL_LINE */
55 : 238 : sb->data = p;
56 : 238 : sb->cap = new_cap;
57 : 238 : return TP_OK;
58 : : }
59 : :
60 : 10734 : static tp_result sb_append(strbuf *sb, const char *s, size_t n)
61 : : {
62 : 10734 : tp_result rc = sb_grow(sb, n);
63 [ - + ]: 10734 : if (rc != TP_OK)
64 : : return rc; /* LCOV_EXCL_LINE */
65 : 10734 : memcpy(sb->data + sb->len, s, n);
66 : 10734 : sb->len += n;
67 : 10734 : return TP_OK;
68 : : }
69 : :
70 : 9521 : static tp_result sb_appendc(strbuf *sb, char c)
71 : : {
72 : 9521 : return sb_append(sb, &c, 1);
73 : : }
74 : :
75 : 835 : static tp_result sb_append_str(strbuf *sb, const char *s)
76 : : {
77 : 835 : return sb_append(sb, s, strlen(s));
78 : : }
79 : :
80 : : /* ── Flat key-value entry for sorting ────────────────────────────────── */
81 : :
82 : : typedef struct {
83 : : char *key;
84 : : size_t key_len;
85 : : tp_value val;
86 : : } flat_entry;
87 : :
88 : : /* Compare two flat entries lexicographically by key */
89 : 1107 : static int flat_entry_cmp(const void *a, const void *b)
90 : : {
91 : 1107 : const flat_entry *ea = (const flat_entry *)a;
92 : 1107 : const flat_entry *eb = (const flat_entry *)b;
93 : 1107 : size_t min_len = ea->key_len < eb->key_len ? ea->key_len : eb->key_len;
94 : 1107 : int c = memcmp(ea->key, eb->key, min_len);
95 [ + + ]: 1107 : if (c != 0)
96 : 876 : return c;
97 [ + + ]: 231 : if (ea->key_len < eb->key_len)
98 : 151 : return -1;
99 [ + + ]: 80 : if (ea->key_len > eb->key_len)
100 : 15 : return 1;
101 : 65 : return 0;
102 : : }
103 : :
104 : : /* ── JSON string escaping ────────────────────────────────────────────── */
105 : :
106 : 1089 : static tp_result sb_append_json_string(strbuf *sb, const char *str, size_t len)
107 : : {
108 : 1089 : tp_result rc = sb_appendc(sb, '"');
109 [ - + ]: 1089 : if (rc != TP_OK)
110 : : return rc; /* LCOV_EXCL_LINE */
111 [ + + ]: 5427 : for (size_t i = 0; i < len; i++) {
112 : 4338 : unsigned char c = (unsigned char)str[i];
113 [ + + + + : 4338 : switch (c) {
+ + + + ]
114 : 1 : case '"':
115 : 1 : rc = sb_append(sb, "\\\"", 2);
116 : 1 : break;
117 : 1 : case '\\':
118 : 1 : rc = sb_append(sb, "\\\\", 2);
119 : 1 : break;
120 : 9 : case '\b':
121 : 9 : rc = sb_append(sb, "\\b", 2);
122 : 9 : break;
123 : 1 : case '\f':
124 : 1 : rc = sb_append(sb, "\\f", 2);
125 : 1 : break;
126 : 1 : case '\n':
127 : 1 : rc = sb_append(sb, "\\n", 2);
128 : 1 : break;
129 : 1 : case '\r':
130 : 1 : rc = sb_append(sb, "\\r", 2);
131 : 1 : break;
132 : 1 : case '\t':
133 : 1 : rc = sb_append(sb, "\\t", 2);
134 : 1 : break;
135 : 4323 : default:
136 [ + + ]: 4323 : if (c < 0x20) {
137 : : char esc[8];
138 : 167 : snprintf(esc, sizeof(esc), "\\u%04x", c);
139 : 167 : rc = sb_append(sb, esc, 6);
140 : : } else {
141 : 4156 : rc = sb_appendc(sb, (char)c);
142 : : }
143 : : }
144 [ - + ]: 4338 : if (rc != TP_OK)
145 : : return rc; /* LCOV_EXCL_LINE */
146 : : }
147 : 1089 : return sb_appendc(sb, '"');
148 : : }
149 : :
150 : : /* ── Value to JSON text ──────────────────────────────────────────────── */
151 : :
152 : 786 : static tp_result sb_append_value(strbuf *sb, const tp_value *val)
153 : : {
154 : : char tmp[64];
155 [ + + + + : 786 : switch (val->type) {
+ + + + ]
156 : 286 : case TP_NULL:
157 : 286 : return sb_append_str(sb, "null");
158 : 70 : case TP_BOOL:
159 [ + + ]: 70 : return sb_append_str(sb, val->data.bool_val ? "true" : "false");
160 : 282 : case TP_INT:
161 : 282 : snprintf(tmp, sizeof(tmp), "%lld", (long long)val->data.int_val);
162 : 282 : return sb_append_str(sb, tmp);
163 : 8 : case TP_UINT:
164 : 8 : snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)val->data.uint_val);
165 : 8 : return sb_append_str(sb, tmp);
166 : 4 : case TP_FLOAT32: {
167 : 4 : double d = (double)val->data.float32_val;
168 : 4 : snprintf(tmp, sizeof(tmp), "%g", d);
169 : : /* Ensure it has a decimal point for JSON */
170 [ + + + - : 4 : if (!strchr(tmp, '.') && !strchr(tmp, 'e') && !strchr(tmp, 'E'))
+ - ]
171 : 1 : snprintf(tmp, sizeof(tmp), "%g.0", d);
172 : 4 : return sb_append_str(sb, tmp);
173 : : }
174 : 11 : case TP_FLOAT64:
175 : 11 : snprintf(tmp, sizeof(tmp), "%.17g", val->data.float64_val);
176 : 11 : return sb_append_str(sb, tmp);
177 : 113 : case TP_STRING:
178 [ + - ]: 113 : if (val->data.string_val.str)
179 : 113 : return sb_append_json_string(sb, val->data.string_val.str,
180 : 113 : val->data.string_val.str_len);
181 : : return sb_append_str(sb, "\"\""); /* LCOV_EXCL_LINE */
182 : 12 : default:
183 : 12 : return sb_append_str(sb, "null");
184 : : }
185 : : }
186 : :
187 : : /* ── Tree reconstruction ─────────────────────────────────────────────── */
188 : :
189 : : /*
190 : : * Reconstruct JSON from sorted flat entries.
191 : : * We use a recursive approach: find all entries at the current prefix level,
192 : : * group by their next path segment, and recurse.
193 : : */
194 : :
195 : : static tp_result emit_json(strbuf *sb, const flat_entry *entries, size_t count, const char *prefix,
196 : : size_t prefix_len, const char *indent, int depth);
197 : :
198 : : /* Check if path segment starting at pos is an array index [N] */
199 : 1566 : static bool is_array_index(const char *key, size_t key_len, size_t pos)
200 : : {
201 [ - + ]: 1566 : if (pos >= key_len)
202 : : return false; /* LCOV_EXCL_LINE */
203 : 1566 : return key[pos] == '[';
204 : : }
205 : :
206 : : /* Find the next segment boundary: returns length of next segment name
207 : : (up to '.' or '[' or end), and whether it's an array index */
208 : 5186 : static size_t next_segment(const char *key, size_t key_len, size_t pos, bool *is_idx)
209 : : {
210 : 5186 : *is_idx = false;
211 [ - + ]: 5186 : if (pos >= key_len)
212 : : return 0; /* LCOV_EXCL_LINE */
213 [ + + ]: 5186 : if (key[pos] == '[') {
214 : 924 : *is_idx = true;
215 : 924 : size_t end = pos + 1;
216 [ + + + + ]: 2022 : while (end < key_len && key[end] != ']')
217 : 1098 : end++;
218 [ + + ]: 924 : if (end < key_len)
219 : 832 : end++; /* include ']' */
220 : 924 : return end - pos;
221 : : }
222 : : /* Regular key segment: up to '.' or '[' or end */
223 : 4262 : size_t end = pos;
224 [ + + + + : 21925 : while (end < key_len && key[end] != '.' && key[end] != '[')
+ + ]
225 : 17663 : end++;
226 : 4262 : return end - pos;
227 : : }
228 : :
229 : 104 : static tp_result emit_indent(strbuf *sb, const char *indent, int depth)
230 : : {
231 [ - + ]: 104 : if (!indent)
232 : : return TP_OK; /* LCOV_EXCL_LINE */
233 : 104 : tp_result rc = sb_appendc(sb, '\n');
234 [ - + ]: 104 : if (rc != TP_OK)
235 : : return rc; /* LCOV_EXCL_LINE */
236 : 104 : size_t ilen = strlen(indent);
237 [ + + ]: 300 : for (int i = 0; i < depth; i++) {
238 : 196 : rc = sb_append(sb, indent, ilen);
239 [ - + ]: 196 : if (rc != TP_OK)
240 : : return rc; /* LCOV_EXCL_LINE */
241 : : }
242 : 104 : return TP_OK;
243 : : }
244 : :
245 : 714 : static tp_result emit_json(strbuf *sb, const flat_entry *entries, size_t count, const char *prefix,
246 : : size_t prefix_len, const char *indent, int depth)
247 : : {
248 : : /*
249 : : * Find all entries that start with prefix.
250 : : * Group them by their next segment after prefix.
251 : : */
252 : :
253 : : /* Determine if this level is an array or object by checking if
254 : : all next segments are array indices */
255 : 714 : bool all_array = true;
256 : 714 : bool any_entry = false;
257 : :
258 [ + + ]: 5465 : for (size_t i = 0; i < count; i++) {
259 [ + + ]: 4751 : if (entries[i].key_len < prefix_len)
260 : 1763 : continue;
261 [ + + + + ]: 2988 : if (prefix_len > 0 && memcmp(entries[i].key, prefix, prefix_len) != 0)
262 : 1248 : continue;
263 : : /* Skip metadata keys — already filtered by extract_entries */
264 [ + + - + ]: 1740 : if (entries[i].key_len > 0 && entries[i].key[0] == '\x01')
265 : : continue; /* LCOV_EXCL_LINE */
266 : :
267 : 1740 : size_t pos = prefix_len;
268 [ + + + + ]: 1740 : if (pos < entries[i].key_len && entries[i].key[pos] == '.')
269 : 155 : pos++;
270 [ + + ]: 1740 : if (pos >= entries[i].key_len)
271 : 174 : continue; /* exact match = leaf, shouldn't be here */
272 : :
273 : 1566 : any_entry = true;
274 [ + + ]: 1566 : if (!is_array_index(entries[i].key, entries[i].key_len, pos))
275 : 1250 : all_array = false;
276 : : }
277 : :
278 [ + + ]: 714 : if (!any_entry) {
279 : : /* Check if there's an exact prefix match (leaf value) */
280 [ + + ]: 1690 : for (size_t i = 0; i < count; i++) {
281 [ + + + + ]: 1533 : if (entries[i].key_len == prefix_len &&
282 [ - + ]: 50 : (prefix_len == 0 || memcmp(entries[i].key, prefix, prefix_len) == 0)) {
283 [ + - ]: 2 : if (entries[i].key[0] != '\x01')
284 : 2 : return sb_append_value(sb, &entries[i].val);
285 : : }
286 : : }
287 : 157 : return sb_append_str(sb, "null");
288 : : }
289 : :
290 : : /* Collect unique next-segments */
291 : : typedef struct {
292 : : size_t seg_start;
293 : : size_t seg_len;
294 : : bool is_idx;
295 : : } segment_info;
296 : :
297 : 555 : segment_info *segments = NULL;
298 : 555 : size_t num_segments = 0;
299 : 555 : size_t seg_cap = 0;
300 : :
301 [ + + ]: 3766 : for (size_t i = 0; i < count; i++) {
302 [ + + ]: 3211 : if (entries[i].key_len < prefix_len)
303 : 1646 : continue;
304 [ + + + + ]: 2755 : if (prefix_len > 0 && memcmp(entries[i].key, prefix, prefix_len) != 0)
305 : 1024 : continue;
306 [ + + - + ]: 1731 : if (entries[i].key_len > 0 && entries[i].key[0] == '\x01')
307 : : continue; /* LCOV_EXCL_LINE */
308 : :
309 : 1731 : size_t pos = prefix_len;
310 [ + + + + ]: 1731 : if (pos < entries[i].key_len && entries[i].key[pos] == '.')
311 : 155 : pos++;
312 [ + + ]: 1731 : if (pos >= entries[i].key_len)
313 : 165 : continue;
314 : :
315 : 1566 : bool is_idx = false;
316 : 1566 : size_t seg_len = next_segment(entries[i].key, entries[i].key_len, pos, &is_idx);
317 [ + + ]: 1566 : if (seg_len == 0)
318 : 1 : continue;
319 : :
320 : : /* Check if already in segments list */
321 : 1565 : bool dup = false;
322 [ + + ]: 3620 : for (size_t s = 0; s < num_segments; s++) {
323 : 2350 : const flat_entry *se = &entries[segments[s].seg_start];
324 : 2350 : size_t sp = prefix_len;
325 [ + - + + ]: 2350 : if (sp < se->key_len && se->key[sp] == '.')
326 : 49 : sp++;
327 : 2350 : bool si = false;
328 : 2350 : size_t sl = next_segment(se->key, se->key_len, sp, &si);
329 [ + + + + ]: 2350 : if (sl == seg_len && memcmp(se->key + sp, entries[i].key + pos, seg_len) == 0) {
330 : 295 : dup = true;
331 : 295 : break;
332 : : }
333 : : }
334 : :
335 [ + + ]: 1565 : if (!dup) {
336 [ + + ]: 1270 : if (num_segments >= seg_cap) {
337 [ + + ]: 556 : seg_cap = seg_cap == 0 ? 16 : seg_cap * 2;
338 : 556 : segment_info *new_segs = realloc(segments, seg_cap * sizeof(segment_info));
339 [ - + ]: 556 : if (!new_segs) {
340 : : /* LCOV_EXCL_START */
341 : : free(segments);
342 : : return TP_ERR_ALLOC;
343 : : /* LCOV_EXCL_STOP */
344 : : }
345 : 556 : segments = new_segs;
346 : : }
347 : 1270 : segments[num_segments].seg_start = i;
348 : 1270 : segments[num_segments].seg_len = seg_len;
349 : 1270 : segments[num_segments].is_idx = is_idx;
350 : 1270 : num_segments++;
351 : : }
352 : : }
353 : :
354 : : tp_result rc;
355 : :
356 [ + + ]: 555 : if (all_array) {
357 : 109 : rc = sb_appendc(sb, '[');
358 [ - + ]: 109 : if (rc != TP_OK) {
359 : : /* LCOV_EXCL_START */
360 : : free(segments);
361 : : return rc;
362 : : /* LCOV_EXCL_STOP */
363 : : }
364 : :
365 [ + + ]: 403 : for (size_t s = 0; s < num_segments; s++) {
366 [ + + ]: 294 : if (s > 0) {
367 : 185 : rc = sb_appendc(sb, ',');
368 [ - + ]: 185 : if (rc != TP_OK) {
369 : : /* LCOV_EXCL_START */
370 : : free(segments);
371 : : return rc;
372 : : /* LCOV_EXCL_STOP */
373 : : }
374 : : }
375 [ + + ]: 294 : if (indent) {
376 : 27 : rc = emit_indent(sb, indent, depth + 1);
377 [ - + ]: 27 : if (rc != TP_OK) {
378 : : /* LCOV_EXCL_START */
379 : : free(segments);
380 : : return rc;
381 : : /* LCOV_EXCL_STOP */
382 : : }
383 : : }
384 : :
385 : 294 : const flat_entry *se = &entries[segments[s].seg_start];
386 : 294 : size_t sp = prefix_len;
387 [ + - - + ]: 294 : if (sp < se->key_len && se->key[sp] == '.')
388 : : sp++; /* LCOV_EXCL_LINE */
389 : : bool si;
390 : 294 : size_t sl = next_segment(se->key, se->key_len, sp, &si);
391 : :
392 : : /* Build new prefix for this element */
393 : : char new_prefix[4096];
394 : 294 : size_t new_prefix_len = prefix_len;
395 [ + + ]: 294 : if (prefix_len > 0) {
396 : 285 : memcpy(new_prefix, prefix, prefix_len);
397 : : }
398 : 294 : memcpy(new_prefix + new_prefix_len, se->key + sp, sl);
399 : 294 : new_prefix_len += sl;
400 : 294 : new_prefix[new_prefix_len] = '\0';
401 : :
402 : : /* Check if any entry is exactly this prefix (leaf) */
403 : 294 : bool is_leaf = false;
404 : 294 : const tp_value *leaf_val = NULL;
405 [ + + ]: 1643 : for (size_t i = 0; i < count; i++) {
406 [ + + ]: 1628 : if (entries[i].key_len == new_prefix_len &&
407 [ + + ]: 547 : memcmp(entries[i].key, new_prefix, new_prefix_len) == 0 &&
408 [ + - ]: 279 : entries[i].key[0] != '\x01') {
409 : 279 : is_leaf = true;
410 : 279 : leaf_val = &entries[i].val;
411 : 279 : break;
412 : : }
413 : : }
414 : :
415 : : /* Check if there are sub-entries */
416 : 294 : bool has_children = false;
417 [ + + ]: 2288 : for (size_t i = 0; i < count; i++) {
418 [ + + ]: 2018 : if (entries[i].key_len > new_prefix_len &&
419 [ + + ]: 594 : memcmp(entries[i].key, new_prefix, new_prefix_len) == 0 &&
420 [ + - ]: 24 : entries[i].key[0] != '\x01') {
421 : 24 : has_children = true;
422 : 24 : break;
423 : : }
424 : : }
425 : :
426 [ + + + + ]: 294 : if (is_leaf && !has_children) {
427 : 270 : rc = sb_append_value(sb, leaf_val);
428 : : } else {
429 : 24 : rc = emit_json(sb, entries, count, new_prefix, new_prefix_len, indent, depth + 1);
430 : : }
431 [ - + ]: 294 : if (rc != TP_OK) {
432 : : /* LCOV_EXCL_START */
433 : : free(segments);
434 : : return rc;
435 : : /* LCOV_EXCL_STOP */
436 : : }
437 : : }
438 : :
439 [ + + + - ]: 109 : if (indent && num_segments > 0) {
440 : 10 : rc = emit_indent(sb, indent, depth);
441 [ - + ]: 10 : if (rc != TP_OK) {
442 : : /* LCOV_EXCL_START */
443 : : free(segments);
444 : : return rc;
445 : : /* LCOV_EXCL_STOP */
446 : : }
447 : : }
448 : 109 : rc = sb_appendc(sb, ']');
449 : : } else {
450 : 446 : rc = sb_appendc(sb, '{');
451 [ - + ]: 446 : if (rc != TP_OK) {
452 : : /* LCOV_EXCL_START */
453 : : free(segments);
454 : : return rc;
455 : : /* LCOV_EXCL_STOP */
456 : : }
457 : :
458 [ + + ]: 1422 : for (size_t s = 0; s < num_segments; s++) {
459 [ + + ]: 976 : if (s > 0) {
460 : 530 : rc = sb_appendc(sb, ',');
461 [ - + ]: 530 : if (rc != TP_OK) {
462 : : /* LCOV_EXCL_START */
463 : : free(segments);
464 : : return rc;
465 : : /* LCOV_EXCL_STOP */
466 : : }
467 : : }
468 [ + + ]: 976 : if (indent) {
469 : 49 : rc = emit_indent(sb, indent, depth + 1);
470 [ - + ]: 49 : if (rc != TP_OK) {
471 : : /* LCOV_EXCL_START */
472 : : free(segments);
473 : : return rc;
474 : : /* LCOV_EXCL_STOP */
475 : : }
476 : : }
477 : :
478 : 976 : const flat_entry *se = &entries[segments[s].seg_start];
479 : 976 : size_t sp = prefix_len;
480 [ + - + + ]: 976 : if (sp < se->key_len && se->key[sp] == '.')
481 : 146 : sp++;
482 : : bool si;
483 : 976 : size_t sl = next_segment(se->key, se->key_len, sp, &si);
484 : :
485 : : /* Write key */
486 : 976 : rc = sb_append_json_string(sb, se->key + sp, sl);
487 [ - + ]: 976 : if (rc != TP_OK) {
488 : : /* LCOV_EXCL_START */
489 : : free(segments);
490 : : return rc;
491 : : /* LCOV_EXCL_STOP */
492 : : }
493 : 976 : rc = sb_appendc(sb, ':');
494 [ - + ]: 976 : if (rc != TP_OK) {
495 : : /* LCOV_EXCL_START */
496 : : free(segments);
497 : : return rc;
498 : : /* LCOV_EXCL_STOP */
499 : : }
500 [ + + ]: 976 : if (indent) {
501 : 49 : rc = sb_appendc(sb, ' ');
502 [ - + ]: 49 : if (rc != TP_OK) {
503 : : /* LCOV_EXCL_START */
504 : : free(segments);
505 : : return rc;
506 : : /* LCOV_EXCL_STOP */
507 : : }
508 : : }
509 : :
510 : : /* Build new prefix */
511 : : char new_prefix[4096];
512 : 976 : size_t new_prefix_len = prefix_len;
513 [ + + ]: 976 : if (prefix_len > 0) {
514 : 303 : memcpy(new_prefix, prefix, prefix_len);
515 : 303 : new_prefix[new_prefix_len++] = '.';
516 : : }
517 : 976 : memcpy(new_prefix + new_prefix_len, se->key + sp, sl);
518 : 976 : new_prefix_len += sl;
519 : 976 : new_prefix[new_prefix_len] = '\0';
520 : :
521 : : /* Check if any entry is exactly this prefix (leaf) */
522 : 976 : bool is_leaf = false;
523 : 976 : const tp_value *leaf_val = NULL;
524 [ + + ]: 5324 : for (size_t i = 0; i < count; i++) {
525 [ + + ]: 4941 : if (entries[i].key_len == new_prefix_len &&
526 [ + + ]: 1031 : memcmp(entries[i].key, new_prefix, new_prefix_len) == 0 &&
527 [ + - ]: 593 : entries[i].key[0] != '\x01') {
528 : 593 : is_leaf = true;
529 : 593 : leaf_val = &entries[i].val;
530 : 593 : break;
531 : : }
532 : : }
533 : :
534 : : /* Check if there are sub-entries */
535 : 976 : bool has_children = false;
536 [ + + ]: 6753 : for (size_t i = 0; i < count; i++) {
537 [ + + ]: 6082 : if (entries[i].key_len > new_prefix_len &&
538 [ + + ]: 1970 : memcmp(entries[i].key, new_prefix, new_prefix_len) == 0 &&
539 [ + - ]: 305 : entries[i].key[0] != '\x01') {
540 : 305 : has_children = true;
541 : 305 : break;
542 : : }
543 : : }
544 : :
545 [ + + + + ]: 976 : if (is_leaf && !has_children) {
546 : 514 : rc = sb_append_value(sb, leaf_val);
547 : : } else {
548 : 462 : rc = emit_json(sb, entries, count, new_prefix, new_prefix_len, indent, depth + 1);
549 : : }
550 [ - + ]: 976 : if (rc != TP_OK) {
551 : : /* LCOV_EXCL_START */
552 : : free(segments);
553 : : return rc;
554 : : /* LCOV_EXCL_STOP */
555 : : }
556 : : }
557 : :
558 [ + + + - ]: 446 : if (indent && num_segments > 0) {
559 : 18 : rc = emit_indent(sb, indent, depth);
560 [ - + ]: 18 : if (rc != TP_OK) {
561 : : /* LCOV_EXCL_START */
562 : : free(segments);
563 : : return rc;
564 : : /* LCOV_EXCL_STOP */
565 : : }
566 : : }
567 : 446 : rc = sb_appendc(sb, '}');
568 : : }
569 : :
570 : 555 : free(segments);
571 : 555 : return rc;
572 : : }
573 : :
574 : : /* ── Extract all entries from a trp buffer ───────────────────────────── */
575 : :
576 : 317 : static tp_result extract_entries(const uint8_t *buf, size_t buf_len, flat_entry **out_entries,
577 : : size_t *out_count, uint32_t *out_root_type)
578 : : {
579 : : /* Encode path: we re-encode the JSON to get it back as entries.
580 : : Instead, we use the encoder's sorted key list approach:
581 : : open the dict, iterate by looking up known keys.
582 : :
583 : : Since the iterator is not yet fully implemented, we take a different
584 : : approach: re-open as a dict, then scan the trie by rebuilding from
585 : : the encoder. This is a decode limitation.
586 : :
587 : : Better approach: since we encoded using tp_encoder_build, we know
588 : : the keys are stored in sorted order. We can walk the trie by
589 : : scanning all possible paths. But without a working iterator, this
590 : : is hard.
591 : :
592 : : Practical approach: Use the dict to verify, but the real decode
593 : : needs to be done from the binary trie format directly.
594 : :
595 : : Simplest correct approach for v1.0: re-read using a lookup for
596 : : every key. But we don't know the keys!
597 : :
598 : : The correct solution is to implement a basic trie iterator here. */
599 : :
600 : 317 : tp_dict *dict = NULL;
601 : 317 : tp_result rc = tp_dict_open(&dict, buf, buf_len);
602 [ + + ]: 317 : if (rc != TP_OK)
603 : 74 : return rc;
604 : :
605 : 243 : uint32_t num_keys = tp_dict_count(dict);
606 : :
607 : : /* We need to walk the trie to extract all keys. Since the existing
608 : : tp_iter_next is a stub, we'll implement a local trie walker. */
609 : :
610 : : /* Read the symbol info from the dict to walk the trie */
611 : 243 : flat_entry *entries = calloc(num_keys, sizeof(flat_entry));
612 [ - + ]: 243 : if (!entries) {
613 : : /* LCOV_EXCL_START */
614 : : tp_dict_close(&dict);
615 : : return TP_ERR_ALLOC;
616 : : /* LCOV_EXCL_STOP */
617 : : }
618 : :
619 : : /* Walk the trie using a stack-based DFS */
620 : : typedef struct {
621 : : uint64_t bit_pos;
622 : : size_t key_len;
623 : : uint32_t remaining_children;
624 : : } walk_frame;
625 : :
626 : 243 : walk_frame *stack = calloc(256, sizeof(walk_frame));
627 [ - + ]: 243 : if (!stack) {
628 : : /* LCOV_EXCL_START */
629 : : free(entries);
630 : : tp_dict_close(&dict);
631 : : return TP_ERR_ALLOC;
632 : : /* LCOV_EXCL_STOP */
633 : : }
634 : :
635 : 243 : tp_bitstream_reader *reader = NULL;
636 : 243 : rc = tp_bs_reader_create(&reader, buf, (uint64_t)buf_len * 8);
637 [ - + ]: 243 : if (rc != TP_OK) {
638 : : /* LCOV_EXCL_START */
639 : : free(stack);
640 : : free(entries);
641 : : tp_dict_close(&dict);
642 : : return rc;
643 : : /* LCOV_EXCL_STOP */
644 : : }
645 : :
646 : 243 : rc = tp_bs_reader_seek(reader, dict->trie_start);
647 [ - + ]: 243 : if (rc != TP_OK) {
648 : : /* LCOV_EXCL_START */
649 : : tp_bs_reader_destroy(&reader);
650 : : free(stack);
651 : : free(entries);
652 : : tp_dict_close(&dict);
653 : : return rc;
654 : : /* LCOV_EXCL_STOP */
655 : : }
656 : :
657 : : char key_buf[4096];
658 : 243 : size_t key_len = 0;
659 : 243 : int stack_top = -1;
660 : 243 : size_t entry_count = 0;
661 : 243 : uint8_t bps = dict->sym.bits_per_symbol;
662 : :
663 : : /* DFS trie walk */
664 [ + + ]: 7118 : while (entry_count < num_keys) {
665 : : uint64_t sym_raw;
666 : 6888 : rc = tp_bs_read_bits(reader, bps, &sym_raw);
667 [ + + ]: 6888 : if (rc != TP_OK)
668 : 10 : break;
669 : 6881 : uint32_t sym = (uint32_t)sym_raw;
670 : :
671 [ + + ]: 6881 : if (sym == dict->sym.ctrl_codes[TP_CTRL_END]) {
672 : : /* Terminal without value */
673 [ + - ]: 197 : if (entry_count < num_keys) {
674 : 197 : entries[entry_count].key = malloc(key_len + 1);
675 [ + - ]: 197 : if (entries[entry_count].key) {
676 : 197 : memcpy(entries[entry_count].key, key_buf, key_len);
677 : 197 : entries[entry_count].key[key_len] = '\0';
678 : 197 : entries[entry_count].key_len = key_len;
679 : 197 : entries[entry_count].val = tp_value_null();
680 : : /* Look up actual value */
681 : : tp_value actual;
682 [ + + ]: 197 : if (tp_dict_lookup_n(dict, entries[entry_count].key, key_len, &actual) ==
683 : : TP_OK) {
684 : 46 : entries[entry_count].val = actual;
685 : : }
686 : 197 : entry_count++;
687 : : }
688 : : }
689 : : /* Check if we need to pop the stack */
690 [ + + ]: 237 : while (stack_top >= 0) {
691 [ + + ]: 100 : if (stack[stack_top].remaining_children > 0) {
692 : 60 : stack[stack_top].remaining_children--;
693 : 60 : key_len = stack[stack_top].key_len;
694 [ + + ]: 60 : if (stack[stack_top].remaining_children == 0) {
695 : : /* Last child of this branch: just continue to its subtree */
696 : 59 : break;
697 : : }
698 : : /* Read SKIP + distance for next child */
699 : : uint64_t skip_sym;
700 : 27 : rc = tp_bs_read_bits(reader, bps, &skip_sym);
701 [ - + ]: 27 : if (rc != TP_OK)
702 : 1 : goto done;
703 : : uint64_t skip_dist;
704 : 27 : rc = tp_bs_read_varint_u(reader, &skip_dist);
705 [ + + ]: 27 : if (rc != TP_OK)
706 : 1 : goto done;
707 : : (void)skip_dist;
708 : 26 : break;
709 : : } else {
710 : 40 : stack_top--;
711 : : }
712 : : }
713 : 1710 : continue;
714 : : }
715 : :
716 [ + + ]: 6684 : if (sym == dict->sym.ctrl_codes[TP_CTRL_END_VAL]) {
717 : : uint64_t vi;
718 : 1037 : rc = tp_bs_read_varint_u(reader, &vi);
719 [ - + ]: 1037 : if (rc != TP_OK)
720 : : break; /* LCOV_EXCL_LINE */
721 [ + - ]: 1037 : if (entry_count < num_keys) {
722 : 1037 : entries[entry_count].key = malloc(key_len + 1);
723 [ + - ]: 1037 : if (entries[entry_count].key) {
724 : 1037 : memcpy(entries[entry_count].key, key_buf, key_len);
725 : 1037 : entries[entry_count].key[key_len] = '\0';
726 : 1037 : entries[entry_count].key_len = key_len;
727 : 1037 : entries[entry_count].val = tp_value_null();
728 : : tp_value actual;
729 [ + + ]: 1037 : if (tp_dict_lookup_n(dict, entries[entry_count].key, key_len, &actual) ==
730 : : TP_OK) {
731 : 703 : entries[entry_count].val = actual;
732 : : }
733 : 1037 : entry_count++;
734 : : }
735 : : }
736 [ + + ]: 1337 : while (stack_top >= 0) {
737 [ + + ]: 1108 : if (stack[stack_top].remaining_children > 0) {
738 : 808 : stack[stack_top].remaining_children--;
739 : 808 : key_len = stack[stack_top].key_len;
740 [ + + ]: 808 : if (stack[stack_top].remaining_children == 0) {
741 : 806 : break;
742 : : }
743 : : uint64_t skip_sym;
744 : 513 : rc = tp_bs_read_bits(reader, bps, &skip_sym);
745 [ - + ]: 513 : if (rc != TP_OK)
746 : 2 : goto done;
747 : : uint64_t skip_dist;
748 : 513 : rc = tp_bs_read_varint_u(reader, &skip_dist);
749 [ + + ]: 513 : if (rc != TP_OK)
750 : 2 : goto done;
751 : : (void)skip_dist;
752 : 511 : break;
753 : : } else {
754 : 300 : stack_top--;
755 : : }
756 : : }
757 : 1035 : continue;
758 : : }
759 : :
760 [ + + ]: 5647 : if (sym == dict->sym.ctrl_codes[TP_CTRL_BRANCH]) {
761 : : uint64_t child_count;
762 : 403 : rc = tp_bs_read_varint_u(reader, &child_count);
763 [ + + ]: 403 : if (rc != TP_OK)
764 : 2 : break;
765 : 402 : stack_top++;
766 [ - + ]: 402 : if (stack_top >= 256) {
767 : : /* LCOV_EXCL_START */
768 : : rc = TP_ERR_JSON_DEPTH;
769 : : break;
770 : : /* LCOV_EXCL_STOP */
771 : : }
772 : 402 : stack[stack_top].key_len = key_len;
773 : 402 : stack[stack_top].remaining_children = (uint32_t)child_count;
774 : : /* Read SKIP for first child (if more than 1) */
775 [ + + ]: 402 : if (child_count > 1) {
776 : 377 : stack[stack_top].remaining_children--;
777 : : uint64_t skip_sym;
778 : 377 : rc = tp_bs_read_bits(reader, bps, &skip_sym);
779 [ - + ]: 377 : if (rc != TP_OK)
780 : 1 : break;
781 : : uint64_t skip_dist;
782 : 377 : rc = tp_bs_read_varint_u(reader, &skip_dist);
783 [ + + ]: 377 : if (rc != TP_OK)
784 : 1 : break;
785 : : (void)skip_dist;
786 : : } else {
787 : 25 : stack[stack_top].remaining_children = 0;
788 : : }
789 : 401 : continue;
790 : : }
791 : :
792 [ + + ]: 5244 : if (sym == dict->sym.ctrl_codes[TP_CTRL_SKIP]) {
793 : : /* Shouldn't hit this outside BRANCH handling, but skip */
794 : : uint64_t dist;
795 : 79 : rc = tp_bs_read_varint_u(reader, &dist);
796 [ + + ]: 79 : if (rc != TP_OK)
797 : 1 : break;
798 : 78 : continue;
799 : : }
800 : :
801 : : /* Regular symbol - append to key */
802 [ + - + + ]: 5165 : if (!dict->sym.code_is_ctrl[sym < 256 ? sym : 0]) {
803 [ + - ]: 5087 : uint8_t byte_val = (sym < 256) ? dict->sym.reverse_map[sym] : 0;
804 [ + - ]: 5087 : if (key_len < sizeof(key_buf) - 1) {
805 : 5087 : key_buf[key_len++] = (char)byte_val;
806 : : }
807 : : }
808 : : }
809 : :
810 [ + + ]: 240 : if (rc != TP_OK) {
811 : 10 : tp_bs_reader_destroy(&reader);
812 : 10 : free(stack);
813 : 10 : free(entries);
814 : 10 : tp_dict_close(&dict);
815 : 10 : return rc;
816 : : }
817 : :
818 : 230 : done:
819 : 233 : tp_bs_reader_destroy(&reader);
820 : 233 : free(stack);
821 : :
822 : : /* Look up root type */
823 : 233 : *out_root_type = TP_JSON_ROOT_OBJECT; /* default */
824 : : tp_value root_val;
825 [ + + ]: 233 : if (tp_dict_lookup(dict, TP_JSON_META_ROOT, &root_val) == TP_OK) {
826 [ + + ]: 141 : if (root_val.type == TP_UINT)
827 : 120 : *out_root_type = (uint32_t)root_val.data.uint_val;
828 [ - + ]: 21 : else if (root_val.type == TP_INT)
829 : : *out_root_type = (uint32_t)root_val.data.int_val; /* LCOV_EXCL_LINE */
830 : : }
831 : :
832 : : /* Filter out metadata keys */
833 : 233 : size_t write_pos = 0;
834 [ + + ]: 1428 : for (size_t i = 0; i < entry_count; i++) {
835 [ + + + + ]: 1195 : if (entries[i].key_len > 0 && entries[i].key[0] == '\x01') {
836 : 204 : free(entries[i].key);
837 : 204 : continue;
838 : : }
839 [ + + ]: 991 : if (write_pos != i)
840 : 827 : entries[write_pos] = entries[i];
841 : 991 : write_pos++;
842 : : }
843 : 233 : entry_count = write_pos;
844 : :
845 : : /* Sort entries */
846 [ + + ]: 233 : if (entry_count > 1)
847 : 203 : qsort(entries, entry_count, sizeof(flat_entry), flat_entry_cmp);
848 : :
849 : 233 : tp_dict_close(&dict);
850 : :
851 : 233 : *out_entries = entries;
852 : 233 : *out_count = entry_count;
853 : 233 : return TP_OK;
854 : : }
855 : :
856 : : /* ── One-shot decode ─────────────────────────────────────────────────── */
857 : :
858 : 317 : static tp_result decode_impl(const uint8_t *buf, size_t buf_len, const char *indent,
859 : : char **json_str, size_t *json_len)
860 : : {
861 : 317 : flat_entry *entries = NULL;
862 : 317 : size_t count = 0;
863 : 317 : uint32_t root_type = TP_JSON_ROOT_OBJECT;
864 : :
865 : 317 : tp_result rc = extract_entries(buf, buf_len, &entries, &count, &root_type);
866 [ + + ]: 317 : if (rc != TP_OK)
867 : 84 : return rc;
868 : :
869 : : strbuf sb;
870 : 233 : sb_init(&sb);
871 : :
872 [ + + ]: 233 : if (count == 0) {
873 [ + + ]: 5 : if (root_type == TP_JSON_ROOT_ARRAY)
874 : 1 : rc = sb_append_str(&sb, "[]");
875 : : else
876 : 4 : rc = sb_append_str(&sb, "{}");
877 : : } else {
878 : 228 : rc = emit_json(&sb, entries, count, "", 0, indent, 0);
879 : : }
880 : :
881 : : /* Free entries */
882 [ + + ]: 1224 : for (size_t i = 0; i < count; i++)
883 : 991 : free(entries[i].key);
884 : 233 : free(entries);
885 : :
886 [ - + ]: 233 : if (rc != TP_OK) {
887 : : /* LCOV_EXCL_START */
888 : : sb_free(&sb);
889 : : return rc;
890 : : /* LCOV_EXCL_STOP */
891 : : }
892 : :
893 : : /* NUL-terminate */
894 : 233 : rc = sb_appendc(&sb, '\0');
895 [ - + ]: 233 : if (rc != TP_OK) {
896 : : /* LCOV_EXCL_START */
897 : : sb_free(&sb);
898 : : return rc;
899 : : /* LCOV_EXCL_STOP */
900 : : }
901 : :
902 : 233 : *json_str = sb.data;
903 : 233 : *json_len = sb.len - 1; /* exclude NUL */
904 : 233 : return TP_OK;
905 : : }
906 : :
907 : 316 : tp_result tp_json_decode(const uint8_t *buf, size_t buf_len, char **json_str, size_t *json_len)
908 : : {
909 [ + + + + : 316 : if (!buf || !json_str || !json_len)
+ + ]
910 : 6 : return TP_ERR_INVALID_PARAM;
911 : :
912 : 310 : return decode_impl(buf, buf_len, NULL, json_str, json_len);
913 : : }
914 : :
915 : : /* ── Pretty-printed decode ───────────────────────────────────────────── */
916 : :
917 : 13 : tp_result tp_json_decode_pretty(const uint8_t *buf, size_t buf_len, const char *indent,
918 : : char **json_str, size_t *json_len)
919 : : {
920 [ + - + + : 13 : if (!buf || !indent || !json_str || !json_len)
+ + + + ]
921 : 6 : return TP_ERR_INVALID_PARAM;
922 : :
923 : 7 : return decode_impl(buf, buf_len, indent, json_str, json_len);
924 : : }
|