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