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 : 2153 : static void sb_init(strbuf *sb)
28 : : {
29 : 2153 : sb->data = NULL;
30 : 2153 : sb->len = 0;
31 : 2153 : sb->cap = 0;
32 : 2153 : }
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 : 233867 : static tp_result sb_grow(strbuf *sb, size_t need)
45 : : {
46 [ + + ]: 233867 : if (sb->len + need <= sb->cap)
47 : 231643 : return TP_OK;
48 [ + + ]: 2224 : size_t new_cap = sb->cap == 0 ? 256 : sb->cap;
49 [ + + ]: 2295 : while (new_cap < sb->len + need)
50 : 71 : new_cap *= 2;
51 : : /* Allocation failure paths are excluded from coverage (LCOV_EXCL). */
52 : 2224 : char *p = realloc(sb->data, new_cap);
53 : 2224 : if (!p) /* LCOV_EXCL_BR_LINE */
54 : : return TP_ERR_ALLOC; /* LCOV_EXCL_LINE */
55 : 2224 : sb->data = p;
56 : 2224 : sb->cap = new_cap;
57 : 2224 : return TP_OK;
58 : : }
59 : :
60 : 233867 : static tp_result sb_append(strbuf *sb, const char *s, size_t n)
61 : : {
62 : 233867 : tp_result rc = sb_grow(sb, n);
63 : 233867 : if (rc != TP_OK) /* LCOV_EXCL_BR_LINE */
64 : : return rc; /* LCOV_EXCL_LINE */
65 : 233867 : memcpy(sb->data + sb->len, s, n);
66 : 233867 : sb->len += n;
67 : 233867 : return TP_OK;
68 : : }
69 : :
70 : 204787 : static tp_result sb_appendc(strbuf *sb, char c)
71 : : {
72 : 204787 : return sb_append(sb, &c, 1);
73 : : }
74 : :
75 : 9958 : static tp_result sb_append_str(strbuf *sb, const char *s)
76 : : {
77 : 9958 : 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 : 18896 : static int flat_entry_cmp(const void *a, const void *b)
90 : : {
91 : 18896 : const flat_entry *ea = (const flat_entry *)a;
92 : 18896 : const flat_entry *eb = (const flat_entry *)b;
93 : 18896 : size_t min_len = ea->key_len < eb->key_len ? ea->key_len : eb->key_len;
94 : 18896 : int c = memcmp(ea->key, eb->key, min_len);
95 [ + + ]: 18896 : if (c != 0)
96 : 18049 : return c;
97 [ + + ]: 847 : if (ea->key_len < eb->key_len)
98 : 591 : return -1;
99 [ + + ]: 256 : if (ea->key_len > eb->key_len)
100 : 123 : return 1;
101 : 133 : return 0;
102 : : }
103 : :
104 : : /* ── JSON string escaping ────────────────────────────────────────────── */
105 : :
106 : 19578 : static tp_result sb_append_json_string(strbuf *sb, const char *str, size_t len)
107 : : {
108 : 19578 : tp_result rc = sb_appendc(sb, '"');
109 : 19578 : if (rc != TP_OK) /* LCOV_EXCL_BR_LINE */
110 : : return rc; /* LCOV_EXCL_LINE */
111 [ + + ]: 126833 : for (size_t i = 0; i < len; i++) {
112 : 107255 : unsigned char c = (unsigned char)str[i];
113 [ + + + + : 107255 : switch (c) {
+ + + + ]
114 : 9 : case '"':
115 : 9 : rc = sb_append(sb, "\\\"", 2);
116 : 9 : 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 : 3 : case '\t':
133 : 3 : rc = sb_append(sb, "\\t", 2);
134 : 3 : break;
135 : 107230 : default:
136 [ + + ]: 107230 : if (c < 0x20) {
137 : : char esc[8];
138 : 2892 : snprintf(esc, sizeof(esc), "\\u%04x", c);
139 : 2892 : rc = sb_append(sb, esc, 6);
140 : : } else {
141 : 104338 : rc = sb_appendc(sb, (char)c);
142 : : }
143 : : }
144 : 107255 : if (rc != TP_OK) /* LCOV_EXCL_BR_LINE */
145 : : return rc; /* LCOV_EXCL_LINE */
146 : : }
147 : 19578 : return sb_appendc(sb, '"');
148 : : }
149 : :
150 : : /* ── Value to JSON text ──────────────────────────────────────────────── */
151 : :
152 : 13594 : static tp_result sb_append_value(strbuf *sb, const tp_value *val)
153 : : {
154 : : char tmp[64];
155 [ + + + + : 13594 : switch (val->type) {
+ + + + ]
156 : 3642 : case TP_NULL:
157 : 3642 : return sb_append_str(sb, "null");
158 : 1702 : case TP_BOOL:
159 [ + + ]: 1702 : return sb_append_str(sb, val->data.bool_val ? "true" : "false");
160 : 1872 : case TP_INT:
161 : 1872 : snprintf(tmp, sizeof(tmp), "%lld", (long long)val->data.int_val);
162 : 1872 : return sb_append_str(sb, tmp);
163 : 38 : case TP_UINT:
164 : 38 : snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)val->data.uint_val);
165 : 38 : return sb_append_str(sb, tmp);
166 : 26 : case TP_FLOAT32: {
167 : 26 : double d = (double)val->data.float32_val;
168 : 26 : snprintf(tmp, sizeof(tmp), "%g", d);
169 : : /* Ensure it has a decimal point for JSON */
170 [ + + + - : 26 : if (!strchr(tmp, '.') && !strchr(tmp, 'e') && !strchr(tmp, 'E'))
+ - ]
171 : 1 : snprintf(tmp, sizeof(tmp), "%g.0", d);
172 : 26 : return sb_append_str(sb, tmp);
173 : : }
174 : 1691 : case TP_FLOAT64:
175 : 1691 : snprintf(tmp, sizeof(tmp), "%.17g", val->data.float64_val);
176 : 1691 : return sb_append_str(sb, tmp);
177 : 4571 : case TP_STRING:
178 [ + - ]: 4571 : if (val->data.string_val.str)
179 : 4571 : return sb_append_json_string(sb, val->data.string_val.str,
180 : 4571 : val->data.string_val.str_len);
181 : : return sb_append_str(sb, "\"\""); /* LCOV_EXCL_LINE */
182 : 52 : default:
183 : 52 : 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 : 25170 : static bool is_array_index(const char *key, size_t key_len, size_t pos)
200 : : {
201 : 25170 : if (pos >= key_len) /* LCOV_EXCL_BR_LINE */
202 : : return false; /* LCOV_EXCL_LINE */
203 : 25170 : 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 : 78971 : static size_t next_segment(const char *key, size_t key_len, size_t pos, bool *is_idx)
209 : : {
210 : 78971 : *is_idx = false;
211 : 78971 : if (pos >= key_len) /* LCOV_EXCL_BR_LINE */
212 : : return 0; /* LCOV_EXCL_LINE */
213 [ + + ]: 78971 : if (key[pos] == '[') {
214 : 10183 : *is_idx = true;
215 : 10183 : size_t end = pos + 1;
216 [ + + + + ]: 23722 : while (end < key_len && key[end] != ']')
217 : 13539 : end++;
218 [ + + ]: 10183 : if (end < key_len)
219 : 9341 : end++; /* include ']' */
220 : 10183 : return end - pos;
221 : : }
222 : : /* Regular key segment: up to '.' or '[' or end */
223 : 68788 : size_t end = pos;
224 [ + + + + : 423400 : while (end < key_len && key[end] != '.' && key[end] != '[')
+ + ]
225 : 354612 : end++;
226 : 68788 : return end - pos;
227 : : }
228 : :
229 : 11775 : static tp_result emit_indent(strbuf *sb, const char *indent, int depth)
230 : : {
231 : 11775 : if (!indent) /* LCOV_EXCL_BR_LINE */
232 : : return TP_OK; /* LCOV_EXCL_LINE */
233 : 11775 : tp_result rc = sb_appendc(sb, '\n');
234 : 11775 : if (rc != TP_OK) /* LCOV_EXCL_BR_LINE */
235 : : return rc; /* LCOV_EXCL_LINE */
236 : 11775 : size_t ilen = strlen(indent);
237 [ + + ]: 27980 : for (int i = 0; i < depth; i++) {
238 : 16205 : rc = sb_append(sb, indent, ilen);
239 : 16205 : if (rc != TP_OK) /* LCOV_EXCL_BR_LINE */
240 : : return rc; /* LCOV_EXCL_LINE */
241 : : }
242 : 11775 : return TP_OK;
243 : : }
244 : :
245 : 7410 : 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 : 7410 : bool all_array = true;
256 : 7410 : bool any_entry = false;
257 : :
258 [ + + ]: 59410 : for (size_t i = 0; i < count; i++) {
259 [ + + ]: 52000 : if (entries[i].key_len < prefix_len)
260 : 6552 : continue;
261 [ + + + + ]: 45448 : if (prefix_len > 0 && memcmp(entries[i].key, prefix, prefix_len) != 0)
262 : 19751 : continue;
263 : : /* Skip metadata keys — already filtered by extract_entries */
264 : 25697 : if (entries[i].key_len > 0 && entries[i].key[0] == '\x01') /* LCOV_EXCL_BR_LINE */
265 : : continue; /* LCOV_EXCL_LINE */
266 : :
267 : 25697 : size_t pos = prefix_len;
268 [ + + + + ]: 25697 : if (pos < entries[i].key_len && entries[i].key[pos] == '.')
269 : 6006 : pos++;
270 [ + + ]: 25697 : if (pos >= entries[i].key_len)
271 : 527 : continue; /* exact match = leaf, shouldn't be here */
272 : :
273 : 25170 : any_entry = true;
274 [ + + ]: 25170 : if (!is_array_index(entries[i].key, entries[i].key_len, pos))
275 : 21225 : all_array = false;
276 : : }
277 : :
278 [ + + ]: 7410 : if (!any_entry) {
279 : : /* Check if there's an exact prefix match (leaf value) */
280 [ + + ]: 8109 : for (size_t i = 0; i < count; i++) {
281 [ + + + + ]: 7185 : if (entries[i].key_len == prefix_len &&
282 [ - + ]: 205 : (prefix_len == 0 || memcmp(entries[i].key, prefix, prefix_len) == 0)) {
283 [ + - ]: 6 : if (entries[i].key[0] != '\x01')
284 : 6 : return sb_append_value(sb, &entries[i].val);
285 : : }
286 : : }
287 : 924 : 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 : 6480 : segment_info *segments = NULL;
298 : 6480 : size_t num_segments = 0;
299 : 6480 : size_t seg_cap = 0;
300 : :
301 [ + + ]: 51288 : for (size_t i = 0; i < count; i++) {
302 [ + + ]: 44808 : if (entries[i].key_len < prefix_len)
303 : 19643 : continue;
304 [ + + + + ]: 43058 : if (prefix_len > 0 && memcmp(entries[i].key, prefix, prefix_len) != 0)
305 : 17384 : continue;
306 : 25674 : if (entries[i].key_len > 0 && entries[i].key[0] == '\x01') /* LCOV_EXCL_BR_LINE */
307 : : continue; /* LCOV_EXCL_LINE */
308 : :
309 : 25674 : size_t pos = prefix_len;
310 [ + + + + ]: 25674 : if (pos < entries[i].key_len && entries[i].key[pos] == '.')
311 : 5996 : pos++;
312 [ + + ]: 25674 : if (pos >= entries[i].key_len)
313 : 504 : continue;
314 : :
315 : 25170 : bool is_idx = false;
316 : 25170 : size_t seg_len = next_segment(entries[i].key, entries[i].key_len, pos, &is_idx);
317 [ + + ]: 25170 : if (seg_len == 0)
318 : 5 : continue;
319 : :
320 : : /* Check if already in segments list */
321 : 25165 : bool dup = false;
322 [ + + ]: 53801 : for (size_t s = 0; s < num_segments; s++) {
323 : 34945 : const flat_entry *se = &entries[segments[s].seg_start];
324 : 34945 : size_t sp = prefix_len;
325 [ + - + + ]: 34945 : if (sp < se->key_len && se->key[sp] == '.')
326 : 6308 : sp++;
327 : 34945 : bool si = false;
328 : 34945 : size_t sl = next_segment(se->key, se->key_len, sp, &si);
329 [ + + + + ]: 34945 : if (sl == seg_len && memcmp(se->key + sp, entries[i].key + pos, seg_len) == 0) {
330 : 6309 : dup = true;
331 : 6309 : break;
332 : : }
333 : : }
334 : :
335 [ + + ]: 25165 : if (!dup) {
336 [ + + ]: 18856 : if (num_segments >= seg_cap) {
337 [ + + ]: 6479 : seg_cap = seg_cap == 0 ? 16 : seg_cap * 2;
338 : 6479 : segment_info *new_segs = realloc(segments, seg_cap * sizeof(segment_info));
339 : 6479 : if (!new_segs) { /* LCOV_EXCL_BR_LINE */
340 : : /* LCOV_EXCL_START */
341 : : free(segments);
342 : : return TP_ERR_ALLOC;
343 : : /* LCOV_EXCL_STOP */
344 : : }
345 : 6479 : segments = new_segs;
346 : : }
347 : 18856 : segments[num_segments].seg_start = i;
348 : 18856 : segments[num_segments].seg_len = seg_len;
349 : 18856 : segments[num_segments].is_idx = is_idx;
350 : 18856 : num_segments++;
351 : : }
352 : : }
353 : :
354 : : tp_result rc;
355 : :
356 [ + + ]: 6480 : if (all_array) {
357 : 1884 : rc = sb_appendc(sb, '[');
358 : 1884 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
359 : : /* LCOV_EXCL_START */
360 : : free(segments);
361 : : return rc;
362 : : /* LCOV_EXCL_STOP */
363 : : }
364 : :
365 [ + + ]: 5733 : for (size_t s = 0; s < num_segments; s++) {
366 [ + + ]: 3849 : if (s > 0) {
367 : 1965 : rc = sb_appendc(sb, ',');
368 : 1965 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
369 : : /* LCOV_EXCL_START */
370 : : free(segments);
371 : : return rc;
372 : : /* LCOV_EXCL_STOP */
373 : : }
374 : : }
375 [ + + ]: 3849 : if (indent) {
376 : 1788 : rc = emit_indent(sb, indent, depth + 1);
377 : 1788 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
378 : : /* LCOV_EXCL_START */
379 : : free(segments);
380 : : return rc;
381 : : /* LCOV_EXCL_STOP */
382 : : }
383 : : }
384 : :
385 : 3849 : const flat_entry *se = &entries[segments[s].seg_start];
386 : 3849 : size_t sp = prefix_len;
387 : 3849 : if (sp < se->key_len && se->key[sp] == '.') /* LCOV_EXCL_BR_LINE */
388 : : sp++; /* LCOV_EXCL_LINE */
389 : : bool si;
390 : 3849 : 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 : 3849 : size_t new_prefix_len = prefix_len;
395 [ + + ]: 3849 : if (prefix_len > 0) {
396 : 3840 : memcpy(new_prefix, prefix, prefix_len);
397 : : }
398 : 3849 : memcpy(new_prefix + new_prefix_len, se->key + sp, sl);
399 : 3849 : new_prefix_len += sl;
400 : 3849 : new_prefix[new_prefix_len] = '\0';
401 : :
402 : : /* Check if any entry is exactly this prefix (leaf) */
403 : 3849 : bool is_leaf = false;
404 : 3849 : const tp_value *leaf_val = NULL;
405 [ + + ]: 24258 : for (size_t i = 0; i < count; i++) {
406 [ + + ]: 24193 : if (entries[i].key_len == new_prefix_len &&
407 [ + + ]: 8887 : memcmp(entries[i].key, new_prefix, new_prefix_len) == 0 &&
408 [ + - ]: 3784 : entries[i].key[0] != '\x01') {
409 : 3784 : is_leaf = true;
410 : 3784 : leaf_val = &entries[i].val;
411 : 3784 : break;
412 : : }
413 : : }
414 : :
415 : : /* Check if there are sub-entries */
416 : 3849 : bool has_children = false;
417 [ + + ]: 30415 : for (size_t i = 0; i < count; i++) {
418 [ + + ]: 26712 : if (entries[i].key_len > new_prefix_len &&
419 [ + + ]: 10866 : memcmp(entries[i].key, new_prefix, new_prefix_len) == 0 &&
420 [ + - ]: 146 : entries[i].key[0] != '\x01') {
421 : 146 : has_children = true;
422 : 146 : break;
423 : : }
424 : : }
425 : :
426 [ + + + + ]: 3849 : if (is_leaf && !has_children) {
427 : 3699 : rc = sb_append_value(sb, leaf_val);
428 : : } else {
429 : 150 : rc = emit_json(sb, entries, count, new_prefix, new_prefix_len, indent, depth + 1);
430 : : }
431 : 3849 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
432 : : /* LCOV_EXCL_START */
433 : : free(segments);
434 : : return rc;
435 : : /* LCOV_EXCL_STOP */
436 : : }
437 : : }
438 : :
439 [ + + + - ]: 1884 : if (indent && num_segments > 0) {
440 : 892 : rc = emit_indent(sb, indent, depth);
441 : 892 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
442 : : /* LCOV_EXCL_START */
443 : : free(segments);
444 : : return rc;
445 : : /* LCOV_EXCL_STOP */
446 : : }
447 : : }
448 : 1884 : rc = sb_appendc(sb, ']');
449 : : } else {
450 : 4596 : rc = sb_appendc(sb, '{');
451 : 4596 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
452 : : /* LCOV_EXCL_START */
453 : : free(segments);
454 : : return rc;
455 : : /* LCOV_EXCL_STOP */
456 : : }
457 : :
458 [ + + ]: 19603 : for (size_t s = 0; s < num_segments; s++) {
459 [ + + ]: 15007 : if (s > 0) {
460 : 10413 : rc = sb_appendc(sb, ',');
461 : 10413 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
462 : : /* LCOV_EXCL_START */
463 : : free(segments);
464 : : return rc;
465 : : /* LCOV_EXCL_STOP */
466 : : }
467 : : }
468 [ + + ]: 15007 : if (indent) {
469 : 7020 : rc = emit_indent(sb, indent, depth + 1);
470 : 7020 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
471 : : /* LCOV_EXCL_START */
472 : : free(segments);
473 : : return rc;
474 : : /* LCOV_EXCL_STOP */
475 : : }
476 : : }
477 : :
478 : 15007 : const flat_entry *se = &entries[segments[s].seg_start];
479 : 15007 : size_t sp = prefix_len;
480 [ + - + + ]: 15007 : if (sp < se->key_len && se->key[sp] == '.')
481 : 5843 : sp++;
482 : : bool si;
483 : 15007 : size_t sl = next_segment(se->key, se->key_len, sp, &si);
484 : :
485 : : /* Write key */
486 : 15007 : rc = sb_append_json_string(sb, se->key + sp, sl);
487 : 15007 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
488 : : /* LCOV_EXCL_START */
489 : : free(segments);
490 : : return rc;
491 : : /* LCOV_EXCL_STOP */
492 : : }
493 : 15007 : rc = sb_appendc(sb, ':');
494 : 15007 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
495 : : /* LCOV_EXCL_START */
496 : : free(segments);
497 : : return rc;
498 : : /* LCOV_EXCL_STOP */
499 : : }
500 [ + + ]: 15007 : if (indent) {
501 : 7020 : rc = sb_appendc(sb, ' ');
502 : 7020 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
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 : 15007 : size_t new_prefix_len = prefix_len;
513 [ + + ]: 15007 : if (prefix_len > 0) {
514 : 6623 : memcpy(new_prefix, prefix, prefix_len);
515 : 6623 : new_prefix[new_prefix_len++] = '.';
516 : : }
517 : 15007 : memcpy(new_prefix + new_prefix_len, se->key + sp, sl);
518 : 15007 : new_prefix_len += sl;
519 : 15007 : new_prefix[new_prefix_len] = '\0';
520 : :
521 : : /* Check if any entry is exactly this prefix (leaf) */
522 : 15007 : bool is_leaf = false;
523 : 15007 : const tp_value *leaf_val = NULL;
524 [ + + ]: 71432 : for (size_t i = 0; i < count; i++) {
525 [ + + ]: 66598 : if (entries[i].key_len == new_prefix_len &&
526 [ + + ]: 14450 : memcmp(entries[i].key, new_prefix, new_prefix_len) == 0 &&
527 [ + - ]: 10173 : entries[i].key[0] != '\x01') {
528 : 10173 : is_leaf = true;
529 : 10173 : leaf_val = &entries[i].val;
530 : 10173 : break;
531 : : }
532 : : }
533 : :
534 : : /* Check if there are sub-entries */
535 : 15007 : bool has_children = false;
536 [ + + ]: 101802 : for (size_t i = 0; i < count; i++) {
537 [ + + ]: 91003 : if (entries[i].key_len > new_prefix_len &&
538 [ + + ]: 38008 : memcmp(entries[i].key, new_prefix, new_prefix_len) == 0 &&
539 [ + - ]: 4208 : entries[i].key[0] != '\x01') {
540 : 4208 : has_children = true;
541 : 4208 : break;
542 : : }
543 : : }
544 : :
545 [ + + + + ]: 15007 : if (is_leaf && !has_children) {
546 : 9889 : rc = sb_append_value(sb, leaf_val);
547 : : } else {
548 : 5118 : rc = emit_json(sb, entries, count, new_prefix, new_prefix_len, indent, depth + 1);
549 : : }
550 : 15007 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
551 : : /* LCOV_EXCL_START */
552 : : free(segments);
553 : : return rc;
554 : : /* LCOV_EXCL_STOP */
555 : : }
556 : : }
557 : :
558 [ + + + + ]: 4596 : if (indent && num_segments > 0) {
559 : 2075 : rc = emit_indent(sb, indent, depth);
560 : 2075 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
561 : : /* LCOV_EXCL_START */
562 : : free(segments);
563 : : return rc;
564 : : /* LCOV_EXCL_STOP */
565 : : }
566 : : }
567 : 4596 : rc = sb_appendc(sb, '}');
568 : : }
569 : :
570 : 6480 : free(segments);
571 : 6480 : return rc;
572 : : }
573 : :
574 : : /* ── Extract all entries from a trp buffer ───────────────────────────── */
575 : :
576 : 2308 : 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 : 2308 : tp_dict *dict = NULL;
601 : 2308 : tp_result rc = tp_dict_open(&dict, buf, buf_len);
602 [ + + ]: 2308 : if (rc != TP_OK)
603 : 102 : return rc;
604 : :
605 : 2206 : 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 : 2206 : flat_entry *entries = calloc(num_keys, sizeof(flat_entry));
612 : 2206 : if (!entries) { /* LCOV_EXCL_BR_LINE */
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 : 2206 : walk_frame *stack = calloc(256, sizeof(walk_frame));
627 : 2206 : if (!stack) { /* LCOV_EXCL_BR_LINE */
628 : : /* LCOV_EXCL_START */
629 : : free(entries);
630 : : tp_dict_close(&dict);
631 : : return TP_ERR_ALLOC;
632 : : /* LCOV_EXCL_STOP */
633 : : }
634 : :
635 : 2206 : tp_bitstream_reader *reader = NULL;
636 : 2206 : rc = tp_bs_reader_create(&reader, buf, (uint64_t)buf_len * 8);
637 : 2206 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
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 : 2206 : rc = tp_bs_reader_seek(reader, dict->trie_start);
647 : 2206 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
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 : 2206 : size_t key_len = 0;
659 : 2206 : int stack_top = -1;
660 : 2206 : size_t entry_count = 0;
661 : 2206 : uint8_t bps = dict->sym.bits_per_symbol;
662 : :
663 : : /* DFS trie walk */
664 [ + + ]: 123164 : while (entry_count < num_keys) {
665 : : uint64_t sym_raw;
666 : 121015 : rc = tp_bs_read_bits(reader, bps, &sym_raw);
667 [ + + ]: 121015 : if (rc != TP_OK)
668 : 53 : break;
669 : 120979 : uint32_t sym = (uint32_t)sym_raw;
670 : :
671 [ + + ]: 120979 : if (sym == dict->sym.ctrl_codes[TP_CTRL_END]) {
672 : : /* Terminal without value */
673 [ + - ]: 2636 : if (entry_count < num_keys) {
674 : 2636 : entries[entry_count].key = malloc(key_len + 1);
675 [ + - ]: 2636 : if (entries[entry_count].key) {
676 : 2636 : memcpy(entries[entry_count].key, key_buf, key_len);
677 : 2636 : entries[entry_count].key[key_len] = '\0';
678 : 2636 : entries[entry_count].key_len = key_len;
679 : 2636 : entries[entry_count].val = tp_value_null();
680 : : /* Look up actual value */
681 : : tp_value actual;
682 [ + + ]: 2636 : if (tp_dict_lookup_n(dict, entries[entry_count].key, key_len, &actual) ==
683 : : TP_OK) {
684 : 1721 : entries[entry_count].val = actual;
685 : : }
686 : 2636 : entry_count++;
687 : : }
688 : : }
689 : : /* Check if we need to pop the stack */
690 [ + + ]: 4562 : while (stack_top >= 0) {
691 [ + + ]: 4223 : if (stack[stack_top].remaining_children > 0) {
692 : 2297 : stack[stack_top].remaining_children--;
693 : 2297 : key_len = stack[stack_top].key_len;
694 [ + + ]: 2297 : if (stack[stack_top].remaining_children == 0) {
695 : : /* Last child of this branch: just continue to its subtree */
696 : 2295 : break;
697 : : }
698 : : /* Read SKIP + distance for next child */
699 : : uint64_t skip_sym;
700 : 473 : rc = tp_bs_read_bits(reader, bps, &skip_sym);
701 [ + + ]: 473 : if (rc != TP_OK)
702 : 2 : goto done;
703 : : uint64_t skip_dist;
704 : 472 : rc = tp_bs_read_varint_u(reader, &skip_dist);
705 [ + + ]: 472 : if (rc != TP_OK)
706 : 1 : goto done;
707 : : (void)skip_dist;
708 : 471 : break;
709 : : } else {
710 : 1926 : stack_top--;
711 : : }
712 : : }
713 : 25134 : continue;
714 : : }
715 : :
716 [ + + ]: 118343 : if (sym == dict->sym.ctrl_codes[TP_CTRL_END_VAL]) {
717 : : uint64_t vi;
718 : 14063 : rc = tp_bs_read_varint_u(reader, &vi);
719 [ + + ]: 14063 : if (rc != TP_OK)
720 : : break; /* LCOV_EXCL_LINE */
721 [ + - ]: 14058 : if (entry_count < num_keys) {
722 : 14058 : entries[entry_count].key = malloc(key_len + 1);
723 [ + - ]: 14058 : if (entries[entry_count].key) {
724 : 14058 : memcpy(entries[entry_count].key, key_buf, key_len);
725 : 14058 : entries[entry_count].key[key_len] = '\0';
726 : 14058 : entries[entry_count].key_len = key_len;
727 : 14058 : entries[entry_count].val = tp_value_null();
728 : : tp_value actual;
729 [ + + ]: 14058 : if (tp_dict_lookup_n(dict, entries[entry_count].key, key_len, &actual) ==
730 : : TP_OK) {
731 : 12052 : entries[entry_count].val = actual;
732 : : }
733 : 14058 : entry_count++;
734 : : }
735 : : }
736 [ + + ]: 19662 : while (stack_top >= 0) {
737 [ + + ]: 17602 : if (stack[stack_top].remaining_children > 0) {
738 : 11998 : stack[stack_top].remaining_children--;
739 : 11998 : key_len = stack[stack_top].key_len;
740 [ + + ]: 11998 : if (stack[stack_top].remaining_children == 0) {
741 : 11996 : break;
742 : : }
743 : : uint64_t skip_sym;
744 : 6220 : rc = tp_bs_read_bits(reader, bps, &skip_sym);
745 [ - + ]: 6220 : if (rc != TP_OK)
746 : 2 : goto done;
747 : : uint64_t skip_dist;
748 : 6220 : rc = tp_bs_read_varint_u(reader, &skip_dist);
749 [ + + ]: 6220 : if (rc != TP_OK)
750 : 2 : goto done;
751 : : (void)skip_dist;
752 : 6218 : break;
753 : : } else {
754 : 5604 : stack_top--;
755 : : }
756 : : }
757 : 14056 : continue;
758 : : }
759 : :
760 [ + + ]: 104280 : if (sym == dict->sym.ctrl_codes[TP_CTRL_BRANCH]) {
761 : : uint64_t child_count;
762 : 8023 : rc = tp_bs_read_varint_u(reader, &child_count);
763 [ + + ]: 8023 : if (rc != TP_OK)
764 : 9 : break;
765 : 8018 : stack_top++;
766 : 8018 : if (stack_top >= 256) { /* LCOV_EXCL_BR_LINE */
767 : : /* LCOV_EXCL_START */
768 : : rc = TP_ERR_JSON_DEPTH;
769 : : break;
770 : : /* LCOV_EXCL_STOP */
771 : : }
772 : 8018 : stack[stack_top].key_len = key_len;
773 : 8018 : stack[stack_top].remaining_children = (uint32_t)child_count;
774 : : /* Read SKIP for first child (if more than 1) */
775 [ + + ]: 8018 : if (child_count > 1) {
776 : 7944 : stack[stack_top].remaining_children--;
777 : : uint64_t skip_sym;
778 : 7944 : rc = tp_bs_read_bits(reader, bps, &skip_sym);
779 [ + + ]: 7944 : if (rc != TP_OK)
780 : 4 : break;
781 : : uint64_t skip_dist;
782 : 7943 : rc = tp_bs_read_varint_u(reader, &skip_dist);
783 [ + + ]: 7943 : if (rc != TP_OK)
784 : 3 : break;
785 : : (void)skip_dist;
786 : : } else {
787 : 74 : stack[stack_top].remaining_children = 0;
788 : : }
789 : 8014 : continue;
790 : : }
791 : :
792 [ + + ]: 96257 : if (sym == dict->sym.ctrl_codes[TP_CTRL_SKIP]) {
793 : : /* Shouldn't hit this outside BRANCH handling, but skip */
794 : : uint64_t dist;
795 : 433 : rc = tp_bs_read_varint_u(reader, &dist);
796 [ + + ]: 433 : if (rc != TP_OK)
797 : 3 : break;
798 : 430 : continue;
799 : : }
800 : :
801 : : /* Regular symbol - append to key */
802 [ + - + + ]: 95824 : if (!dict->sym.code_is_ctrl[sym < 256 ? sym : 0]) {
803 [ + - ]: 95373 : uint8_t byte_val = (sym < 256) ? dict->sym.reverse_map[sym] : 0;
804 [ + - ]: 95373 : if (key_len < sizeof(key_buf) - 1) {
805 : 95373 : key_buf[key_len++] = (char)byte_val;
806 : : }
807 : : }
808 : : }
809 : :
810 [ + + ]: 2202 : if (rc != TP_OK) {
811 : 53 : tp_bs_reader_destroy(&reader);
812 : 53 : free(stack);
813 : : /* Each key was allocated on its own; freeing just the array leaks
814 : : every key the walk got through before the input went bad. */
815 [ + + ]: 210 : for (size_t i = 0; i < entry_count; i++)
816 : 157 : free(entries[i].key);
817 : 53 : free(entries);
818 : 53 : tp_dict_close(&dict);
819 : 53 : return rc;
820 : : }
821 : :
822 : 2149 : done:
823 : 2153 : tp_bs_reader_destroy(&reader);
824 : 2153 : free(stack);
825 : :
826 : : /* Look up root type */
827 : 2153 : *out_root_type = TP_JSON_ROOT_OBJECT; /* default */
828 : : tp_value root_val;
829 [ + + ]: 2153 : if (tp_dict_lookup(dict, TP_JSON_META_ROOT, &root_val) == TP_OK) {
830 [ + + ]: 1845 : if (root_val.type == TP_UINT)
831 : 1804 : *out_root_type = (uint32_t)root_val.data.uint_val;
832 [ + + ]: 41 : else if (root_val.type == TP_INT)
833 : : *out_root_type = (uint32_t)root_val.data.int_val; /* LCOV_EXCL_LINE */
834 : : }
835 : :
836 : : /* Filter out metadata keys */
837 : 2153 : size_t write_pos = 0;
838 [ + + ]: 18690 : for (size_t i = 0; i < entry_count; i++) {
839 [ + + + + ]: 16537 : if (entries[i].key_len > 0 && entries[i].key[0] == '\x01') {
840 : 2161 : free(entries[i].key);
841 : 2161 : continue;
842 : : }
843 [ + + ]: 14376 : if (write_pos != i)
844 : 13775 : entries[write_pos] = entries[i];
845 : 14376 : write_pos++;
846 : : }
847 : 2153 : entry_count = write_pos;
848 : :
849 : : /* Sort entries */
850 [ + + ]: 2153 : if (entry_count > 1)
851 : 2112 : qsort(entries, entry_count, sizeof(flat_entry), flat_entry_cmp);
852 : :
853 : 2153 : tp_dict_close(&dict);
854 : :
855 : 2153 : *out_entries = entries;
856 : 2153 : *out_count = entry_count;
857 : 2153 : return TP_OK;
858 : : }
859 : :
860 : : /* ── One-shot decode ─────────────────────────────────────────────────── */
861 : :
862 : 2308 : static tp_result decode_impl(const uint8_t *buf, size_t buf_len, const char *indent,
863 : : char **json_str, size_t *json_len)
864 : : {
865 : 2308 : flat_entry *entries = NULL;
866 : 2308 : size_t count = 0;
867 : 2308 : uint32_t root_type = TP_JSON_ROOT_OBJECT;
868 : :
869 : 2308 : tp_result rc = extract_entries(buf, buf_len, &entries, &count, &root_type);
870 [ + + ]: 2308 : if (rc != TP_OK)
871 : 155 : return rc;
872 : :
873 : : strbuf sb;
874 : 2153 : sb_init(&sb);
875 : :
876 [ + + ]: 2153 : if (count == 0) {
877 [ + + ]: 11 : if (root_type == TP_JSON_ROOT_ARRAY)
878 : 1 : rc = sb_append_str(&sb, "[]");
879 : : else
880 : 10 : rc = sb_append_str(&sb, "{}");
881 : : } else {
882 : 2142 : rc = emit_json(&sb, entries, count, "", 0, indent, 0);
883 : : }
884 : :
885 : : /* Free entries */
886 [ + + ]: 16529 : for (size_t i = 0; i < count; i++)
887 : 14376 : free(entries[i].key);
888 : 2153 : free(entries);
889 : :
890 : 2153 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
891 : : /* LCOV_EXCL_START */
892 : : sb_free(&sb);
893 : : return rc;
894 : : /* LCOV_EXCL_STOP */
895 : : }
896 : :
897 : : /* NUL-terminate */
898 : 2153 : rc = sb_appendc(&sb, '\0');
899 : 2153 : if (rc != TP_OK) { /* LCOV_EXCL_BR_LINE */
900 : : /* LCOV_EXCL_START */
901 : : sb_free(&sb);
902 : : return rc;
903 : : /* LCOV_EXCL_STOP */
904 : : }
905 : :
906 : 2153 : *json_str = sb.data;
907 : 2153 : *json_len = sb.len - 1; /* exclude NUL */
908 : 2153 : return TP_OK;
909 : : }
910 : :
911 : 1347 : tp_result tp_json_decode(const uint8_t *buf, size_t buf_len, char **json_str, size_t *json_len)
912 : : {
913 [ + + + + : 1347 : if (!buf || !json_str || !json_len)
+ + ]
914 : 6 : return TP_ERR_INVALID_PARAM;
915 : :
916 : 1341 : return decode_impl(buf, buf_len, NULL, json_str, json_len);
917 : : }
918 : :
919 : : /* ── Pretty-printed decode ───────────────────────────────────────────── */
920 : :
921 : 973 : tp_result tp_json_decode_pretty(const uint8_t *buf, size_t buf_len, const char *indent,
922 : : char **json_str, size_t *json_len)
923 : : {
924 [ + - + + : 973 : if (!buf || !indent || !json_str || !json_len)
+ + + + ]
925 : 6 : return TP_ERR_INVALID_PARAM;
926 : :
927 : 967 : return decode_impl(buf, buf_len, indent, json_str, json_len);
928 : : }
|