File Coverage

file_raw_json.c
Criterion Covered Total %
statement 392 473 82.8
branch 245 424 57.7
condition n/a
subroutine n/a
pod n/a
total 637 897 71.0


line stmt bran cond sub pod time code
1             /*
2             * file_raw_json.c - File::Raw::JSON value-mapping + JSONL brace-balancer
3             */
4              
5             #include "file_raw_json.h"
6              
7             #include
8             #include
9             #include
10             #include
11             #include "tie_orderedhash.h"
12              
13             /* ============================================================
14             * Defaults
15             * ============================================================ */
16              
17             void
18 352           json_options_defaults(json_options_t *o)
19             {
20 352           o->mode = JSON_MODE_DOCUMENT;
21 352           o->pretty = 0;
22 352           o->indent = 2;
23 352           o->sort_keys = 0;
24 352           o->canonical = 0;
25 352           o->utf8 = 1;
26 352           o->relaxed = 0;
27 352           o->allow_nonref = 1;
28 352           o->allow_nan_inf = 0;
29 352           o->ordered = 0;
30 352           o->max_depth = 512;
31 352           o->eol[0] = '\n';
32 352           o->eol[1] = '\0';
33 352           o->eol_len = 1;
34 352           }
35              
36             /* ============================================================
37             * JSONL brace-balancer
38             *
39             * Walk a buffer, find the byte range of the next balanced top-level
40             * JSON value (object or array). Skip leading whitespace; track string
41             * state and escape state; bail on truncation with NEED_MORE; bail on
42             * a non-opener with NO_OPENER.
43             *
44             * Equivalent to JSON::Lines's recursive Perl regex but:
45             * - O(n) byte scan with no backtracking
46             * - no PCRE2 dep
47             * - resumable: caller can retry with a longer buffer after NEED_MORE
48             * ============================================================ */
49              
50             jsonl_scan_t
51 6137           json_jsonl_next(const char *buf, STRLEN len,
52             STRLEN *out_start, STRLEN *out_end, STRLEN *next_pos)
53             {
54 6137           STRLEN i = 0;
55 6137           int depth = 0;
56 6137           int in_string = 0;
57 6137           int prev_backslash = 0;
58             char c;
59              
60             /* Skip ASCII whitespace before the value. */
61 6143 100         while (i < len) {
62 6142           c = buf[i];
63 6142 100         if (c != ' ' && c != '\t' && c != '\n' && c != '\r') break;
    50          
    100          
    50          
64 6           i++;
65             }
66 6137 100         if (i >= len) {
67 1           *out_start = i;
68 1           *out_end = i;
69 1           *next_pos = i;
70 1           return JSONL_NO_OPENER; /* only whitespace */
71             }
72              
73 6136           c = buf[i];
74 6136 100         if (c != '[' && c != '{') {
    50          
75             /* Non-opener: caller decides whether to skip or croak. */
76 0           *out_start = i;
77 0           *out_end = i;
78 0           *next_pos = i;
79 0           return JSONL_NO_OPENER;
80             }
81              
82 6136           *out_start = i;
83 6136           depth = 1;
84 6136           i++;
85              
86 265539 100         while (i < len) {
87 265534           c = buf[i];
88 265534 100         if (in_string) {
89 166139 100         if (prev_backslash) {
90 8           prev_backslash = 0;
91 166131 100         } else if (c == '\\') {
92 8           prev_backslash = 1;
93 166123 100         } else if (c == '"') {
94 26276           in_string = 0;
95             }
96 166139           i++;
97 166139           continue;
98             }
99 99395           switch (c) {
100 2034           case '[': case '{':
101 2034           depth++;
102 2034           break;
103 8163           case ']': case '}':
104 8163           depth--;
105 8163 100         if (depth == 0) {
106 6131           *out_end = i + 1;
107             /* Skip trailing whitespace so next_pos points at the
108             * next value (or end of buffer). */
109 6131           i++;
110 12235 100         while (i < len) {
111 12186           char w = buf[i];
112 12186 100         if (w != ' ' && w != '\t' && w != '\n' && w != '\r')
    50          
    100          
    50          
113 6082           break;
114 6104           i++;
115             }
116 6131           *next_pos = i;
117 6131           return JSONL_FOUND;
118             }
119 2032           break;
120 26279           case '"':
121 26279           in_string = 1;
122 26279           break;
123 62919           default:
124             /* literal data */
125 62919           break;
126             }
127 93264           i++;
128             }
129              
130             /* Hit EOF mid-value: caller must buffer the tail and retry. */
131 5           return JSONL_NEED_MORE;
132             }
133              
134             /* ============================================================
135             * Value mapping: yyjson_val -> Perl SV
136             *
137             * Recursive walker. Caller owns the returned SV (refcount 1).
138             * boolean_stash, if non-NULL, is the HV* of the class to bless
139             * true/false sentinels into.
140             * ============================================================ */
141              
142             static SV *make_bool_sv(pTHX_ int truth, HV *stash);
143             static HV *make_ordered_hv(pTHX);
144             static void ordered_hv_set(pTHX_ HV *hv, const char *key, STRLEN klen, SV *val);
145              
146             /* Internal recursive walker. Threads `depth` so we can enforce
147             * `max_depth` (the public option) without yyjson cooperation. */
148             static SV *
149 50195           sv_from_yyjson_d(pTHX_ yyjson_val *val, HV *boolean_stash,
150             int ordered, int depth, int max_depth)
151             {
152             yyjson_type t;
153             yyjson_subtype st;
154              
155 50195 50         if (!val) return newSV(0);
156              
157 50195 50         t = yyjson_get_type(val);
158 50195           st = yyjson_get_subtype(val);
159              
160             /* Depth check fires when we *enter* a container, so primitives
161             * one level past the cap still parse - matches what callers
162             * intuitively expect from "max_depth = N nested levels". */
163 50195 100         if ((t == YYJSON_TYPE_ARR || t == YYJSON_TYPE_OBJ) && depth >= max_depth) {
    100          
    100          
164 1           croak("File::Raw::JSON: max_depth (%d) exceeded during decode",
165             max_depth);
166             }
167              
168 50194           switch (t) {
169 20           case YYJSON_TYPE_NULL:
170 20           return newSV(0);
171              
172 23 50         case YYJSON_TYPE_BOOL: {
173             /* Inline fast path for the default boolean class (~99% of
174             * decodes): pointer-compare the stash and bump the pre-
175             * built singleton's refcount. Saves a function call to
176             * make_bool_sv per boolean. */
177 23           int truth = yyjson_get_bool(val);
178 23 100         if (boolean_stash == g_frj_default_stash) {
179 21 100         SV *s = truth ? g_frj_true_sv : g_frj_false_sv;
180 21 50         if (s) {
181 21           SvREFCNT_inc_simple_void_NN(s);
182 21           return s;
183             }
184             }
185 2           return make_bool_sv(aTHX_ truth, boolean_stash);
186             }
187              
188 13845           case YYJSON_TYPE_NUM:
189 13845           switch (st) {
190 7812 50         case YYJSON_SUBTYPE_UINT: {
191 7812           uint64_t u = yyjson_get_uint(val);
192 7812 100         if (u <= (uint64_t)IV_MAX) return newSViv((IV)u);
193 2           return newSVuv((UV)u);
194             }
195 3 50         case YYJSON_SUBTYPE_SINT:
196 3           return newSViv((IV)yyjson_get_sint(val));
197 6030 50         case YYJSON_SUBTYPE_REAL:
198 6030           return newSVnv(yyjson_get_real(val));
199 0           default:
200 0           return newSV(0);
201             }
202              
203 23315 50         case YYJSON_TYPE_STR: {
204 23315 50         const char *s = yyjson_get_str(val);
205 23315           size_t n = yyjson_get_len(val);
206 23315           SV *out = newSVpvn(s, n);
207 23315           sv_utf8_decode(out);
208 23315           return out;
209             }
210              
211 3481 50         case YYJSON_TYPE_ARR: {
212 3481           size_t n = yyjson_arr_size(val);
213 3481           AV *av = newAV();
214             /* Empty-array fast path: skip iter setup. */
215 3481 100         if (n == 0) return newRV_noinc((SV *)av);
216             {
217             yyjson_val *elem;
218             yyjson_arr_iter it;
219 3455           av_extend(av, (SSize_t)n);
220             yyjson_arr_iter_init(val, &it);
221 23037 100         while ((elem = yyjson_arr_iter_next(&it))) {
222 16639           av_push(av, sv_from_yyjson_d(aTHX_ elem, boolean_stash,
223             ordered, depth + 1,
224             max_depth));
225             }
226             }
227 2943           return newRV_noinc((SV *)av);
228             }
229              
230 9510 50         case YYJSON_TYPE_OBJ: {
231             /* Tied OrderedHash when ordered=>1 (each insert dispatches
232             * tie_oh_store so insertion order is preserved); plain HV
233             * with hv_store otherwise. */
234 9510           size_t n = yyjson_obj_size(val);
235 9510 100         HV *hv = ordered ? make_ordered_hv(aTHX) : newHV();
236             /* Empty-object fast path. */
237 9510 100         if (n == 0) return newRV_noinc((SV *)hv);
238             /* Pre-size the bucket table for the non-ordered case so
239             * hv_store doesn't trigger 2-3 splits + rehashes for any
240             * object with more than 8 keys. */
241 9499 100         if (!ordered && n > 8) hv_ksplit(hv, (IV)n);
    100          
242             {
243             yyjson_val *key, *vv;
244             yyjson_obj_iter it;
245             yyjson_obj_iter_init(val, &it);
246 46287 100         while ((key = yyjson_obj_iter_next(&it))) {
247 27289 50         vv = yyjson_obj_iter_get_val(key);
248 27289 50         const char *kp = yyjson_get_str(key);
249 27289           size_t kl = yyjson_get_len(key);
250 27289           SV *child = sv_from_yyjson_d(aTHX_ vv, boolean_stash,
251             ordered, depth + 1,
252             max_depth);
253 27289 100         if (ordered) {
254 414           ordered_hv_set(aTHX_ hv, kp, kl, child);
255             } else {
256             /* Negative klen tells hv_store the bytes are
257             * UTF-8 - so wide-char Perl literals (eg
258             * "\x{00e9}") match keys decoded from yyjson's
259             * native UTF-8 byte stream. */
260 26875 50         if (!hv_store(hv, kp, -(I32)kl, child, 0)) {
261 0           SvREFCNT_dec(child);
262             }
263             }
264             }
265             }
266 9499           return newRV_noinc((SV *)hv);
267             }
268              
269 0           default:
270 0           return newSV(0);
271             }
272             }
273              
274             SV *
275 6267           json_sv_from_yyjson(pTHX_ yyjson_val *val, HV *boolean_stash,
276             int ordered, int max_depth)
277             {
278 6267           return sv_from_yyjson_d(aTHX_ val, boolean_stash, ordered, 0, max_depth);
279             }
280              
281             static HV *
282 85           make_ordered_hv(pTHX)
283             {
284 85           HV *hv = newHV();
285 85           SV *tied = tie_oh_new(aTHX); /* refcount=1, owned */
286 85           sv_magic((SV *)hv, tied, PERL_MAGIC_tied, NULL, 0);
287 85           SvREFCNT_dec(tied); /* sv_magic took its own */
288 85           return hv;
289             }
290              
291             /* Insert (key, val) into the tied HV. Detects our impl object and
292             * calls tie_oh_store directly (no method dispatch). For foreign tie
293             * classes - if a caller hands us an HV tied to something we don't
294             * recognise - fall back to call_method("STORE") so the contract still
295             * holds. */
296             static void
297 414           ordered_hv_set(pTHX_ HV *hv, const char *key, STRLEN klen, SV *val)
298             {
299 414           MAGIC *mg = mg_find((SV *)hv, PERL_MAGIC_tied);
300             SV *tied_obj;
301 414           dSP;
302              
303 414 50         if (!mg || !mg->mg_obj) {
    50          
304             /* No tie magic. Plain hv_store is fine. */
305 0 0         if (!hv_store(hv, key, (I32)klen, val, 0)) {
306 0           SvREFCNT_dec(val);
307             }
308 0           return;
309             }
310 414           tied_obj = mg->mg_obj;
311              
312             /* Fast path: our own class. tie_oh_store takes ownership of val,
313             * so no SvREFCNT bookkeeping needed. */
314 414 50         if (tie_oh_is_instance(aTHX_ tied_obj)) {
315 414           tie_oh_store(aTHX_ tied_obj, key, klen, val);
316 414           return;
317             }
318              
319             /* Slow path: foreign tie class. Dispatch STORE via call_method,
320             * mortalising the value so it gets cleaned up after the call. */
321 0           ENTER; SAVETMPS;
322 0 0         PUSHMARK(SP);
323 0 0         XPUSHs(tied_obj);
324 0 0         XPUSHs(sv_2mortal(newSVpvn(key, klen)));
325 0 0         XPUSHs(sv_2mortal(val));
326 0           PUTBACK;
327 0           call_method("STORE", G_DISCARD);
328 0           SPAGAIN;
329 0           PUTBACK;
330 0 0         FREETMPS; LEAVE;
331             }
332              
333             static SV *
334 2           make_bool_sv(pTHX_ int truth, HV *stash)
335             {
336 2 50         if (!stash) {
337 0 0         return newSVsv(truth ? &PL_sv_yes : &PL_sv_no);
338             }
339             /* Hot path: default class returns the pre-built read-only
340             * singleton. Caller's SvREFCNT_dec balances our SvREFCNT_inc. */
341 2 50         if (g_frj_default_stash && stash == g_frj_default_stash) {
    50          
342 0 0         SV *s = truth ? g_frj_true_sv : g_frj_false_sv;
343 0 0         if (s) {
344 0 0         SvREFCNT_inc_simple_void(s);
345 0           return s;
346             }
347             /* Singletons not initialised yet (eg called from BOOT order
348             * race). Fall through to per-call allocation. */
349             }
350             {
351 2           SV *inner = newSViv(truth ? 1 : 0);
352 2           SV *rv = newRV_noinc(inner);
353 2           sv_bless(rv, stash);
354 2           return rv;
355             }
356             }
357              
358             /* ============================================================
359             * Value mapping: Perl SV -> yyjson_mut_val (for encode)
360             * ============================================================ */
361              
362             static int sv_is_known_boolean_class(pTHX_ SV *sv);
363              
364             /* Cycle detection.
365             *
366             * Cycles can only form when a value points back to one of its
367             * ancestors on the current descent path. Sibling subtrees can
368             * never form cycles with each other - they're disjoint by the time
369             * we visit them. So we only need to track the *active path*, not
370             * everything ever visited. For real-world JSON-shaped data the
371             * path is typically 3-10 deep; a linear scan of a small SV* array
372             * is dramatically cheaper than HV insert / lookup / delete.
373             *
374             * If a structure exceeds VISITED_STACK_MAX (rare; we'd croak under
375             * default max_depth=512 long before then), we fall back to an HV
376             * for the overflow. Combined with the inline stack the worst case
377             * stays correct without slowing the common case. */
378             #define VISITED_STACK_MAX 64
379              
380             typedef struct {
381             SV *stack[VISITED_STACK_MAX];
382             int depth;
383             HV *overflow; /* lazy; created only when stack is full */
384             } visited_t;
385              
386             PERL_STATIC_INLINE int
387 6635           visited_seen(pTHX_ visited_t *v, SV *target)
388             {
389             int i;
390             /* Linear scan from top of stack (most recently entered first -
391             * cycles tend to point near where we just were). */
392 17822 100         for (i = v->depth - 1; i >= 0; i--) {
393 11189 100         if (v->stack[i] == target) return 1;
394             }
395 6633 50         if (v->overflow) {
396 0           return hv_exists(v->overflow, (const char *)&target, sizeof(SV *));
397             }
398 6633           return 0;
399             }
400              
401             PERL_STATIC_INLINE void
402 6633           visited_enter(pTHX_ visited_t *v, SV *target)
403             {
404 6633 50         if (v->depth < VISITED_STACK_MAX) {
405 6633           v->stack[v->depth++] = target;
406 6633           return;
407             }
408             /* Stack full - spill to HV. */
409 0 0         if (!v->overflow) v->overflow = newHV();
410 0           SvREFCNT_inc_simple_void_NN(&PL_sv_undef);
411 0 0         if (!hv_store(v->overflow, (const char *)&target, sizeof(SV *),
412             &PL_sv_undef, 0))
413 0           SvREFCNT_dec(&PL_sv_undef);
414             }
415              
416             PERL_STATIC_INLINE void
417 6631           visited_leave(pTHX_ visited_t *v, SV *target)
418             {
419             /* Stack is LIFO - the last visited_enter must match this leave.
420             * Defensive: walk back if not at top (shouldn't happen). */
421 6631 50         if (v->depth > 0 && v->stack[v->depth - 1] == target) {
    50          
422 6631           v->depth--;
423 6631           return;
424             }
425 0 0         if (v->overflow) {
426 0           hv_delete(v->overflow, (const char *)&target, sizeof(SV *),
427             G_DISCARD);
428             }
429             }
430              
431             static yyjson_mut_val *sv_to_yyjson_v(pTHX_ SV *sv, yyjson_mut_doc *doc,
432             const json_options_t *opts,
433             visited_t *visited);
434              
435             yyjson_mut_val *
436 1155           json_sv_to_yyjson(pTHX_ SV *sv, yyjson_mut_doc *doc,
437             const json_options_t *opts)
438             {
439             visited_t visited;
440             yyjson_mut_val *r;
441 1155           visited.depth = 0;
442 1155           visited.overflow = NULL;
443 1155           r = sv_to_yyjson_v(aTHX_ sv, doc, opts, &visited);
444 1150 50         if (visited.overflow) SvREFCNT_dec((SV *)visited.overflow);
445 1150           return r;
446             }
447              
448             static yyjson_mut_val *
449 32627           sv_to_yyjson_v(pTHX_ SV *sv, yyjson_mut_doc *doc,
450             const json_options_t *opts, visited_t *visited)
451             {
452             PERL_UNUSED_ARG(opts);
453              
454 32641 50         if (!sv || !SvOK(sv)) return yyjson_mut_null(doc);
    100          
455              
456 32613 100         if (SvROK(sv)) {
457 6653           SV *target = SvRV(sv);
458 6653 100         if (SvOBJECT(target) && sv_is_known_boolean_class(aTHX_ sv)) {
    100          
459 13           int truth = SvTRUE(target) ? 1 : 0;
460 26 50         return yyjson_mut_bool(doc, truth);
461             }
462 6640 100         if (SvTYPE(target) == SVt_PVAV) {
463 2228           AV *av = (AV *)target;
464             yyjson_mut_val *arr;
465             SV **arr_ptr;
466             SSize_t i, n;
467 2228 100         if (visited_seen(aTHX_ visited, target)) {
468 1           croak("File::Raw::JSON: circular reference detected "
469             "(array references itself)");
470             }
471 2227           visited_enter(aTHX_ visited, target);
472 2227           arr = yyjson_mut_arr(doc);
473 2227           n = av_len(av) + 1;
474             /* Direct AvARRAY access skips av_fetch's bounds + magic
475             * check per element. Holes (sparse arrays) come back as
476             * NULL pointers; we substitute &PL_sv_undef. */
477 2227           arr_ptr = AvARRAY(av);
478 17556 100         for (i = 0; i < n; i++) {
479 15330           SV *ep = arr_ptr[i];
480             yyjson_mut_val *child =
481 15330 100         sv_to_yyjson_v(aTHX_ ep ? ep : &PL_sv_undef,
482             doc, opts, visited);
483             yyjson_mut_arr_append(arr, child);
484             }
485 2226           visited_leave(aTHX_ visited, target);
486 2226           return arr;
487             }
488 4412 100         if (SvTYPE(target) == SVt_PVHV) {
489             /* Three dispatches:
490             * - Tie::OrderedHash: walk via the public C ABI
491             * tie_oh_iter_* (no method dispatch).
492             * - Other tied HVs: walk via hv_iternext (which goes
493             * through tied FIRSTKEY/NEXTKEY) and dispatch FETCH
494             * per key via call_method.
495             * - Untied HVs: walk via hv_iternext + hv_iterval.
496             *
497             * sort_keys / canonical: collect (key, val) pairs first,
498             * sort, emit. Default: walk and emit in one pass - no
499             * collection buffer alloc. HvUSEDKEYS returns 0 for tied
500             * HVs (their bucket storage is empty), so we can't use it
501             * to pre-size or short-circuit; the live walks above
502             * handle tied HVs correctly. */
503 4407           HV *hv = (HV *)target;
504             yyjson_mut_val *obj;
505             HE *he;
506             MAGIC *tied_mg;
507 4407 100         int do_sort = opts->sort_keys || opts->canonical;
    100          
508              
509 4407 100         if (visited_seen(aTHX_ visited, target)) {
510 1           croak("File::Raw::JSON: circular reference detected "
511             "(hash references itself)");
512             }
513 4406           visited_enter(aTHX_ visited, target);
514 4406           obj = yyjson_mut_obj(doc);
515 4406           tied_mg = mg_find((SV *)hv, PERL_MAGIC_tied);
516              
517 4406 100         if (do_sort) {
518             /* Collect-sort-emit. Most JSON objects have <32
519             * keys; stack-allocate the buffers for that case to
520             * skip the malloc/free pair (significant when
521             * sort_keys is set on a large array of small
522             * objects - was the dominant cost on the
523             * sort_keys+pretty bench at medium size). */
524             #define FRJ_SORT_STACK_SIZE 32
525             SV *stack_keys[FRJ_SORT_STACK_SIZE];
526             SV *stack_vals[FRJ_SORT_STACK_SIZE];
527 4166           SV **keys_buf = stack_keys;
528 4166           SV **vals_buf = stack_vals;
529 4166           SSize_t count = 0;
530 4166           SSize_t cap = FRJ_SORT_STACK_SIZE;
531 4166           int on_heap = 0;
532             SSize_t i;
533              
534             #define FRJ_PUSH_PAIR(KSV, VSV) STMT_START { \
535             if (count >= cap) { \
536             SSize_t new_cap = cap * 2; \
537             if (on_heap) { \
538             Renew(keys_buf, new_cap, SV *); \
539             Renew(vals_buf, new_cap, SV *); \
540             } else { \
541             SV **nk, **nv; \
542             Newx(nk, new_cap, SV *); \
543             Newx(nv, new_cap, SV *); \
544             memcpy(nk, keys_buf, sizeof(SV *) * (size_t)count); \
545             memcpy(nv, vals_buf, sizeof(SV *) * (size_t)count); \
546             keys_buf = nk; \
547             vals_buf = nv; \
548             on_heap = 1; \
549             } \
550             cap = new_cap; \
551             } \
552             keys_buf[count] = (KSV); \
553             vals_buf[count] = (VSV); \
554             count++; \
555             } STMT_END
556              
557 4166 100         if (tied_mg && tied_mg->mg_obj
    50          
558 5 50         && tie_oh_is_instance(aTHX_ tied_mg->mg_obj))
559 5           {
560 5           SV *self = tied_mg->mg_obj;
561             tie_oh_iter_t it;
562             const char *key;
563             STRLEN klen;
564             SV *vsv;
565 5           tie_oh_iter_init(aTHX_ self, &it);
566 18 100         while (tie_oh_iter_next(aTHX_ self, &it,
567             &key, &klen, &vsv)) {
568 13           SV *ksv = sv_2mortal(newSVpvn(key, klen));
569 13 50         FRJ_PUSH_PAIR(ksv, vsv);
    0          
    0          
    0          
    0          
    0          
570             }
571 4161 50         } else if (tied_mg && tied_mg->mg_obj) {
    0          
572 0           hv_iterinit(hv);
573 0 0         while ((he = hv_iternext(hv))) {
574             I32 klen;
575 0           const char *key = hv_iterkey(he, &klen);
576             SV *fetched, *copy, *ksv;
577             int rc;
578 0           dSP;
579 0           ENTER; SAVETMPS;
580 0 0         PUSHMARK(SP);
581 0 0         XPUSHs(tied_mg->mg_obj);
582 0 0         XPUSHs(sv_2mortal(newSVpvn(key, klen)));
583 0           PUTBACK;
584 0           rc = call_method("FETCH", G_SCALAR);
585 0           SPAGAIN;
586 0 0         fetched = rc > 0 ? POPs : &PL_sv_undef;
587 0           copy = newSVsv(fetched);
588 0           PUTBACK;
589 0 0         FREETMPS; LEAVE;
590 0           ksv = sv_2mortal(newSVpvn(key, klen));
591 0 0         FRJ_PUSH_PAIR(ksv, sv_2mortal(copy));
    0          
    0          
    0          
    0          
    0          
592             }
593             } else {
594             /* Untied HV: HeVAL(he) is direct, no tie-magic
595             * re-check (hv_iterval would do that since it's
596             * the polymorphic accessor). */
597 4161           hv_iterinit(hv);
598 18561 100         while ((he = hv_iternext(hv))) {
599 14400 50         FRJ_PUSH_PAIR(hv_iterkeysv(he), HeVAL(he));
    0          
    0          
    0          
    0          
    0          
600             }
601             }
602              
603             #undef FRJ_PUSH_PAIR
604              
605             /* Insertion sort: small n typical, no comparator
606             * indirection. */
607             {
608             SSize_t a, b;
609 14418 100         for (a = 1; a < count; a++) {
610 10252           SV *kcur = keys_buf[a];
611 10252           SV *vcur = vals_buf[a];
612             STRLEN clen;
613 10252           const char *cpv = SvPV(kcur, clen);
614 10252           b = a - 1;
615 21437 100         while (b >= 0) {
616             STRLEN plen;
617 18108           const char *ppv = SvPV(keys_buf[b], plen);
618 18108           STRLEN cmplen = clen < plen ? clen : plen;
619 18108           int rc = memcmp(cpv, ppv, cmplen);
620 18108 100         if (rc < 0 || (rc == 0 && clen < plen)) {
    100          
    50          
621 11185           keys_buf[b + 1] = keys_buf[b];
622 11185           vals_buf[b + 1] = vals_buf[b];
623 11185           b--;
624             } else break;
625             }
626 10252           keys_buf[b + 1] = kcur;
627 10252           vals_buf[b + 1] = vcur;
628             }
629             }
630              
631 18579 100         for (i = 0; i < count; i++) {
632             STRLEN klen;
633 14413           const char *kp = SvPV(keys_buf[i], klen);
634 14413 50         yyjson_mut_val *kval = yyjson_mut_strn(doc, kp, (size_t)klen);
635 14413           yyjson_mut_val *vval = sv_to_yyjson_v(aTHX_ vals_buf[i],
636             doc, opts, visited);
637             yyjson_mut_obj_add(obj, kval, vval);
638             }
639              
640 4166 50         if (on_heap) {
641 0           Safefree(keys_buf);
642 0           Safefree(vals_buf);
643             }
644             #undef FRJ_SORT_STACK_SIZE
645             }
646 240 100         else if (tied_mg && tied_mg->mg_obj
    50          
647 54 50         && tie_oh_is_instance(aTHX_ tied_mg->mg_obj))
648 54           {
649             /* Single-pass: tie_oh_iter_* + emit. */
650 54           SV *self = tied_mg->mg_obj;
651             tie_oh_iter_t it;
652             const char *key;
653             STRLEN klen;
654             SV *vsv;
655 54           tie_oh_iter_init(aTHX_ self, &it);
656 387 100         while (tie_oh_iter_next(aTHX_ self, &it,
657             &key, &klen, &vsv)) {
658 333 50         yyjson_mut_val *kval = yyjson_mut_strn(
659             doc, key, (size_t)klen);
660 333           yyjson_mut_val *vval = sv_to_yyjson_v(
661             aTHX_ vsv, doc, opts, visited);
662             yyjson_mut_obj_add(obj, kval, vval);
663             }
664             }
665 186 50         else if (tied_mg && tied_mg->mg_obj) {
    0          
666             /* Single-pass: hv_iternext + per-key call_method FETCH. */
667 0           hv_iterinit(hv);
668 0 0         while ((he = hv_iternext(hv))) {
669             I32 klen;
670 0           const char *key = hv_iterkey(he, &klen);
671             SV *fetched, *copy, *vsv;
672             yyjson_mut_val *kval, *vval;
673             int rc;
674 0           dSP;
675 0           ENTER; SAVETMPS;
676 0 0         PUSHMARK(SP);
677 0 0         XPUSHs(tied_mg->mg_obj);
678 0 0         XPUSHs(sv_2mortal(newSVpvn(key, klen)));
679 0           PUTBACK;
680 0           rc = call_method("FETCH", G_SCALAR);
681 0           SPAGAIN;
682 0 0         fetched = rc > 0 ? POPs : &PL_sv_undef;
683 0           copy = newSVsv(fetched);
684 0           PUTBACK;
685 0 0         FREETMPS; LEAVE;
686 0           vsv = sv_2mortal(copy);
687              
688 0 0         kval = yyjson_mut_strn(doc, key, (size_t)klen);
689 0           vval = sv_to_yyjson_v(aTHX_ vsv, doc, opts, visited);
690             yyjson_mut_obj_add(obj, kval, vval);
691             }
692             }
693             else {
694             /* Untied HV: HeVAL(he) is direct. hv_iterval is the
695             * tie-aware polymorphic accessor and would re-check
696             * magic per key; we already know we're not tied here. */
697 186           hv_iterinit(hv);
698 1581 100         while ((he = hv_iternext(hv))) {
699             I32 klen;
700 1396           const char *key = hv_iterkey(he, &klen);
701 1396 50         yyjson_mut_val *kval = yyjson_mut_strn(
702             doc, key, (size_t)klen);
703 1396           yyjson_mut_val *vval = sv_to_yyjson_v(
704             aTHX_ HeVAL(he), doc, opts, visited);
705             yyjson_mut_obj_add(obj, kval, vval);
706             }
707             }
708              
709 4405           visited_leave(aTHX_ visited, target);
710 4405           return obj;
711             }
712             /* Reject things that have no sane JSON encoding. */
713             {
714 5           svtype tt = SvTYPE(target);
715 5 100         if (tt == SVt_PVCV) {
716 1           croak("File::Raw::JSON: cannot encode CODE reference as JSON");
717             }
718 4 100         if (tt == SVt_PVGV) {
719 1           croak("File::Raw::JSON: cannot encode GLOB reference as JSON");
720             }
721             /* SvRXOK is the portable regex check - handles SVt_REGEXP
722             * and the older magic-attached form. */
723 3 100         if (SvRXOK(sv) || SvRXOK(target)) {
    50          
724 1           croak("File::Raw::JSON: cannot encode Regexp reference as JSON");
725             }
726             }
727             /* Non-blessed scalar refs are treated as JSON booleans by
728             * truthiness, matching JSON::XS / Cpanel::JSON::XS / JSON::PP:
729             * \1 -> true
730             * \0 -> false
731             * \"" -> false
732             * \"x" -> true
733             * Lets callers express booleans without loading a sentinel
734             * class. */
735 2 50         if (!SvOBJECT(target)) {
736 4 50         return yyjson_mut_bool(doc, SvTRUE(target) ? 1 : 0);
737             }
738             /* Blessed non-recognised refs (eg bless \1, 'My::Class' that
739             * isn't a known boolean class) fall through to SvPV which
740             * emits the stringified form ("My::Class=SCALAR(0x...)"). */
741             {
742             STRLEN len;
743 0           const char *s = SvPV(sv, len);
744 0 0         return yyjson_mut_strn(doc, s, (size_t)len);
745             }
746             }
747              
748 25960 100         if (SvIOK(sv) && !SvNOK(sv)) {
    100          
749 1691 100         if (SvIsUV(sv)) return yyjson_mut_uint(doc, (uint64_t)SvUV(sv));
750 3374           return yyjson_mut_sint(doc, (int64_t)SvIV(sv));
751             }
752 24271 100         if (SvNOK(sv)) {
753 12038           return yyjson_mut_real(doc, SvNV(sv));
754             }
755             {
756             STRLEN len;
757 18252           const char *s = SvPV(sv, len);
758 36504 50         return yyjson_mut_strn(doc, s, (size_t)len);
759             }
760             }
761              
762             /* Recognise known boolean sentinel classes.
763             *
764             * Hot path: pointer-compare against g_frj_default_stash (cached at
765             * BOOT for File::Raw::JSON::Boolean). ~99% of decoded booleans
766             * come back blessed into this class, so a single pointer comparison
767             * resolves them without touching the stash name. Foreign classes
768             * fall through to the HvNAME_get + strEQ chain. */
769             static int
770 15           sv_is_known_boolean_class(pTHX_ SV *sv)
771             {
772             HV *stash;
773             const char *name;
774 15 50         if (!SvROK(sv)) return 0;
775 15 50         if (!SvOBJECT(SvRV(sv))) return 0;
776 15           stash = SvSTASH(SvRV(sv));
777 15 50         if (!stash) return 0;
778             /* Hot path: our default class. */
779 15 50         if (g_frj_default_stash && stash == g_frj_default_stash) return 1;
    100          
780             /* Cold path: foreign boolean sentinels. */
781 4 50         name = HvNAME_get(stash);
    50          
    50          
    0          
    50          
    50          
782 4 50         if (!name) return 0;
783 4 100         if (strEQ(name, "JSON::PP::Boolean")) return 1;
784 2 50         if (strEQ(name, "Types::Serialiser::Boolean")) return 1;
785 2 50         if (strEQ(name, "Cpanel::JSON::XS::Boolean")) return 1;
786 2 50         if (strEQ(name, "JSON::XS::Boolean")) return 1;
787 2 50         if (strEQ(name, "boolean")) return 1;
788             /* Last: name-compare File::Raw::JSON::Boolean for the case where
789             * the stash pointer drifted (eg multiple interpreter contexts). */
790 2           return strEQ(name, "File::Raw::JSON::Boolean");
791             }
792              
793             /* ============================================================
794             * Decode entry points
795             * ============================================================ */
796              
797             static void
798 6           croak_yyjson_read(pTHX_ const yyjson_read_err *err,
799             const char *bytes, STRLEN len)
800             {
801 6           STRLEN ctx_off = err->pos > 16 ? err->pos - 16 : 0;
802 6           STRLEN ctx_end = err->pos + 16 < len ? err->pos + 16 : len;
803 6           STRLEN ctx_len = ctx_end - ctx_off;
804 6           SV *ctx = sv_2mortal(newSVpvn(bytes + ctx_off, ctx_len));
805 6           char *p = SvPVX(ctx);
806             STRLEN i;
807 50 100         for (i = 0; i < ctx_len; i++) {
808 44 100         if (p[i] == '\n' || p[i] == '\r' || p[i] == '\t') p[i] = ' ';
    50          
    50          
809             }
810 6 50         croak("File::Raw::JSON: %s at byte offset %lu near \"%.*s\"",
811             err->msg ? err->msg : "parse error",
812             (unsigned long)err->pos, (int)ctx_len, p);
813             }
814              
815             static yyjson_read_flag
816 233           build_read_flags(const json_options_t *opts)
817             {
818 233           yyjson_read_flag f = 0;
819 233 100         if (opts->relaxed) {
820 5           f |= YYJSON_READ_ALLOW_COMMENTS;
821 5           f |= YYJSON_READ_ALLOW_TRAILING_COMMAS;
822             }
823 233 100         if (opts->allow_nan_inf) f |= YYJSON_READ_ALLOW_INF_AND_NAN;
824 233           return f;
825             }
826              
827             /* Hybrid bump-then-malloc allocator for yyjson read.
828             *
829             * yyjson allocates one ~16-byte mut_val per JSON value via its
830             * configured allocator (default: malloc). For small/medium docs
831             * those allocations are pure overhead - we'd rather slice them off
832             * a pre-allocated stack buffer.
833             *
834             * This allocator does exactly that: serves from a stack-resident
835             * pool by bump pointer until exhausted, then falls back to malloc
836             * for individual oversize requests. free() is a no-op for in-pool
837             * pointers, real free() for malloc'd ones.
838             *
839             * Why hybrid (rather than just yyjson_alc_pool_init): the built-in
840             * pool init returns NULL on overflow, which fails the parse. We
841             * want graceful fallback so any doc parses, with the small/medium
842             * fast path getting the perf win. */
843             typedef struct {
844             char *pool;
845             size_t pool_size;
846             size_t pool_used;
847             } frj_alc_ctx_t;
848              
849             static void *
850 544           frj_alc_malloc(void *ctx, size_t size)
851             {
852 544           frj_alc_ctx_t *c = (frj_alc_ctx_t *)ctx;
853             /* 16-byte aligned bump (yyjson values are typically 16-byte). */
854 544           size_t aligned = (c->pool_used + 15) & ~(size_t)15;
855 544 100         if (aligned + size <= c->pool_size) {
856 535           c->pool_used = aligned + size;
857 535           return c->pool + aligned;
858             }
859 9           return malloc(size);
860             }
861              
862             static void *
863 82           frj_alc_realloc(void *ctx, void *ptr, size_t old_size, size_t size)
864             {
865 82           frj_alc_ctx_t *c = (frj_alc_ctx_t *)ctx;
866 82 100         if (ptr >= (void *)c->pool && ptr < (void *)(c->pool + c->pool_size)) {
    50          
867             /* In-pool memory: allocate fresh, copy, leave the old block
868             * in the pool (will be reclaimed when the pool is reset). */
869 78           void *new_ptr = frj_alc_malloc(ctx, size);
870 78 50         if (new_ptr && old_size > 0) {
    50          
871 78           memcpy(new_ptr, ptr, old_size < size ? old_size : size);
872             }
873 78           return new_ptr;
874             }
875 4           return realloc(ptr, size);
876             }
877              
878             static void
879 464           frj_alc_free(void *ctx, void *ptr)
880             {
881 464           frj_alc_ctx_t *c = (frj_alc_ctx_t *)ctx;
882 464 100         if (ptr >= (void *)c->pool && ptr < (void *)(c->pool + c->pool_size)) {
    50          
883 456           return; /* No-op for pool memory; reset on caller exit */
884             }
885 8           free(ptr);
886             }
887              
888             #define FRJ_READ_POOL_BYTES (16 * 1024)
889              
890             SV *
891 143           json_decode_document(pTHX_ const char *bytes, STRLEN len,
892             const json_options_t *opts, HV *boolean_stash)
893             {
894             char pool_buf[FRJ_READ_POOL_BYTES];
895             frj_alc_ctx_t alc_ctx;
896             yyjson_alc alc;
897             yyjson_read_err err;
898             yyjson_doc *doc;
899             SV *out;
900             yyjson_val *root;
901              
902 143           alc_ctx.pool = pool_buf;
903 143           alc_ctx.pool_size = sizeof pool_buf;
904 143           alc_ctx.pool_used = 0;
905 143           alc.malloc = frj_alc_malloc;
906 143           alc.realloc = frj_alc_realloc;
907 143           alc.free = frj_alc_free;
908 143           alc.ctx = &alc_ctx;
909              
910 143           doc = yyjson_read_opts((char *)bytes, (size_t)len,
911             build_read_flags(opts), &alc, &err);
912              
913 143 100         if (!doc) {
914 6           croak_yyjson_read(aTHX_ &err, bytes, len);
915             /* NOTREACHED */
916             }
917 137           root = yyjson_doc_get_root(doc);
918 137 100         if (!opts->allow_nonref && root) {
    50          
919 1           yyjson_type t = yyjson_get_type(root);
920 1 50         if (t != YYJSON_TYPE_ARR && t != YYJSON_TYPE_OBJ) {
    50          
921             yyjson_doc_free(doc);
922 1           croak("File::Raw::JSON: top-level value is not an object/array "
923             "and allow_nonref is false");
924             }
925             }
926 136           out = json_sv_from_yyjson(aTHX_ root, boolean_stash,
927 136           opts->ordered, opts->max_depth);
928             yyjson_doc_free(doc);
929 135           return out;
930             }
931              
932             AV *
933 38           json_decode_lines(pTHX_ const char *bytes, STRLEN len,
934             const json_options_t *opts, HV *boolean_stash)
935             {
936 38           AV *result = newAV();
937 38           STRLEN cursor = 0;
938             /* Per-line pool: reused across all lines. Reset to empty after
939             * each parse (the doc is freed before next parse, so all in-pool
940             * memory is logically reclaimed). */
941             char pool_buf[FRJ_READ_POOL_BYTES];
942             frj_alc_ctx_t alc_ctx;
943             yyjson_alc alc;
944 38           alc_ctx.pool = pool_buf;
945 38           alc_ctx.pool_size = sizeof pool_buf;
946 38           alc.malloc = frj_alc_malloc;
947 38           alc.realloc = frj_alc_realloc;
948 38           alc.free = frj_alc_free;
949 38           alc.ctx = &alc_ctx;
950              
951 128 100         while (cursor < len) {
952             STRLEN s, e, np;
953             jsonl_scan_t rc =
954 92           json_jsonl_next(bytes + cursor, len - cursor, &s, &e, &np);
955 92 100         if (rc == JSONL_FOUND) {
956             yyjson_read_err err;
957             yyjson_doc *doc;
958 90           alc_ctx.pool_used = 0; /* reset pool for next line */
959 90           doc = yyjson_read_opts(
960 90           (char *)bytes + cursor + s, (size_t)(e - s),
961             build_read_flags(opts), &alc, &err);
962 90 50         if (!doc) {
963 0           err.pos += cursor + s;
964 0           SvREFCNT_dec((SV *)result);
965 0           croak_yyjson_read(aTHX_ &err, bytes, len);
966             }
967 180 50         av_push(result,
968             json_sv_from_yyjson(aTHX_ yyjson_doc_get_root(doc),
969             boolean_stash, opts->ordered,
970             opts->max_depth));
971             yyjson_doc_free(doc);
972 90           cursor += np;
973 90           continue;
974             }
975 2 100         if (rc == JSONL_NEED_MORE) {
976 1           SvREFCNT_dec((SV *)result);
977 1           croak("File::Raw::JSON: truncated JSON value at byte offset %lu",
978             (unsigned long)(cursor + s));
979             }
980             /* JSONL_NO_OPENER: lenient = skip; strict = croak. */
981 1 50         if (s >= len - cursor) break; /* trailing whitespace */
982 0 0         if (opts->relaxed) { cursor += s + 1; continue; }
983 0           SvREFCNT_dec((SV *)result);
984 0           croak("File::Raw::JSON: unexpected byte at offset %lu "
985             "(expected '{' or '[' to start a JSONL value)",
986             (unsigned long)(cursor + s));
987             }
988 37           return result;
989             }
990              
991             /* ============================================================
992             * Encode entry points
993             * ============================================================ */
994              
995             static yyjson_write_flag
996 1150           build_write_flags(const json_options_t *opts)
997             {
998 1150           yyjson_write_flag f = 0;
999 1150 100         if (opts->pretty && !opts->canonical) {
    50          
1000 14 100         if (opts->indent == 4) {
1001 1           f |= YYJSON_WRITE_PRETTY; /* 4 spaces */
1002             } else {
1003 13           f |= YYJSON_WRITE_PRETTY_TWO_SPACES;
1004             }
1005             }
1006 1150 50         if (!opts->utf8) f |= YYJSON_WRITE_ESCAPE_UNICODE;
1007 1150 100         if (opts->allow_nan_inf) f |= YYJSON_WRITE_ALLOW_INF_AND_NAN;
1008 1150           return f;
1009             }
1010              
1011             SV *
1012 1155           json_encode_document(pTHX_ SV *value, const json_options_t *opts)
1013             {
1014 1155           yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
1015             yyjson_mut_val *root;
1016             yyjson_write_err err;
1017             char *out;
1018             size_t out_len;
1019             SV *result;
1020              
1021 1155 50         if (!doc) croak("File::Raw::JSON: out of memory (mut_doc_new)");
1022              
1023 1155           root = json_sv_to_yyjson(aTHX_ value, doc, opts);
1024             yyjson_mut_doc_set_root(doc, root);
1025              
1026 1150           out = yyjson_mut_write_opts(doc, build_write_flags(opts), NULL,
1027             &out_len, &err);
1028 1150 50         if (!out) {
1029 0           yyjson_mut_doc_free(doc);
1030 0 0         croak("File::Raw::JSON: encode failed: %s",
1031             err.msg ? err.msg : "unknown error");
1032             }
1033 1150           result = newSVpvn(out, out_len);
1034 1150           free(out);
1035 1150           yyjson_mut_doc_free(doc);
1036 1150           return result;
1037             }
1038              
1039             SV *
1040 13           json_encode_lines(pTHX_ SV *payload, const json_options_t *opts)
1041             {
1042             AV *av;
1043             SSize_t i, n;
1044             SV *out;
1045              
1046 13 50         if (!payload || !SvROK(payload) || SvTYPE(SvRV(payload)) != SVt_PVAV)
    100          
    100          
1047 2           croak("File::Raw::JSON: jsonl write expects an arrayref of records");
1048              
1049 11           av = (AV *)SvRV(payload);
1050 11           n = av_len(av) + 1;
1051 11           out = newSVpvn("", 0);
1052              
1053 1032 100         for (i = 0; i < n; i++) {
1054 1021           SV **ep = av_fetch(av, i, 0);
1055             SV *rec_bytes;
1056             STRLEN blen;
1057             const char *bp;
1058 2042 50         rec_bytes = json_encode_document(aTHX_
1059 1021 50         (ep && *ep) ? *ep : &PL_sv_undef, opts);
1060 1021           bp = SvPV(rec_bytes, blen);
1061 1021           sv_catpvn(out, bp, blen);
1062 1021           sv_catpvn(out, opts->eol, opts->eol_len);
1063 1021           SvREFCNT_dec(rec_bytes);
1064             }
1065 11           return out;
1066             }