File Coverage

Replace.xs
Criterion Covered Total %
statement 241 263 91.6
branch 252 356 70.7
condition n/a
subroutine n/a
pod n/a
total 493 619 79.6


line stmt bran cond sub pod time code
1             /*
2             *
3             * Copyright (c) 2018, cPanel, LLC.
4             * All rights reserved.
5             * http://cpanel.net
6             *
7             * This is free software; you can redistribute it and/or modify it under the
8             * same terms as Perl itself.
9             *
10             */
11              
12             #include
13             #include
14             #include
15             #include
16              
17             #define IS_SPACE(c) ((c) == ' ' || (c) == '\n' || (c) == '\r' || (c) == '\t' || (c) == '\f' || (c) == '\v')
18              
19             /*
20             * UTF8_SEQ_LEN: given a lead byte c (>= 0x80), return the expected
21             * number of bytes in the UTF-8 sequence. Continuation bytes (0x80-0xBF)
22             * return 1 (copy-as-is).
23             */
24             #define UTF8_SEQ_LEN(c) \
25             ( (c) >= 0xFC ? 6 : \
26             (c) >= 0xF8 ? 5 : \
27             (c) >= 0xF0 ? 4 : \
28             (c) >= 0xE0 ? 3 : \
29             (c) >= 0xC0 ? 2 : 1 )
30              
31             #define IS_CODEREF(sv) (SvROK(sv) && SvTYPE(SvRV(sv)) == SVt_PVCV)
32             #define PROPAGATE_TAINT(from, to) do { if (SvTAINTED(from)) SvTAINTED_on(to); } while (0)
33              
34             /* croak_sv was introduced in Perl 5.18; provide fallback for older versions */
35             #if PERL_VERSION < 18
36             #define croak_sv(sv) croak("%s", SvPV_nolen(sv))
37             #endif
38              
39             SV *_replace_str( SV *sv, SV *map );
40             SV *_trim_sv( SV *sv );
41             IV _replace_inplace( SV *sv, SV *map );
42             IV _trim_inplace( SV *sv );
43              
44             /*
45             * ensure_buffer_space: grow the buffer if needed to accommodate additional bytes.
46             * Returns the updated string pointer (SvGROW may relocate).
47             */
48 447           static inline char *ensure_buffer_space(SV *sv, STRLEN *str_size, STRLEN needed) {
49 447 100         if (*str_size <= needed) {
50 10 100         while (*str_size <= needed) {
51 5           *str_size *= 2;
52             }
53 5 50         SvGROW(sv, *str_size);
    50          
54             }
55 447           return SvPVX(sv);
56             }
57              
58             /*
59             * copy_replacement: copy replacement string into buffer, handling multi-char replacements.
60             * Returns the new buffer position.
61             */
62 447           static inline STRLEN copy_replacement(char *str, STRLEN ix, const char *replace, STRLEN slen) {
63             STRLEN j;
64 1311 100         for (j = 0; j < slen - 1; ++j) {
65 864           str[ix++] = replace[j];
66             }
67 447           str[ix] = replace[j];
68 447           return ix;
69             }
70              
71             /*
72             * _build_fast_map: populate a 256-byte identity lookup table, then
73             * overwrite entries according to the Perl map array.
74             *
75             * Returns 1 if every map entry is a 1:1 byte replacement (fast-path
76             * eligible). Returns 0 if any entry requires expansion, deletion,
77             * or is otherwise incompatible — the caller should fall through to
78             * the general path.
79             */
80 1119           static int _build_fast_map( char fast_map[256], SV **ary, SSize_t map_top ) {
81             dTHX;
82             int ix;
83 1119           SSize_t scan_top = map_top < 255 ? map_top : 255;
84              
85 287583 100         for ( ix = 0; ix < 256; ++ix )
86 286464           fast_map[ix] = (char) ix;
87              
88 119723 100         for ( ix = 0; ix <= scan_top; ++ix ) {
89             SV *entry;
90 119667 50         if ( !ary[ix] )
91 0           continue;
92 119667           entry = ary[ix];
93 119667 100         if ( SvPOK( entry ) ) {
94             STRLEN slen;
95 118614           char *pv = SvPV( entry, slen );
96 118614 100         if ( slen == 1 ) {
97 118572           fast_map[ix] = pv[0];
98             } else {
99 42           return 0;
100             }
101 1084 100         } else if ( SvIOK( entry ) || SvNOK( entry ) ) {
    100          
102 31           IV val = SvIV( entry );
103 31 100         if ( val >= 0 && val <= 255 ) {
    100          
104 25           fast_map[ix] = (char) val;
105             }
106             /* out-of-range: keep identity (already set) */
107             }
108             /* code ref: not eligible for fast path */
109 1022 100         else if ( IS_CODEREF( entry ) ) {
    50          
110 1021           return 0;
111             }
112             /* undef/other: identity (already set) */
113             }
114 56           return 1;
115             }
116              
117 33           SV *_trim_sv( SV *sv ) {
118             dTHX;
119 33           STRLEN len = SvCUR(sv);
120 33           char *str = SvPVX(sv);
121             char *end;
122             SV *reply;
123              
124 33 100         if ( len == 0 ) {
125 3           reply = newSVpvn_flags( str, 0, SvUTF8(sv) );
126 3 100         PROPAGATE_TAINT(sv, reply);
    50          
    50          
127 3           return reply;
128             }
129              
130 30           end = str + len - 1;
131              
132             // Skip whitespace at front...
133 86 100         while ( len > 0 && IS_SPACE( (unsigned char) *str) ) {
    100          
    100          
    100          
    100          
    100          
    100          
134 56           ++str;
135 56           --len;
136             }
137              
138             // Trim at end...
139 69 100         while (end > str && IS_SPACE( (unsigned char) *end) ) {
    100          
    100          
    100          
    100          
    100          
    100          
140 39           end--;
141 39           --len;
142             }
143              
144 30           reply = newSVpvn_flags( str, len, SvUTF8(sv) );
145 30 100         PROPAGATE_TAINT(sv, reply);
    50          
    50          
146 30           return reply;
147             }
148              
149             /*
150             * _trim_inplace: remove leading and trailing whitespace from an SV
151             * in place (no allocation).
152             *
153             * Returns the total number of whitespace bytes removed.
154             * Uses sv_chop() to advance past leading whitespace efficiently,
155             * and adjusts SvCUR for trailing whitespace.
156             */
157 31           IV _trim_inplace( SV *sv ) {
158             dTHX;
159             STRLEN len;
160             char *str;
161             char *end;
162 31           STRLEN lead = 0;
163 31           STRLEN trail = 0;
164              
165 31           SvPV_force_nolen(sv);
166 31           str = SvPVX(sv);
167 31           len = SvCUR(sv);
168              
169 31 100         if ( len == 0 )
170 2           return 0;
171              
172 29           end = str + len - 1;
173              
174             /* count and skip leading whitespace */
175 79 100         while ( lead < len && IS_SPACE( (unsigned char) str[lead] ) )
    100          
    100          
    100          
    100          
    100          
    50          
176 50           ++lead;
177              
178             /* count trailing whitespace (don't go past the leading trim point) */
179 69 100         while ( end > (str + lead) && IS_SPACE( (unsigned char) *end ) ) {
    100          
    100          
    100          
    100          
    100          
    50          
180 40           --end;
181 40           ++trail;
182             }
183              
184 29 100         if ( lead == 0 && trail == 0 )
    100          
185 5           return 0;
186              
187             /* trim trailing first (just shorten the string) */
188 24 100         if ( trail ) {
189 20           SvCUR_set(sv, len - trail);
190 20           SvPVX(sv)[len - trail] = '\0';
191             }
192              
193             /* trim leading via sv_chop (adjusts PVX pointer + OOK offset) */
194 24 100         if ( lead )
195 23           sv_chop(sv, SvPVX(sv) + lead);
196              
197 24 100         SvSETMAGIC(sv);
198 24           return (IV)(lead + trail);
199             }
200              
201              
202 1103           SV *_replace_str( SV *sv, SV *map ) {
203             dTHX;
204             STRLEN len;
205             char *src;
206 1103           STRLEN i = 0;
207             char *ptr;
208             char *str; /* pointer into reply SV's buffer */
209             STRLEN str_size; /* start with input length + some padding */
210 1103           STRLEN ix_newstr = 0;
211             AV *mapav;
212             SV *reply;
213             SSize_t map_top; /* highest valid index in the map */
214             int is_utf8; /* whether the input string is UTF-8 */
215              
216 1103 50         if ( !map || SvTYPE(map) != SVt_RV || SvTYPE(SvRV(map)) != SVt_PVAV
    100          
    50          
217 1101 50         || AvFILL( SvRV(map) ) < 0
    0          
    100          
218             ) {
219 4           src = SvPV(sv, len);
220 4           reply = newSVpvn_flags( src, len, SvUTF8(sv) );
221 4 100         PROPAGATE_TAINT(sv, reply);
    50          
    50          
222 4           return reply;
223             }
224              
225 1099           src = SvPV(sv, len);
226 1099           ptr = src;
227 1099           str_size = len + 64;
228              
229 1099           mapav = (AV *)SvRV(map);
230 1099           SV **ary = AvARRAY(mapav);
231 1099 50         map_top = AvFILL(mapav);
232 1099           is_utf8 = SvUTF8(sv) ? 1 : 0;
233              
234             /*
235             * Fast path: when every map entry is a 1:1 byte replacement (single-char
236             * PV, IV 0-255, or identity), we precompute a 256-byte lookup table and
237             * avoid per-byte SV type dispatch entirely.
238             */
239             {
240             char fast_map[256];
241              
242 1099 100         if ( _build_fast_map( fast_map, ary, map_top ) ) {
243 39           reply = newSV( len + 1 );
244 39           SvPOK_on(reply);
245 39           str = SvPVX(reply);
246              
247 39 100         if ( !is_utf8 ) {
248             /* tight loop: no SV dispatch, no UTF-8 checks */
249 950 100         for ( i = 0; i < len; ++i )
250 918           str[i] = fast_map[(unsigned char) src[i]];
251              
252 32           str[len] = '\0';
253 32           SvCUR_set(reply, len);
254             } else {
255             /* UTF-8 aware fast path: use table for ASCII, copy multi-byte sequences */
256 7           STRLEN out = 0;
257 47 100         for ( i = 0; i < len; ++i, ++out ) {
258 40           unsigned char c = (unsigned char) src[i];
259 40 100         if ( c >= 0x80 ) {
260 22 50         STRLEN seq_len = UTF8_SEQ_LEN(c);
    50          
    100          
    50          
    50          
261             STRLEN k;
262 22 50         if ( i + seq_len > len ) seq_len = len - i;
263 68 100         for ( k = 0; k < seq_len; ++k )
264 46           str[out + k] = src[i + k];
265 22           i += seq_len - 1; /* -1: loop increments */
266 22           out += seq_len - 1; /* -1: loop increments */
267             } else {
268 18           str[out] = fast_map[c];
269             }
270             }
271              
272 7           str[out] = '\0';
273 7           SvCUR_set(reply, out);
274             }
275              
276 39 100         if ( SvUTF8(sv) )
277 7           SvUTF8_on(reply);
278 39 100         PROPAGATE_TAINT(sv, reply);
    50          
    50          
279 39           return reply;
280             }
281             }
282             /* end fast path — fall through to general path */
283              
284             /*
285             * Allocate the reply SV up front and write directly into its buffer.
286             * This avoids Newx + newSVpvn_flags + Safefree (one alloc + copy saved).
287             */
288 1060           reply = newSV( str_size );
289 1060           SvPOK_on(reply);
290 1060           str = SvPVX(reply);
291              
292 1669 100         for ( i = 0; i < len; ++i, ++ptr, ++ix_newstr ) {
293 1610           unsigned char c = (unsigned char) *ptr;
294 1610           int ix = (int) c;
295              
296             /*
297             * UTF-8 safety: when the input has the UTF-8 flag set,
298             * multi-byte sequences (bytes >= 0x80) must be copied through
299             * unchanged. We only apply the replacement map to ASCII bytes
300             * (0x00–0x7F). This prevents corrupting multi-byte characters
301             * whose continuation bytes might collide with map entries.
302             */
303 1610 100         if ( is_utf8 && c >= 0x80 ) {
    100          
304 59 50         STRLEN seq_len = UTF8_SEQ_LEN(c);
    50          
    50          
    100          
    50          
305              
306             /* clamp to remaining bytes to avoid overread on malformed input */
307 59 50         if ( i + seq_len > len ) seq_len = len - i;
308              
309             /* ensure buffer has room */
310 59 50         if ( str_size <= (ix_newstr + seq_len + 1) ) {
311 0 0         while ( str_size <= (ix_newstr + seq_len + 1) )
312 0           str_size *= 2;
313 0 0         SvGROW( reply, str_size );
    0          
314 0           str = SvPVX(reply);
315             }
316              
317             /* copy the entire multi-byte sequence */
318 59           str[ix_newstr] = (char) c;
319             {
320             STRLEN k;
321 119 100         for ( k = 1; k < seq_len; ++k ) {
322 60           ++i; ++ptr; ++ix_newstr;
323 60           str[ix_newstr] = *ptr;
324             }
325             }
326 59           continue;
327             }
328              
329 1551           str[ix_newstr] = (char) c; /* default always performed... */
330 1551 50         if ( ix > map_top
331 1551 50         || !ary[ix]
332             ) {
333 0           continue;
334             } else {
335 1551           SV *entry = ary[ix];
336 1551 100         if ( SvPOK( entry ) ) {
337             STRLEN slen;
338 415           char *replace = SvPV( entry, slen ); /* length of the string used for replacement */
339 415 100         if ( slen == 0 ) {
340 97           --ix_newstr;
341 97           continue;
342             } else {
343 318           str = ensure_buffer_space(reply, &str_size, ix_newstr + slen + 1);
344 318           ix_newstr = copy_replacement(str, ix_newstr, replace, slen);
345             }
346 1139 100         } else if ( SvIOK( entry ) || SvNOK( entry ) ) {
    50          
347             /* IV/NV support: treat the integer value as an ordinal (chr) */
348 3           IV val = SvIV( entry );
349 3 50         if ( val >= 0 && val <= 255 ) {
    50          
350 3           str[ix_newstr] = (char) val;
351             }
352             /* out-of-range values: keep original character (already written) */
353 1133 50         } else if ( IS_CODEREF( entry ) ) {
    50          
354             /* Code ref: call the sub with the character as argument */
355 1133           dSP;
356             SV *arg;
357             SV *result;
358             I32 count;
359             char ch_buf[2];
360              
361 1133           ch_buf[0] = (char) c;
362 1133           ch_buf[1] = '\0';
363 1133           arg = newSVpvn( ch_buf, 1 );
364 1133 100         if ( is_utf8 )
365 1           SvUTF8_on( arg );
366             /* Propagate taint from source to callback argument */
367 1133 100         if ( SvTAINTED(sv) )
    50          
368 2 50         SvTAINTED_on( arg );
369 1133           sv_2mortal( arg );
370              
371 1133           ENTER;
372 1133           SAVETMPS;
373              
374 1133 50         PUSHMARK(SP);
375 1133 50         XPUSHs( arg );
376 1133           PUTBACK;
377              
378 1133           count = call_sv( SvRV( entry ), G_SCALAR | G_EVAL );
379              
380 1133           SPAGAIN;
381              
382 1133 50         if ( SvTRUE( ERRSV ) ) {
    100          
383             /* Callback died: clean up the reply SV we allocated,
384             * then re-throw so the caller sees the original error. */
385 1001           (void) POPs;
386 1001           PUTBACK;
387 1001 50         FREETMPS;
388 1001           LEAVE;
389 1001           SvREFCNT_dec(reply);
390 1001 50         croak_sv( ERRSV );
391             }
392              
393 132 50         if ( count == 1 ) {
394 132           result = POPs;
395 132 100         if ( SvOK( result ) ) {
396             STRLEN slen;
397 131           char *replace = SvPV( result, slen );
398             /* SvPV guaranteed non-NULL for valid SV; SvOK check above ensures valid SV */
399              
400 131 100         if ( slen == 0 ) {
401 2           --ix_newstr;
402             } else {
403 129           str = ensure_buffer_space(reply, &str_size, ix_newstr + slen + 1);
404 129           ix_newstr = copy_replacement(str, ix_newstr, replace, slen);
405             }
406             }
407             /* undef result: keep original (already written) */
408             }
409              
410 132           PUTBACK;
411 132 50         FREETMPS;
412 132           LEAVE;
413             } /* end - SvPOK / SvIOK / SvNOK / code ref */
414             } /* end - map_top || AvARRAY */
415             }
416              
417 59           str[ix_newstr] = '\0';
418 59           SvCUR_set(reply, ix_newstr);
419 59 100         if ( SvUTF8(sv) )
420 10           SvUTF8_on(reply);
421 59 100         PROPAGATE_TAINT(sv, reply);
    50          
    50          
422              
423 59           return reply;
424             }
425              
426             /*
427             * _replace_inplace: modify the SV's string buffer directly.
428             *
429             * Only supports 1:1 byte replacements: each map entry must be either
430             * undef (keep original), a single-character PV, or an IV/NV in 0-255.
431             * Entries that would expand or delete characters cause a croak.
432             *
433             * Returns the number of bytes actually changed.
434             * UTF-8 safe: multi-byte sequences (>= 0x80) are skipped.
435             */
436 22           IV _replace_inplace( SV *sv, SV *map ) {
437             dTHX;
438             STRLEN len;
439             char *str;
440             STRLEN i;
441             AV *mapav;
442             SV **ary;
443             SSize_t map_top;
444             int is_utf8;
445 22           IV count = 0;
446              
447 22 50         if ( !map || SvTYPE(map) != SVt_RV || SvTYPE(SvRV(map)) != SVt_PVAV
    100          
    50          
448 21 50         || AvFILL( SvRV(map) ) < 0
    0          
    100          
449             ) {
450 2           return 0; /* no valid map, nothing to do */
451             }
452              
453             /* make the SV writable (COW handling) */
454 20           SvPV_force_nolen(sv);
455 20           str = SvPVX(sv);
456 20           len = SvCUR(sv);
457              
458 20           mapav = (AV *)SvRV(map);
459 20           ary = AvARRAY(mapav);
460 20 50         map_top = AvFILL(mapav);
461 20           is_utf8 = SvUTF8(sv) ? 1 : 0;
462              
463             /*
464             * Fast path: precompute a 256-byte lookup table.
465             * Only valid when all map entries are 1:1 byte replacements.
466             * Croaks on multi-char/empty entries are deferred to the general path.
467             */
468             {
469             char fast_map[256];
470              
471 20 100         if ( _build_fast_map( fast_map, ary, map_top ) ) {
472 17 100         if ( !is_utf8 ) {
473 314 100         for ( i = 0; i < len; ++i ) {
474 300           char replacement = fast_map[(unsigned char) str[i]];
475 300 100         if ( str[i] != replacement ) {
476 229           str[i] = replacement;
477 229           ++count;
478             }
479             }
480             } else {
481 14 100         for ( i = 0; i < len; ++i ) {
482 11           unsigned char c = (unsigned char) str[i];
483 11 100         if ( c >= 0x80 ) {
484 3 50         STRLEN seq_len = UTF8_SEQ_LEN(c);
    50          
    100          
    100          
    50          
485 3 50         if ( i + seq_len > len ) seq_len = len - i;
486 3           i += seq_len - 1;
487 3           continue;
488             }
489             {
490 8           char replacement = fast_map[c];
491 8 100         if ( str[i] != replacement ) {
492 5           str[i] = replacement;
493 5           ++count;
494             }
495             }
496             }
497             }
498 17 100         if ( count )
499 14 100         SvSETMAGIC(sv);
500 17           return count;
501             }
502             }
503             /* end fast path */
504              
505 3 50         for ( i = 0; i < len; ++i ) {
506 3           unsigned char c = (unsigned char) str[i];
507 3           int ix = (int) c;
508              
509             /* UTF-8 safety: skip multi-byte sequences */
510 3 50         if ( is_utf8 && c >= 0x80 ) {
    0          
511 0 0         STRLEN seq_len = UTF8_SEQ_LEN(c);
    0          
    0          
    0          
    0          
512 0 0         if ( i + seq_len > len ) seq_len = len - i;
513 0           i += seq_len - 1; /* -1 because the loop increments */
514 0           continue;
515             }
516              
517 3 50         if ( ix > map_top || !ary[ix] )
    50          
518 0           continue;
519              
520             {
521 3           SV *entry = ary[ix];
522 3 100         if ( SvPOK( entry ) ) {
523             STRLEN slen;
524 2           char *replace = SvPV( entry, slen );
525 2 50         if ( slen == 1 ) {
526 0 0         if ( str[i] != replace[0] ) {
527 0           str[i] = replace[0];
528 0           ++count;
529             }
530             } else {
531 2           croak("replace_inplace: map entry for byte %d is a %"UVuf"-char string"
532             " (only single-char replacements allowed)", ix, (UV)slen);
533             }
534 1 50         } else if ( SvIOK( entry ) || SvNOK( entry ) ) {
    50          
535 0           IV val = SvIV( entry );
536 0 0         if ( val >= 0 && val <= 255 ) {
    0          
537 0 0         if ( str[i] != (char) val ) {
538 0           str[i] = (char) val;
539 0           ++count;
540             }
541             }
542             /* out-of-range: keep original */
543 1 50         } else if ( IS_CODEREF( entry ) ) {
    50          
544 1           croak("replace_inplace: map entry for byte %d is a code ref"
545             " (not supported for in-place replacement; use replace() instead)", ix);
546             }
547             }
548             }
549              
550 0 0         if ( count )
551 0 0         SvSETMAGIC(sv);
552 0           return count;
553             }
554              
555             MODULE = Char__Replace PACKAGE = Char::Replace
556              
557             SV*
558             replace(sv, map)
559             SV *sv;
560             SV *map;
561             CODE:
562 1106 50         if ( sv && SvPOK(sv) ) {
    100          
563 1103           RETVAL = _replace_str( sv, map );
564             } else {
565 3           RETVAL = &PL_sv_undef;
566             }
567             OUTPUT:
568             RETVAL
569              
570             SV*
571             trim(sv)
572             SV *sv;
573             CODE:
574 36 50         if ( sv && SvPOK(sv) ) {
    100          
575 33           RETVAL = _trim_sv( sv );
576             } else {
577 3           RETVAL = &PL_sv_undef;
578             }
579             OUTPUT:
580             RETVAL
581              
582             IV
583             replace_inplace(sv, map)
584             SV *sv;
585             SV *map;
586             CODE:
587 24 50         if ( sv && SvPOK(sv) ) {
    100          
588 22           RETVAL = _replace_inplace( sv, map );
589             } else {
590 2           RETVAL = 0;
591             }
592             OUTPUT:
593             RETVAL
594              
595             IV
596             trim_inplace(sv)
597             SV *sv;
598             CODE:
599 33 50         if ( sv && SvPOK(sv) ) {
    100          
600 31           RETVAL = _trim_inplace( sv );
601             } else {
602 2           RETVAL = 0;
603             }
604             OUTPUT:
605             RETVAL