LCOV - code coverage report
Current view: top level - src/json - json_decode.c (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 451 451
Test Date: 2026-03-09 04:08:32 Functions: 100.0 % 16 16
Branches: 83.7 % 368 308

             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                 :             : }
        

Generated by: LCOV version 2.0-1