File Coverage

Gzip.xs
Criterion Covered Total %
statement 170 191 89.0
branch 113 174 64.9
condition n/a
subroutine n/a
pod n/a
total 283 365 77.5


line stmt bran cond sub pod time code
1             /*
2             * Gzip.xs - File::Raw plugin bindings for File::Raw::Gzip.
3             *
4             * Single plugin "gzip" registered at BOOT. READ inflates, WRITE
5             * deflates, STREAM inflates a chunk at a time and emits decompressed
6             * lines through the user callback (each_line). Per-call options arrive
7             * via FilePluginContext::options and are validated/decoded by
8             * decode_opts() below.
9             */
10              
11             #define PERL_NO_GET_CONTEXT
12             #include "EXTERN.h"
13             #include "perl.h"
14             #include "XSUB.h"
15              
16             /* gz.c uses raw libc malloc/realloc/free; the unsigned char *out
17             * pointers we receive back are libc-allocated. On Strawberry / Win32
18             * Perl (PERL_IMPLICIT_SYS), iperlsys.h redefines free as a 1-arg
19             * macro that routes through PerlMem_free, which tracks per-interpreter
20             * pool ownership. Freeing a libc-malloc'd pointer through
21             * PerlMem_free triggers "Free to wrong pool ... at t line N" at
22             * global destruction (or sooner) on every Gzip read/write call site.
23             * Undef the four memory macros so the free()s below resolve to the
24             * libc free that matches the libc malloc in gz.c. No-op on Unix. */
25             #undef malloc
26             #undef free
27             #undef realloc
28             #undef calloc
29              
30             #include "file_plugin.h"
31             #include "gz.h"
32              
33             #include
34             #include
35              
36             /* XS_EXTERNAL not defined on perl < 5.16. */
37             #ifndef XS_EXTERNAL
38             #define XS_EXTERNAL(name) XS(name)
39             #endif
40              
41             /* ============================================================
42             * Option decoding
43             * ============================================================ */
44              
45             static const char *VALID_OPT_KEYS[] = {
46             "level", "mode", "chunk_size", "strategy", "mem_level",
47             /* present in the HV File::Raw built for our dispatch call; we
48             * recognise and ignore it so unknown-key detection doesn't fire. */
49             "plugin",
50             NULL
51             };
52              
53             static int
54 121           known_opt(const char *key, STRLEN klen)
55             {
56             const char *const *p;
57 524 100         for (p = VALID_OPT_KEYS; *p; p++) {
58 523 100         if (strlen(*p) == klen && memcmp(*p, key, klen) == 0) return 1;
    50          
59             }
60 1           return 0;
61             }
62              
63             static gz_mode_t
64 42           parse_mode(const char *s, STRLEN len)
65             {
66 42 100         if (len == 4 && memcmp(s, "gzip", 4) == 0) return GZ_MODE_GZIP;
    100          
67 30 100         if (len == 4 && memcmp(s, "zlib", 4) == 0) return GZ_MODE_ZLIB;
    100          
68 16 100         if (len == 3 && memcmp(s, "raw", 3) == 0) return GZ_MODE_RAW;
    100          
69 3 100         if (len == 4 && memcmp(s, "auto", 4) == 0) return GZ_MODE_AUTO;
    50          
70 1           return (gz_mode_t)-1;
71             }
72              
73             static int
74 1           parse_strategy(const char *s, STRLEN len)
75             {
76 1 50         if (len == 7 && memcmp(s, "default", 7) == 0) return Z_DEFAULT_STRATEGY;
    0          
77 1 50         if (len == 8 && memcmp(s, "filtered", 8) == 0) return Z_FILTERED;
    0          
78 1 50         if (len == 12 && memcmp(s, "huffman_only",12) == 0) return Z_HUFFMAN_ONLY;
    0          
79 1 50         if (len == 3 && memcmp(s, "rle", 3) == 0) return Z_RLE;
    0          
80 1 50         if (len == 5 && memcmp(s, "fixed", 5) == 0) return Z_FIXED;
    0          
81 1           return -1;
82             }
83              
84             static void
85 74           decode_opts(pTHX_ HV *opts_hv, gz_options_t *opts)
86             {
87 74 50         if (!opts_hv) return;
88              
89 74           hv_iterinit(opts_hv);
90             HE *he;
91 186 100         while ((he = hv_iternext(opts_hv))) {
92             I32 klen_i;
93 121           const char *key = hv_iterkey(he, &klen_i);
94 121           STRLEN klen = (STRLEN)klen_i;
95 121           SV *val = hv_iterval(opts_hv, he);
96              
97 121 100         if (!known_opt(key, klen)) {
98 1           croak("File::Raw::Gzip: unknown option '%.*s'",
99             (int)klen, key);
100             }
101 120 50         if (!SvOK(val)) continue;
102              
103 120 100         if (klen == 5 && memcmp(key, "level", 5) == 0) {
    50          
104 5           IV n = SvIV(val);
105 5 100         if (n < 0 || n > 9)
    100          
106 2           croak("File::Raw::Gzip: level must be 0..9");
107 3           opts->level = (int)n;
108 115 100         } else if (klen == 4 && memcmp(key, "mode", 4) == 0) {
    50          
109             STRLEN slen;
110 42           const char *sp = SvPV(val, slen);
111 42           gz_mode_t m = parse_mode(sp, slen);
112 42 100         if ((int)m < 0)
113 1           croak("File::Raw::Gzip: mode must be one of "
114             "gzip / zlib / raw / auto");
115 41           opts->mode = m;
116 73 100         } else if (klen == 10 && memcmp(key, "chunk_size", 10) == 0) {
    50          
117 2           IV n = SvIV(val);
118 2 50         if (n <= 0 || n > (IV)(64 * 1024 * 1024))
    0          
119 2           croak("File::Raw::Gzip: chunk_size must be 1..67108864");
120 0           opts->chunk_size = (size_t)n;
121 71 100         } else if (klen == 8 && memcmp(key, "strategy", 8) == 0) {
    50          
122             STRLEN slen;
123 1           const char *sp = SvPV(val, slen);
124 1           int s = parse_strategy(sp, slen);
125 1 50         if (s < 0)
126 1           croak("File::Raw::Gzip: strategy must be one of "
127             "default / filtered / huffman_only / rle / fixed");
128 0           opts->strategy = s;
129 70 100         } else if (klen == 9 && memcmp(key, "mem_level", 9) == 0) {
    50          
130 2           IV n = SvIV(val);
131 2 100         if (n < 1 || n > 9)
    50          
132 2           croak("File::Raw::Gzip: mem_level must be 1..9");
133 0           opts->mem_level = (int)n;
134             }
135             /* "plugin" key: ignored. */
136             }
137             }
138              
139             /* ============================================================
140             * Plugin callbacks
141             * ============================================================ */
142              
143             static SV *
144 35           gz_read_cb(pTHX_ FilePluginContext *ctx)
145             {
146             gz_options_t opts;
147 35           gz_options_init(&opts);
148             /* Decode default already = AUTO; no further seeding needed. */
149 35 50         if (ctx->options) decode_opts(aTHX_ ctx->options, &opts);
150              
151 31 50         if (!ctx->data) return &PL_sv_undef;
152             STRLEN ilen;
153 31           const char *ipv = SvPV(ctx->data, ilen);
154              
155 31           unsigned char *out = NULL;
156 31           size_t out_cap = 0, out_len = 0;
157 31           gz_err_t rc = gz_inflate((const unsigned char *)ipv, ilen, &opts,
158             &out, &out_cap, &out_len);
159 31 100         if (rc != GZ_OK) {
160 2           free(out);
161 2           croak("File::Raw::Gzip: %s", gz_strerror(rc));
162             }
163              
164 29 100         SV *result = newSVpvn(out_len ? (const char *)out : "", out_len);
165 29           free(out);
166 29           return result;
167             }
168              
169             static SV *
170 30           gz_write_cb(pTHX_ FilePluginContext *ctx)
171             {
172             gz_options_t opts;
173 30           gz_options_init(&opts);
174             /* Encoder default: gzip wrap. AUTO is decode-only. */
175 30           opts.mode = GZ_MODE_GZIP;
176 30 50         if (ctx->options) decode_opts(aTHX_ ctx->options, &opts);
177              
178 25 50         if (!ctx->data) return &PL_sv_undef;
179             STRLEN ilen;
180 25           const char *ipv = SvPV(ctx->data, ilen);
181              
182 25           unsigned char *out = NULL;
183 25           size_t out_cap = 0, out_len = 0;
184 25           gz_err_t rc = gz_deflate((const unsigned char *)ipv, ilen, &opts,
185             &out, &out_cap, &out_len);
186 25 100         if (rc != GZ_OK) {
187 1           free(out);
188 1           croak("File::Raw::Gzip: %s", gz_strerror(rc));
189             }
190              
191 24 50         SV *result = newSVpvn(out_len ? (const char *)out : "", out_len);
192 24           free(out);
193 24           return result;
194             }
195              
196             /* ============================================================
197             * STREAM phase
198             *
199             * Driven by File::Raw::each_line($p, $cb, plugin => 'gzip'). File::Raw
200             * opens the file and feeds us raw bytes a chunk at a time; we own the
201             * inflate state and a "carry" buffer (the partial trailing line that
202             * hasn't seen a newline yet) across calls via FilePluginContext::
203             * call_state. Each complete decompressed line is emitted to the user
204             * callback with $_ bound to the line, mirroring File::Raw's builtin
205             * each_line.
206             * ============================================================ */
207              
208             typedef struct {
209             z_stream zs;
210             int zs_inited;
211             int stream_end; /* set once inflate returned Z_STREAM_END */
212             unsigned char *carry; /* partial line buffer (no '\n' inside) */
213             size_t carry_len;
214             size_t carry_cap;
215             SV *line_sv; /* reused $_ target across emissions */
216             } gz_stream_state_t;
217              
218             static int
219 9           mode_to_inflate_wbits(gz_mode_t mode)
220             {
221 9           switch (mode) {
222 0           case GZ_MODE_GZIP: return MAX_WBITS | 16; /* 31 */
223 1           case GZ_MODE_ZLIB: return MAX_WBITS; /* 15 */
224 1           case GZ_MODE_RAW: return -MAX_WBITS; /* -15 */
225 7           case GZ_MODE_AUTO: return MAX_WBITS | 32; /* 47 */
226             }
227 0           return MAX_WBITS | 32;
228             }
229              
230             static void
231 8           gz_stream_state_free(pTHX_ gz_stream_state_t *st)
232             {
233 8 50         if (!st) return;
234 8 50         if (st->zs_inited) { inflateEnd(&st->zs); st->zs_inited = 0; }
235 8 100         if (st->carry) { Safefree(st->carry); st->carry = NULL; }
236 8 50         if (st->line_sv) { SvREFCNT_dec(st->line_sv); st->line_sv = NULL; }
237 8           Safefree(st);
238             }
239              
240             static void
241 70           gz_carry_append(pTHX_ gz_stream_state_t *st, const unsigned char *p, size_t n)
242             {
243 70 50         if (!n) return;
244 70 100         if (st->carry_len + n > st->carry_cap) {
245 4 100         size_t want = st->carry_cap ? st->carry_cap : 256;
246 5 100         while (want < st->carry_len + n) want *= 2;
247 4           Renew(st->carry, want, unsigned char);
248 4           st->carry_cap = want;
249             }
250 70           memcpy(st->carry + st->carry_len, p, n);
251 70           st->carry_len += n;
252             }
253              
254             /* Emit one line: $_ = (carry ++ buf[0..n]) ; cb->() ; clear carry. */
255             static void
256 20456           gz_emit_line(pTHX_ gz_stream_state_t *st, SV *cb,
257             const unsigned char *buf, size_t n)
258             {
259 20456           dSP;
260 20456 100         if (st->carry_len) {
261 70           sv_setpvn(st->line_sv, (const char *)st->carry, st->carry_len);
262 70 100         if (n) sv_catpvn(st->line_sv, (const char *)buf, n);
263 70           st->carry_len = 0;
264             } else {
265 20386           sv_setpvn(st->line_sv, (const char *)buf, n);
266             }
267              
268 20456           ENTER;
269 20456           SAVETMPS;
270 20456           SAVE_DEFSV;
271 20456 50         DEFSV = st->line_sv;
272 20456 50         PUSHMARK(SP);
273 20456           call_sv(cb, G_VOID | G_DISCARD);
274 20455 50         FREETMPS;
275 20455           LEAVE;
276 20455           }
277              
278             /* Scan `buf` for newlines, emitting one line per newline and stashing
279             * the trailing partial line in st->carry. */
280             static void
281 77           gz_split_and_emit(pTHX_ gz_stream_state_t *st, SV *cb,
282             const unsigned char *buf, size_t n)
283             {
284 77           const unsigned char *p = buf;
285 77           const unsigned char *end = buf + n;
286 20531 100         while (p < end) {
287             const unsigned char *nl = (const unsigned char *)
288 20525           memchr(p, '\n', (size_t)(end - p));
289 20525 100         if (!nl) {
290 70           gz_carry_append(aTHX_ st, p, (size_t)(end - p));
291 70           return;
292             }
293 20455           gz_emit_line(aTHX_ st, cb, p, (size_t)(nl - p));
294 20454           p = nl + 1;
295             }
296             }
297              
298             static int
299 16           gz_stream_cb(pTHX_ FilePluginContext *ctx,
300             const char *chunk, size_t len, int eof)
301             {
302 16           gz_stream_state_t *st = (gz_stream_state_t *)ctx->call_state;
303 16           SV *cb = ctx->callback;
304             unsigned char zout[64 * 1024];
305             int z_rc;
306              
307 16 100         if (!st) {
308             gz_options_t opts;
309             int wbits;
310 9           gz_options_init(&opts);
311 9 50         if (ctx->options) decode_opts(aTHX_ ctx->options, &opts);
312 9           wbits = mode_to_inflate_wbits(opts.mode);
313              
314 9           Newxz(st, 1, gz_stream_state_t);
315 9           st->line_sv = newSV(256);
316              
317 9           z_rc = inflateInit2(&st->zs, wbits);
318 9 50         if (z_rc != Z_OK) {
319 0           gz_stream_state_free(aTHX_ st);
320 0           ctx->cancel = 1;
321 0           croak("File::Raw::Gzip: zlib inflateInit2 failed (%d)", z_rc);
322             }
323 9           st->zs_inited = 1;
324 9           ctx->call_state = st;
325             }
326              
327 16 100         if (chunk && len > 0 && !st->stream_end) {
    50          
    50          
328 9           st->zs.next_in = (Bytef *)chunk;
329 9           st->zs.avail_in = (uInt)len;
330 78 50         while (st->zs.avail_in > 0 && !st->stream_end) {
    50          
331 78           st->zs.next_out = zout;
332 78           st->zs.avail_out = (uInt)sizeof(zout);
333 78           z_rc = inflate(&st->zs, Z_NO_FLUSH);
334             {
335 78           size_t produced = sizeof(zout) - st->zs.avail_out;
336 78 100         if (produced) gz_split_and_emit(aTHX_ st, cb, zout, produced);
337             }
338 77 100         if (z_rc == Z_STREAM_END) { st->stream_end = 1; break; }
339 70 50         if (z_rc == Z_BUF_ERROR && st->zs.avail_out > 0) {
    0          
340             /* Needs more input; will get it on next chunk. */
341 0           break;
342             }
343 70 100         if (z_rc != Z_OK) {
344 1           gz_stream_state_free(aTHX_ st);
345 1           ctx->call_state = NULL;
346 1           ctx->cancel = 1;
347 1           croak("File::Raw::Gzip: %s", zError(z_rc));
348             }
349             }
350             }
351              
352 14 100         if (eof) {
353             /* Drain any tail still buffered inside zlib (unlikely after the
354             * loop above for a healthy stream, but safe to attempt). */
355 7 50         if (!st->stream_end) {
356             do {
357 0           st->zs.next_out = zout;
358 0           st->zs.avail_out = (uInt)sizeof(zout);
359 0           z_rc = inflate(&st->zs, Z_FINISH);
360             {
361 0           size_t produced = sizeof(zout) - st->zs.avail_out;
362 0 0         if (produced) gz_split_and_emit(aTHX_ st, cb, zout, produced);
363             }
364 0 0         if (z_rc == Z_STREAM_END) { st->stream_end = 1; break; }
365 0 0         if (z_rc == Z_BUF_ERROR && st->zs.avail_out == 0) continue;
    0          
366 0 0         if (z_rc == Z_OK) continue;
367             /* Any other code: input ended before the gzip trailer. */
368 0           gz_stream_state_free(aTHX_ st);
369 0           ctx->call_state = NULL;
370 0           ctx->cancel = 1;
371 0           croak("File::Raw::Gzip: input ended before stream completion");
372             } while (1);
373             }
374 7 100         if (st->carry_len) {
375 1           gz_emit_line(aTHX_ st, cb, NULL, 0);
376             }
377 7           gz_stream_state_free(aTHX_ st);
378 7           ctx->call_state = NULL;
379             }
380              
381 14           return 0;
382             }
383              
384             /* Plugin descriptor. Static-storage lifetime so the registry's
385             * non-owning pointer stays valid for the life of the process. */
386             static FilePlugin gzip_plugin;
387              
388             /* ============================================================ */
389              
390             MODULE = File::Raw::Gzip PACKAGE = File::Raw::Gzip
391              
392             PROTOTYPES: DISABLE
393              
394             BOOT:
395 16           memset(&gzip_plugin, 0, sizeof gzip_plugin);
396 16           gzip_plugin.name = "gzip";
397 16           gzip_plugin.read_fn = gz_read_cb;
398 16           gzip_plugin.write_fn = gz_write_cb;
399 16           gzip_plugin.record_fn = NULL;
400 16           gzip_plugin.stream_fn = gz_stream_cb;
401 16           gzip_plugin.state = NULL;
402 16           file_register_plugin(aTHX_ &gzip_plugin);