File Coverage

XS.xs
Criterion Covered Total %
statement 238 263 90.4
branch 161 266 60.5
condition n/a
subroutine n/a
pod n/a
total 399 529 75.4


line stmt bran cond sub pod time code
1             #define PERL_NO_GET_CONTEXT
2             #include "EXTERN.h"
3             #include "perl.h"
4             #include "XSUB.h"
5              
6             /* okchr = %x21 / %x23-3c / %x3e-5b / %x5d-7e ; VCHAR minus \ and " and = */
7             PERL_STATIC_INLINE int
8 1180           is_okchr(unsigned char c)
9             {
10 1180 100         return (c == 0x21 ||
11 1180 100         (c >= 0x23 && c <= 0x3C) ||
    100          
12 3354 50         (c >= 0x3E && c <= 0x5B) ||
    100          
    100          
13 994 100         (c >= 0x5D && c <= 0x7E));
14             }
15              
16             /* Check if string can be used as a bare logfmt value (matches KEY_RE) */
17             static int
18 143           is_bare_value(const char *s, STRLEN len)
19             {
20             STRLEN i;
21 143 100         if (len == 0)
22 2           return 0;
23 728 100         for (i = 0; i < len; i++) {
24 630 100         if (!is_okchr((unsigned char)s[i]))
25 43           return 0;
26             }
27 98           return 1;
28             }
29              
30             /*
31             * Check if a Unicode codepoint needs \x{XX} escaping.
32             * This is called AFTER \, ", \t, \n, \r are handled.
33             * Matches Perl's [\pC\v] — General Category C (Cc, Cf, Co, Cs) plus
34             * vertical whitespace characters.
35             */
36             static int
37 7           needs_escape(UV cp)
38             {
39             /* Cc: C0 controls (0x00-0x1F) — includes \t, \n, \r but those are
40             * already handled before this function is called */
41 7 50         if (cp <= 0x1F)
42 0           return 1;
43              
44             /* DEL */
45 7 50         if (cp == 0x7F)
46 0           return 1;
47              
48             /* C1 controls (0x80-0x9F), includes NEL (0x85) */
49 7 50         if (cp >= 0x80 && cp <= 0x9F)
    50          
50 0           return 1;
51              
52             /* Vertical whitespace not in Cc: LINE SEPARATOR, PARAGRAPH SEPARATOR */
53 7 100         if (cp == 0x2028 || cp == 0x2029)
    50          
54 2           return 1;
55              
56             /* Cf (format) characters — comprehensive list */
57 5 50         if (cp == 0x00AD) return 1; /* SOFT HYPHEN */
58 5 100         if (cp >= 0x0600 && cp <= 0x0605) return 1;
    50          
59 5 50         if (cp == 0x061C) return 1;
60 5 50         if (cp == 0x06DD) return 1;
61 5 50         if (cp == 0x070F) return 1;
62 5 50         if (cp == 0x08E2) return 1;
63 5 50         if (cp == 0x180E) return 1;
64 5 100         if (cp >= 0x200B && cp <= 0x200F) return 1; /* includes ZWJ (0x200D) */
    50          
65 3 50         if (cp >= 0x202A && cp <= 0x202E) return 1;
    0          
66 3 50         if (cp >= 0x2060 && cp <= 0x2064) return 1;
    0          
67 3 50         if (cp >= 0x2066 && cp <= 0x206F) return 1;
    0          
68 3 50         if (cp == 0xFEFF) return 1; /* BOM */
69 3 50         if (cp >= 0xFFF9 && cp <= 0xFFFB) return 1;
    0          
70              
71             /* Co (private use) */
72 3 50         if (cp >= 0xE000 && cp <= 0xF8FF) return 1;
    0          
73              
74             /* Cs (surrogates) — shouldn't appear in valid strings */
75 3 50         if (cp >= 0xD800 && cp <= 0xDFFF) return 1;
    0          
76              
77             /* Higher plane Cf */
78 3 50         if (cp == 0x110BD || cp == 0x110CD) return 1;
    50          
79 3 50         if (cp >= 0x13430 && cp <= 0x1343F) return 1;
    0          
80 3 50         if (cp >= 0x1BCA0 && cp <= 0x1BCA3) return 1;
    0          
81 3 50         if (cp >= 0x1D173 && cp <= 0x1D17A) return 1;
    0          
82 3 50         if (cp == 0xE0001) return 1;
83 3 50         if (cp >= 0xE0020 && cp <= 0xE007F) return 1;
    0          
84              
85             /* Higher plane Co (private use) */
86 3 50         if (cp >= 0xF0000 && cp <= 0xFFFFD) return 1;
    0          
87 3 50         if (cp >= 0x100000 && cp <= 0x10FFFD) return 1;
    0          
88              
89             /* Noncharacters (subset of Cn) */
90 3 50         if ((cp & 0xFFFE) == 0xFFFE) return 1;
91 3 50         if (cp >= 0xFDD0 && cp <= 0xFDEF) return 1;
    0          
92              
93 3           return 0;
94             }
95              
96             /*
97             * Quote a string value for logfmt output.
98             * Input: a Perl SV (character string, may have UTF8 flag).
99             * Output: a new SV containing the quoted byte string (no UTF8 flag),
100             * wrapped in double quotes, with proper escaping.
101             */
102             static SV *
103 24           quote_string_xs(pTHX_ SV *input)
104             {
105             STRLEN len;
106 24           const char *s = SvPV(input, len);
107 24           bool is_utf8 = cBOOL(SvUTF8(input));
108 24           const char *end = s + len;
109             SV *out;
110              
111             /* Optimistic pre-allocate: most chars pass through or become 2-char escapes */
112 24           out = newSV(len * 2 + 3);
113 24           SvPOK_on(out);
114 24           sv_catpvn(out, "\"", 1);
115              
116 294 100         while (s < end) {
117             UV cp;
118             STRLEN char_len;
119              
120 270 100         if (is_utf8) {
121 92           cp = utf8_to_uvchr_buf((const U8 *)s, (const U8 *)end, &char_len);
122 92 50         if (char_len == 0) {
123             /* Malformed UTF-8, skip byte */
124 0           s++;
125 0           continue;
126             }
127             } else {
128 178           cp = (UV)(unsigned char)*s;
129 178           char_len = 1;
130             }
131              
132 270 100         if (cp == '\\') {
133 5           sv_catpvn(out, "\\\\", 2);
134 265 100         } else if (cp == '"') {
135 14           sv_catpvn(out, "\\\"", 2);
136 251 100         } else if (cp == '\t') {
137 3           sv_catpvn(out, "\\t", 2);
138 248 100         } else if (cp == '\n') {
139 3           sv_catpvn(out, "\\n", 2);
140 245 100         } else if (cp == '\r') {
141 3           sv_catpvn(out, "\\r", 2);
142 242 50         } else if (cp >= 0x20 && cp < 0x7F) {
    100          
143             /* Common ASCII printable — pass through directly */
144 235           sv_catpvn(out, s, 1);
145 7 100         } else if (needs_escape(cp)) {
146             /* Control, format, or vertical whitespace — \x{XX} each UTF-8 byte */
147             U8 utf8buf[UTF8_MAXBYTES + 1];
148 4           U8 *utf8end = uvchr_to_utf8(utf8buf, cp);
149 4           STRLEN utf8len = utf8end - utf8buf;
150             STRLEN j;
151 16 100         for (j = 0; j < utf8len; j++) {
152             char hexbuf[8];
153 12           int hexlen = snprintf(hexbuf, sizeof(hexbuf), "\\x{%02x}", utf8buf[j]);
154 12           sv_catpvn(out, hexbuf, hexlen);
155             }
156             } else {
157             /* Safe non-ASCII (e.g. ë, ü) — output as UTF-8 bytes */
158 3 50         if (is_utf8) {
159 3           sv_catpvn(out, s, char_len);
160             } else {
161             /* Latin-1 codepoint → UTF-8 encode */
162             U8 utf8buf[UTF8_MAXBYTES + 1];
163 0           U8 *utf8end = uvchr_to_utf8(utf8buf, cp);
164 0           sv_catpvn(out, (const char *)utf8buf, utf8end - utf8buf);
165             }
166             }
167              
168 270           s += char_len;
169             }
170              
171 24           sv_catpvn(out, "\"", 1);
172             /* Result is bytes — do NOT set UTF8 flag */
173 24           return out;
174             }
175              
176             /*
177             * Sanitize a key: replace non-okchr characters with '?'.
178             * Empty keys become '~'.
179             * Returns a new mortal SV.
180             */
181             static SV *
182 146           sanitize_key(pTHX_ SV *key_sv)
183             {
184             STRLEN len;
185             const char *key_s;
186             SV *result;
187              
188 146 50         if (!SvOK(key_sv)) {
189 0           return sv_2mortal(newSVpvn("~", 1));
190             }
191              
192 146           key_s = SvPV(key_sv, len);
193 146 100         if (len == 0) {
194 2           return sv_2mortal(newSVpvn("~", 1));
195             }
196              
197 144 50         if (SvUTF8(key_sv)) {
198             /* Walk codepoint by codepoint */
199 0           const char *s = key_s;
200 0           const char *end = s + len;
201 0           result = newSVpvn("", 0);
202 0 0         while (s < end) {
203             STRLEN char_len;
204 0           UV cp = utf8_to_uvchr_buf((const U8 *)s, (const U8 *)end, &char_len);
205 0 0         if (char_len == 0) { s++; continue; }
206 0 0         if (cp <= 0x7E && is_okchr((unsigned char)cp)) {
    0          
207 0           sv_catpvn(result, s, char_len);
208             } else {
209 0           sv_catpvn(result, "?", 1);
210             }
211 0           s += char_len;
212             }
213 0           return sv_2mortal(result);
214             } else {
215             /* Byte mode — modify in place on a copy */
216             char *buf;
217             STRLEN i;
218 144           result = newSVpvn(key_s, len);
219 144           buf = SvPVX(result);
220 694 100         for (i = 0; i < len; i++) {
221 550 100         if (!is_okchr((unsigned char)buf[i])) {
222 7           buf[i] = '?';
223             }
224             }
225 144           return sv_2mortal(result);
226             }
227             }
228              
229             /* Forward declaration */
230             static AV *
231             pairs_to_kvstr_impl(pTHX_ SV *self, AV *aref, HV *seen, SV *prefix);
232              
233             /*
234             * Core implementation of _pairs_to_kvstr_aref.
235             * Returns a new (non-mortal) AV* of key=value strings.
236             */
237             static AV *
238 85           pairs_to_kvstr_impl(pTHX_ SV *self, AV *aref, HV *seen, SV *prefix)
239             {
240 85           AV *kvstrs = newAV();
241 85           SSize_t alen = av_len(aref); /* last index, -1 if empty */
242             SSize_t i;
243              
244 231 100         for (i = 0; i <= alen; i += 2) {
245             SV **key_svp, **val_svp;
246             SV *key, *value, *str;
247             STRLEN val_len;
248             const char *val_s;
249              
250             /* Get and sanitize key */
251 146           key_svp = av_fetch(aref, i, 0);
252 146 50         key = sanitize_key(aTHX_ key_svp ? *key_svp : &PL_sv_undef);
253              
254             /* Prepend prefix if defined */
255 146 100         if (prefix && SvOK(prefix)) {
    50          
256 49           SV *prefixed = sv_2mortal(newSVsv(prefix));
257 49           sv_catpvn(prefixed, ".", 1);
258 49           sv_catsv(prefixed, key);
259 49           key = prefixed;
260             }
261              
262             /* Build "key=" early so the fast path can just append */
263 146           str = newSVsv(key);
264 146           sv_catpvn(str, "=", 1);
265              
266             /* Get value */
267 146 50         val_svp = (i + 1 <= alen) ? av_fetch(aref, i + 1, 0) : NULL;
268 146 50         value = val_svp ? *val_svp : &PL_sv_undef;
269              
270             /* Fast path: defined non-ref scalar that's a bare value */
271 146 100         if (SvOK(value) && !SvROK(value)) {
    100          
272 103           val_s = SvPV(value, val_len);
273 103 100         if (is_bare_value(val_s, val_len)) {
274 82           sv_catpvn(str, val_s, val_len);
275 82           av_push(kvstrs, str);
276 106           continue;
277             }
278             }
279              
280             /* Handle coderef: call it to get the actual value */
281 64 100         if (SvROK(value) && SvTYPE(SvRV(value)) == SVt_PVCV) {
    100          
282 5           dSP;
283             int count;
284 5           ENTER;
285 5           SAVETMPS;
286 5 50         PUSHMARK(SP);
287 5           PUTBACK;
288 5           count = call_sv(value, G_SCALAR);
289 5           SPAGAIN;
290 5 50         if (count > 0) {
291 5           value = SvREFCNT_inc(POPs);
292             } else {
293 0           value = &PL_sv_undef;
294             }
295 5           PUTBACK;
296 5 50         FREETMPS;
297 5           LEAVE;
298 5           value = sv_2mortal(value);
299             }
300              
301             /* Handle ref-to-ref: flog via String::Flogger */
302 64 100         if (SvROK(value) && !sv_isobject(value) && SvROK(SvRV(value))) {
    100          
    100          
303 3           dSP;
304             int count;
305             SV *flogger_class;
306 3           SV *derefed = SvRV(value);
307             AV *flog_args;
308             SV *flog_args_ref;
309              
310             /* Call $self->string_flogger to get the class */
311 3           ENTER;
312 3           SAVETMPS;
313 3 50         PUSHMARK(SP);
314 3 50         XPUSHs(self);
315 3           PUTBACK;
316 3           count = call_method("string_flogger", G_SCALAR);
317 3           SPAGAIN;
318 3 50         flogger_class = (count > 0) ? POPs : &PL_sv_undef;
319 3           SvREFCNT_inc(flogger_class);
320 3           PUTBACK;
321 3 50         FREETMPS;
322 3           LEAVE;
323              
324             /* Call $flogger_class->flog([ '%s', $$value ]) */
325 3           flog_args = newAV();
326 3           av_push(flog_args, newSVpvn("%s", 2));
327 3           av_push(flog_args, newSVsv(derefed));
328 3           flog_args_ref = sv_2mortal(newRV_noinc((SV*)flog_args));
329              
330 3           ENTER;
331 3           SAVETMPS;
332 3 50         PUSHMARK(SP);
333 3 50         XPUSHs(sv_2mortal(flogger_class));
334 3 50         XPUSHs(flog_args_ref);
335 3           PUTBACK;
336 3           count = call_method("flog", G_SCALAR);
337 3           SPAGAIN;
338 3 50         if (count > 0) {
339 3           value = SvREFCNT_inc(POPs);
340             } else {
341 0           value = &PL_sv_undef;
342             }
343 3           PUTBACK;
344 3 50         FREETMPS;
345 3           LEAVE;
346 3           value = sv_2mortal(value);
347             }
348              
349 64 100         if (!SvOK(value)) {
350             /* undef → ~missing~ */
351 1           value = sv_2mortal(newSVpvn("~missing~", 9));
352 63 100         } else if (SvROK(value)) {
353 34           UV refaddr = PTR2UV(SvRV(value));
354             char refaddr_str[32];
355 34           int refaddr_len = snprintf(refaddr_str, sizeof(refaddr_str),
356             "%" UVuf, refaddr);
357 34           SV **seen_svp = hv_fetch(seen, refaddr_str, refaddr_len, 0);
358              
359 34 100         if (seen_svp && SvOK(*seen_svp)) {
    50          
360             /* Already seen this ref — use the backreference */
361 2           value = *seen_svp;
362 32 100         } else if (!sv_isobject(value) &&
363 24 100         SvTYPE(SvRV(value)) == SVt_PVAV) {
364             /* Unblessed arrayref — recurse */
365 13           AV *arr = (AV*)SvRV(value);
366 13           SSize_t arr_len = av_len(arr);
367 13           AV *new_aref = newAV();
368             AV *sub_kvstrs;
369             SSize_t j, sub_len;
370             SV *backref;
371              
372             /* Store backreference in seen */
373 13           backref = newSVpvf("&%" SVf, SVfARG(key));
374 13           hv_store(seen, refaddr_str, refaddr_len, backref, 0);
375              
376             /* Build pairs: [ 0 => arr[0], 1 => arr[1], ... ] */
377 44 100         for (j = 0; j <= arr_len; j++) {
378 31           SV **elem = av_fetch(arr, j, 0);
379 31           av_push(new_aref, newSViv(j));
380 31 50         av_push(new_aref, elem ? newSVsv(*elem) : newSVsv(&PL_sv_undef));
381             }
382              
383 13           sub_kvstrs = pairs_to_kvstr_impl(aTHX_ self, new_aref, seen, key);
384 13           sub_len = av_len(sub_kvstrs);
385 54 100         for (j = 0; j <= sub_len; j++) {
386 41           SV **svp = av_fetch(sub_kvstrs, j, 0);
387 41 50         if (svp) av_push(kvstrs, newSVsv(*svp));
388             }
389 13           SvREFCNT_dec(new_aref);
390 13           SvREFCNT_dec(sub_kvstrs);
391 13           SvREFCNT_dec(str);
392 24           continue; /* next KEY */
393 19 100         } else if (!sv_isobject(value) &&
394 11 50         SvTYPE(SvRV(value)) == SVt_PVHV) {
395             /* Unblessed hashref — recurse with sorted keys */
396 11           HV *hv = (HV*)SvRV(value);
397             AV *sorted_keys;
398             AV *new_aref;
399             AV *sub_kvstrs;
400             SSize_t j, nkeys, sub_len;
401             SV *backref;
402             HE *entry;
403              
404             /* Store backreference in seen */
405 11           backref = newSVpvf("&%" SVf, SVfARG(key));
406 11           hv_store(seen, refaddr_str, refaddr_len, backref, 0);
407              
408             /* Collect and sort keys */
409 11           sorted_keys = newAV();
410 11           hv_iterinit(hv);
411 29 100         while ((entry = hv_iternext(hv))) {
412 18           av_push(sorted_keys, newSVsv(hv_iterkeysv(entry)));
413             }
414 11           nkeys = av_len(sorted_keys);
415 11 100         if (nkeys >= 1) {
416 6           sortsv(AvARRAY(sorted_keys), nkeys + 1, Perl_sv_cmp);
417             }
418              
419             /* Build pairs from sorted keys */
420 11           new_aref = newAV();
421 29 100         for (j = 0; j <= nkeys; j++) {
422 18           SV **kp = av_fetch(sorted_keys, j, 0);
423 18 50         if (kp) {
424             STRLEN klen;
425 18           const char *ks = SvPV(*kp, klen);
426 18 50         SV **vp = hv_fetch(hv, ks,
427             SvUTF8(*kp) ? -(I32)klen : (I32)klen, 0);
428 18           av_push(new_aref, newSVsv(*kp));
429 18 50         av_push(new_aref, vp ? newSVsv(*vp) : newSVsv(&PL_sv_undef));
430             }
431             }
432              
433 11           sub_kvstrs = pairs_to_kvstr_impl(aTHX_ self, new_aref, seen, key);
434 11           sub_len = av_len(sub_kvstrs);
435 38 100         for (j = 0; j <= sub_len; j++) {
436 27           SV **svp = av_fetch(sub_kvstrs, j, 0);
437 27 50         if (svp) av_push(kvstrs, newSVsv(*svp));
438             }
439 11           SvREFCNT_dec(sorted_keys);
440 11           SvREFCNT_dec(new_aref);
441 11           SvREFCNT_dec(sub_kvstrs);
442 11           SvREFCNT_dec(str);
443 11           continue; /* next KEY */
444             } else {
445             /* Other ref types — stringify */
446             STRLEN slen;
447             const char *ss;
448 8           SV *strval = newSVpvn("", 0);
449 8           sv_catsv(strval, value);
450 8           value = sv_2mortal(strval);
451             }
452             }
453              
454             /* Append value (bare or quoted) to the "key=" we built earlier */
455 40           val_s = SvPV(value, val_len);
456 40 100         if (is_bare_value(val_s, val_len)) {
457 16           sv_catpvn(str, val_s, val_len);
458             } else {
459 24           SV *quoted = quote_string_xs(aTHX_ value);
460 24           sv_catsv(str, quoted);
461 24           SvREFCNT_dec(quoted);
462             }
463              
464 40           av_push(kvstrs, str);
465             }
466              
467 85           return kvstrs;
468             }
469              
470              
471             MODULE = Log::Fmt::XS PACKAGE = Log::Fmt::XS
472              
473             PROTOTYPES: DISABLE
474              
475             SV *
476             _pairs_to_kvstr_aref(self, aref_ref, ...)
477             SV *self
478             SV *aref_ref
479             PREINIT:
480             AV *aref;
481             HV *seen;
482             SV *prefix;
483             AV *result;
484             CODE:
485 61 50         if (!SvROK(aref_ref) || SvTYPE(SvRV(aref_ref)) != SVt_PVAV) {
    50          
486 0           croak("Second argument must be an array reference");
487             }
488 61           aref = (AV *)SvRV(aref_ref);
489              
490             /* Handle optional $seen hashref */
491 61 50         if (items >= 3 && SvROK(ST(2)) && SvTYPE(SvRV(ST(2))) == SVt_PVHV) {
    0          
    0          
492 0           seen = (HV *)SvRV(ST(2));
493             } else {
494 61           seen = newHV();
495 61           sv_2mortal((SV*)seen);
496             }
497              
498             /* Handle optional $prefix */
499 61 50         if (items >= 4 && SvOK(ST(3))) {
    0          
500 0           prefix = ST(3);
501             } else {
502 61           prefix = NULL;
503             }
504              
505 61           result = pairs_to_kvstr_impl(aTHX_ self, aref, seen, prefix);
506 61           RETVAL = newRV_noinc((SV *)result);
507             OUTPUT:
508             RETVAL
509              
510             SV *
511             _quote_string(input)
512             SV *input
513             CODE:
514 0           RETVAL = quote_string_xs(aTHX_ input);
515             OUTPUT:
516             RETVAL