LCOV - code coverage report
Current view: top level - src/json - json_decode.c (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 453 453
Test Date: 2026-09-17 22:59:44 Functions: 100.0 % 16 16
Branches: 93.0 % 298 277

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

Generated by: LCOV version 2.0-1