File Coverage

Hash.xs
Criterion Covered Total %
statement 196 231 84.8
branch 136 176 77.2
condition n/a
subroutine n/a
pod n/a
total 332 407 81.5


line stmt bran cond sub pod time code
1             /*
2             * Hash.xs - File::Raw plugin bindings for File::Raw::Hash.
3             *
4             * file_slurp($p, plugin => 'hash', algo => 'sha256', into => \my $d);
5             * file_slurp($p, plugin => 'hash', algos => [qw(sha256 md5)],
6             * into => \my %digests);
7             */
8              
9             #define PERL_NO_GET_CONTEXT
10             #include "EXTERN.h"
11             #include "perl.h"
12             #include "XSUB.h"
13              
14             #include "file_plugin.h"
15             #include "hashx.h"
16              
17             #include
18             #include
19              
20             /* ============================================================
21             * Resolved options for a single plugin call.
22             * ============================================================ */
23              
24             #define MAX_ALGOS 8
25              
26             typedef struct {
27             hash_algo_id_t ids[MAX_ALGOS];
28             int n_ids;
29             int multi; /* 0 = `algo`, 1 = `algos` */
30             hash_format_t format;
31             SV *into; /* the user's ref SV; not refcount-bumped here */
32             SV *hmac_key_sv; /* NULL if not set */
33             int xxh64_seed_set;
34             uint64_t xxh64_seed;
35             } hash_opts_t;
36              
37             /* Phase context for `into` validation. RECORD wants an arrayref
38             * because we push one entry per record; READ/WRITE/STREAM want a
39             * scalar (single algo) or hash (multi-algo) ref. */
40             typedef enum {
41             PHASE_ONE_SHOT = 0,
42             PHASE_RECORD = 1
43             } decode_phase_t;
44              
45             static int
46 2800           str_eq(const char *a, STRLEN alen, const char *b)
47             {
48 2800 100         return alen == strlen(b) && memcmp(a, b, alen) == 0;
    100          
49             }
50              
51             static const char *VALID_OPT_KEYS[] = {
52             "algo", "algos", "into", "format",
53             "hmac_key", "xxh64_seed",
54             "plugin", /* always present from the dispatcher */
55             NULL
56             };
57              
58             static int
59 382           known_opt(const char *key, STRLEN klen)
60             {
61             const char *const *p;
62 1463 100         for (p = VALID_OPT_KEYS; *p; p++) {
63 1462 100         if (str_eq(key, klen, *p)) return 1;
64             }
65 1           return 0;
66             }
67              
68             /* Parse one algo name SV, croak on unknown. */
69             static hash_algo_id_t
70 134           parse_algo_sv(pTHX_ SV *sv)
71             {
72             STRLEN alen;
73             const char *ap;
74             const hash_algo_info_t *info;
75              
76 134 50         if (!SvOK(sv))
77 0           croak("File::Raw::Hash: algo name must not be undef");
78 134 50         if (SvROK(sv))
79 0           croak("File::Raw::Hash: algo name must be a string, not a reference");
80 134           ap = SvPV(sv, alen);
81 134           info = hash_algo_lookup(ap, alen);
82 134 100         if (!info)
83 1           croak("File::Raw::Hash: unknown algo '%.*s' "
84             "(known: sha256 sha512 sha1 md5 crc32 xxh64 blake3)",
85             (int)alen, ap);
86 133           return info->id;
87             }
88              
89             /* Decode the per-call options HV into a hash_opts_t. Croaks on any
90             * validation failure. Caller passes a zeroed struct. */
91             static void
92 117           decode_opts(pTHX_ HV *opts_hv, hash_opts_t *opts, decode_phase_t phase)
93             {
94             HE *he;
95 117           SV *algo_sv = NULL;
96 117           SV *algos_sv = NULL;
97 117           SV *fmt_sv = NULL;
98 117           SV *seed_sv = NULL;
99              
100 117 50         if (!opts_hv) croak("File::Raw::Hash: missing options");
101              
102             /* First pass: validate keys + grab the ones we care about. */
103 117           hv_iterinit(opts_hv);
104 498 100         while ((he = hv_iternext(opts_hv))) {
105             I32 klen_i;
106 382           const char *key = hv_iterkey(he, &klen_i);
107 382           STRLEN klen = (STRLEN)klen_i;
108 382           SV *val = hv_iterval(opts_hv, he);
109              
110 382 100         if (!known_opt(key, klen)) {
111 1           croak("File::Raw::Hash: unknown option '%.*s' (known: algo, "
112             "algos, into, format, hmac_key, xxh64_seed)",
113             (int)klen, key);
114             }
115 381 100         if (str_eq(key, klen, "algo")) algo_sv = val;
116 280 100         else if (str_eq(key, klen, "algos")) algos_sv = val;
117 264 100         else if (str_eq(key, klen, "into")) opts->into = val;
118 149 100         else if (str_eq(key, klen, "format")) fmt_sv = val;
119 140 100         else if (str_eq(key, klen, "hmac_key")) opts->hmac_key_sv = val;
120 124 100         else if (str_eq(key, klen, "xxh64_seed")) seed_sv = val;
121             /* "plugin" key: silently ignored. */
122             }
123              
124             /* Mutual exclusion. */
125 116 100         if (algo_sv && SvOK(algo_sv) && algos_sv && SvOK(algos_sv))
    50          
    100          
    50          
126 1           croak("File::Raw::Hash: 'algo' and 'algos' are mutually exclusive");
127              
128             /* Resolve algorithm list. */
129 115 100         if (algos_sv && SvOK(algos_sv)) {
    50          
130             AV *av;
131             SSize_t n, i;
132 15 100         if (!SvROK(algos_sv) || SvTYPE(SvRV(algos_sv)) != SVt_PVAV)
    50          
133 1           croak("File::Raw::Hash: 'algos' must be an arrayref");
134 14           av = (AV *)SvRV(algos_sv);
135 14           n = av_len(av) + 1;
136 14 100         if (n < 1)
137 1           croak("File::Raw::Hash: 'algos' arrayref is empty");
138 13 50         if (n > MAX_ALGOS)
139 0           croak("File::Raw::Hash: too many algos (%ld); max %d",
140             (long)n, MAX_ALGOS);
141 47 100         for (i = 0; i < n; i++) {
142 34           SV **slot = av_fetch(av, i, 0);
143 34 50         if (!slot || !*slot)
    50          
144 0           croak("File::Raw::Hash: undef entry in 'algos' at index %ld",
145             (long)i);
146 34           opts->ids[i] = parse_algo_sv(aTHX_ *slot);
147             }
148 13           opts->n_ids = (int)n;
149 13           opts->multi = 1;
150 100 50         } else if (algo_sv && SvOK(algo_sv)) {
    50          
151 100           opts->ids[0] = parse_algo_sv(aTHX_ algo_sv);
152 99           opts->n_ids = 1;
153 99           opts->multi = 0;
154             } else {
155             /* Default: single sha256. */
156 0           opts->ids[0] = HA_SHA256;
157 0           opts->n_ids = 1;
158 0           opts->multi = 0;
159             }
160              
161             /* Resolve format. */
162 120 100         if (fmt_sv && SvOK(fmt_sv)) {
    50          
163             STRLEN flen;
164             const char *fp;
165 9 50         if (SvROK(fmt_sv))
166 0           croak("File::Raw::Hash: 'format' must be a string");
167 9           fp = SvPV(fmt_sv, flen);
168 9 100         if (hash_format_parse(fp, flen, &opts->format) != 0)
169 1           croak("File::Raw::Hash: unknown format '%.*s' "
170             "(known: hex, HEX, base64, base64url, raw)",
171             (int)flen, fp);
172             } else {
173 103           opts->format = HF_HEX;
174             }
175              
176             /* Resolve xxh64_seed. */
177 111 100         if (seed_sv && SvOK(seed_sv)) {
    50          
178 7 100         if (SvROK(seed_sv))
179 1           croak("File::Raw::Hash: 'xxh64_seed' must be an integer");
180 6           opts->xxh64_seed = (uint64_t)SvUV(seed_sv);
181 6           opts->xxh64_seed_set = 1;
182             }
183              
184             /* HMAC key validation. The key itself can be any byte string
185             * including binary / empty. Only the value's *type* is checked
186             * here; per-algo HMAC-able-ness is checked when set_hmac runs. */
187 120 100         if (opts->hmac_key_sv && SvOK(opts->hmac_key_sv)) {
    50          
188             int j;
189 16 100         if (SvROK(opts->hmac_key_sv))
190 1           croak("File::Raw::Hash: 'hmac_key' must be a byte string, "
191             "not a reference");
192 28 100         for (j = 0; j < opts->n_ids; j++) {
193 18           const hash_algo_info_t *info = hash_algo_by_id(opts->ids[j]);
194 18 100         if (!info->hmac_able)
195 5           croak("File::Raw::Hash: HMAC is not defined for algo "
196             "'%s' (HMAC-able: sha256, sha512, sha1, md5)",
197             info->name);
198             }
199             } else {
200 94           opts->hmac_key_sv = NULL;
201             }
202              
203             /* Validate `into`. Required; shape depends on phase. */
204 104 100         if (!opts->into || !SvOK(opts->into))
    100          
205 2           croak("File::Raw::Hash: 'into' is required");
206 102 100         if (!SvROK(opts->into))
207 1           croak("File::Raw::Hash: 'into' must be a reference");
208              
209 101 100         if (phase == PHASE_RECORD) {
210 10 100         if (SvTYPE(SvRV(opts->into)) != SVt_PVAV)
211 2           croak("File::Raw::Hash: in record phase, 'into' must be an "
212             "ARRAY ref (one entry pushed per record)");
213 8           return;
214             }
215              
216 91 100         if (opts->multi) {
217 9 100         if (SvTYPE(SvRV(opts->into)) != SVt_PVHV)
218 1           croak("File::Raw::Hash: 'into' must be a hash ref when "
219             "'algos' is used");
220             } else {
221 82           SV *referent = SvRV(opts->into);
222 82           svtype t = SvTYPE(referent);
223 82 50         if (t == SVt_PVAV || t == SVt_PVHV || t == SVt_PVCV
    100          
    50          
224 81 50         || t == SVt_PVGV || t == SVt_PVFM || t == SVt_PVIO)
    50          
    50          
225 1           croak("File::Raw::Hash: 'into' must be a SCALAR ref for "
226             "single-algo (got %s ref)", sv_reftype(referent, 0));
227             }
228             }
229              
230             /* Helper: build, run and finalise a runner over the given bytes,
231             * applying HMAC if a key is present. Returns 0 on success, croaks on
232             * setup error. results[*] is owned by the runner and lives until
233             * hash_runner_free. */
234             static void
235 90           run_full(pTHX_ const hash_opts_t *opts,
236             const char *data, size_t len,
237             hash_runner_t *runner, const hash_result_t **out_results)
238             {
239 90 50         if (hash_runner_init(runner, opts->ids, opts->n_ids, opts->format,
240 90           opts->xxh64_seed) != 0)
241 0           croak("File::Raw::Hash: out of memory initialising runner");
242              
243 90 100         if (opts->hmac_key_sv) {
244             STRLEN klen;
245             const unsigned char *kp =
246 9           (const unsigned char *)SvPV(opts->hmac_key_sv, klen);
247 9 50         if (hash_runner_set_hmac(runner, kp, (size_t)klen) != 0) {
248 0           hash_runner_free(runner);
249             /* Only reachable if a non-HMAC-able algo slipped past
250             * decode_opts; defensive. */
251 0           croak("File::Raw::Hash: HMAC mode rejected for the requested "
252             "algorithm set");
253             }
254             }
255              
256 90 50         if (data && len) hash_runner_update(runner, data, len);
    100          
257              
258 90 50         if (hash_runner_finish(runner, out_results) != 0) {
259 0           hash_runner_free(runner);
260 0           croak("File::Raw::Hash: out of memory finalising runner");
261             }
262 90           }
263              
264             /* Write digest results into the user's `into` target (READ/WRITE/STREAM
265             * shape). For RECORD phase use append_record_results. */
266             static void
267 89           emit_results(pTHX_ const hash_opts_t *opts, const hash_result_t *results)
268             {
269             int i;
270 89 100         if (opts->multi) {
271 8           HV *h = (HV *)SvRV(opts->into);
272 30 100         for (i = 0; i < opts->n_ids; i++) {
273 22           const hash_result_t *r = &results[i];
274 22           SV *val = newSVpvn(r->out, r->out_len);
275 22 50         if (opts->format != HF_RAW) SvUTF8_off(val);
276 22           (void)hv_store(h, r->name, (I32)strlen(r->name), val, 0);
277             }
278             } else {
279 81           SV *target = SvRV(opts->into);
280 81           const hash_result_t *r = &results[0];
281 81           sv_setpvn(target, r->out, r->out_len);
282 81 100         if (opts->format != HF_RAW) SvUTF8_off(target);
283             }
284 89           }
285              
286             /* RECORD-phase emission: push one element into the user's arrayref.
287             * Element shape mirrors the READ/WRITE convention:
288             * single algo -> a scalar (the digest)
289             * multi algos -> a hashref (algo => digest, ...)
290             */
291             static void
292 8           append_record_results(pTHX_ const hash_opts_t *opts,
293             const hash_result_t *results)
294             {
295 8           AV *av = (AV *)SvRV(opts->into);
296             int i;
297 8 100         if (opts->multi) {
298 2           HV *h = newHV();
299 8 100         for (i = 0; i < opts->n_ids; i++) {
300 6           const hash_result_t *r = &results[i];
301 6           SV *val = newSVpvn(r->out, r->out_len);
302 6 50         if (opts->format != HF_RAW) SvUTF8_off(val);
303 6           (void)hv_store(h, r->name, (I32)strlen(r->name), val, 0);
304             }
305 2           av_push(av, newRV_noinc((SV *)h));
306             } else {
307 6           const hash_result_t *r = &results[0];
308 6           SV *val = newSVpvn(r->out, r->out_len);
309 6 100         if (opts->format != HF_RAW) SvUTF8_off(val);
310 6           av_push(av, val);
311             }
312 8           }
313              
314             /* ============================================================
315             * READ / WRITE callbacks (passthrough + side-channel digest).
316             * ============================================================ */
317              
318             static SV *
319 100           hash_one_shot(pTHX_ FilePluginContext *ctx)
320             {
321             hash_opts_t opts;
322             hash_runner_t runner;
323 100           const hash_result_t *results = NULL;
324 100           STRLEN dlen = 0;
325 100           const char *dp = NULL;
326              
327 100           memset(&opts, 0, sizeof opts);
328 100           memset(&runner, 0, sizeof runner);
329              
330 100           decode_opts(aTHX_ ctx->options, &opts, PHASE_ONE_SHOT);
331              
332 82 50         if (ctx->data && SvOK(ctx->data)) dp = SvPV(ctx->data, dlen);
    50          
333              
334 82           run_full(aTHX_ &opts, dp, (size_t)dlen, &runner, &results);
335 82           emit_results(aTHX_ &opts, results);
336 82           hash_runner_free(&runner);
337              
338             /* Passthrough. */
339 82 50         if (!ctx->data) return newSVpvn("", 0);
340 82           return SvREFCNT_inc_simple_NN(ctx->data);
341             }
342              
343             static SV *
344 94           hash_read_cb(pTHX_ FilePluginContext *ctx)
345             {
346 94           return hash_one_shot(aTHX_ ctx);
347             }
348              
349             static SV *
350 6           hash_write_cb(pTHX_ FilePluginContext *ctx)
351             {
352 6           return hash_one_shot(aTHX_ ctx);
353             }
354              
355             /* ============================================================
356             * RECORD callback (one digest per record, pushed into arrayref).
357             * ============================================================ */
358              
359             static SV *
360 10           hash_record_cb(pTHX_ FilePluginContext *ctx, SV *record)
361             {
362             hash_opts_t opts;
363             hash_runner_t runner;
364 10           const hash_result_t *results = NULL;
365 10           STRLEN dlen = 0;
366 10           const char *dp = NULL;
367              
368 10           memset(&opts, 0, sizeof opts);
369 10           memset(&runner, 0, sizeof runner);
370              
371 10           decode_opts(aTHX_ ctx->options, &opts, PHASE_RECORD);
372              
373 8 50         if (record && SvOK(record)) dp = SvPV(record, dlen);
    50          
374              
375 8           run_full(aTHX_ &opts, dp, (size_t)dlen, &runner, &results);
376 8           append_record_results(aTHX_ &opts, results);
377 8           hash_runner_free(&runner);
378              
379             /* Passthrough the record so downstream filters / map_lines see it
380             * unchanged. The dispatcher mortalises on its way out. */
381 8 50         if (!record) return &PL_sv_undef;
382 8           return SvREFCNT_inc_simple_NN(record);
383             }
384              
385             /* ============================================================
386             * STREAM callback.
387             * ============================================================ */
388              
389             typedef struct {
390             hash_runner_t runner;
391             hash_opts_t opts;
392             SV *into_ref; /* +1 refcount */
393             } hash_stream_state_t;
394              
395             static int
396 102           hash_stream_cb(pTHX_ FilePluginContext *ctx,
397             const char *chunk, size_t len, int eof)
398             {
399 102           hash_stream_state_t *st = (hash_stream_state_t *)ctx->call_state;
400              
401 102 100         if (!st) {
402 7           st = (hash_stream_state_t *)calloc(1, sizeof *st);
403 7 50         if (!st) {
404 0           warn("File::Raw::Hash: stream alloc failed");
405 0           ctx->cancel = 1;
406 0           return 1;
407             }
408 7           decode_opts(aTHX_ ctx->options, &st->opts, PHASE_ONE_SHOT);
409 7 50         if (hash_runner_init(&st->runner, st->opts.ids, st->opts.n_ids,
410             st->opts.format, st->opts.xxh64_seed) != 0) {
411 0           free(st);
412 0           warn("File::Raw::Hash: stream runner init failed");
413 0           ctx->cancel = 1;
414 0           return 1;
415             }
416 7 100         if (st->opts.hmac_key_sv) {
417             STRLEN klen;
418             const unsigned char *kp =
419 1           (const unsigned char *)SvPV(st->opts.hmac_key_sv, klen);
420 1 50         if (hash_runner_set_hmac(&st->runner, kp, (size_t)klen) != 0) {
421 0           hash_runner_free(&st->runner);
422 0           free(st);
423 0           warn("File::Raw::Hash: stream HMAC setup failed");
424 0           ctx->cancel = 1;
425 0           return 1;
426             }
427             }
428 7           st->into_ref = SvREFCNT_inc_simple_NN(st->opts.into);
429 7           ctx->call_state = st;
430             }
431              
432 102 100         if (chunk && len) {
    50          
433 95           hash_runner_update(&st->runner, chunk, len);
434             }
435              
436 102 100         if (eof) {
437 7           const hash_result_t *results = NULL;
438 7 50         if (hash_runner_finish(&st->runner, &results) != 0) {
439 0           hash_runner_free(&st->runner);
440 0           SvREFCNT_dec(st->into_ref);
441 0           free(st);
442 0           ctx->call_state = NULL;
443 0           warn("File::Raw::Hash: stream finish failed");
444 0           ctx->cancel = 1;
445 0           return 1;
446             }
447 7           emit_results(aTHX_ &st->opts, results);
448 7           hash_runner_free(&st->runner);
449 7           SvREFCNT_dec(st->into_ref);
450 7           free(st);
451 7           ctx->call_state = NULL;
452             }
453              
454 102           return 0; /* continue */
455             }
456              
457             /* ============================================================ */
458              
459             static FilePlugin hash_plugin;
460              
461             MODULE = File::Raw::Hash PACKAGE = File::Raw::Hash
462              
463             PROTOTYPES: DISABLE
464              
465             BOOT:
466 14           memset(&hash_plugin, 0, sizeof hash_plugin);
467 14           hash_plugin.name = "hash";
468 14           hash_plugin.read_fn = hash_read_cb;
469 14           hash_plugin.write_fn = hash_write_cb;
470 14           hash_plugin.record_fn = hash_record_cb;
471 14           hash_plugin.stream_fn = hash_stream_cb;
472 14           file_register_plugin(aTHX_ &hash_plugin);
473              
474             # ============================================================
475             # Test helper: invoke the hash plugin's record_fn through File::Raw's
476             # dispatch_record entry point. Public name has a leading underscore to
477             # signal "not part of the supported API" - it exists so the test suite
478             # can exercise RECORD phase end-to-end before File::Raw exposes a
479             # user-facing per-record iterator.
480             # ============================================================
481              
482             SV*
483             _test_record_one(record_sv, ...)
484             SV *record_sv
485             PREINIT:
486             HV *opts;
487             SV *result;
488             int i;
489             CODE:
490 10 50         if ((items - 1) % 2 != 0)
491 0           croak("File::Raw::Hash::_test_record_one: odd number of "
492             "key/value option args");
493 10           opts = newHV();
494             /* Default plugin to "hash" so the caller can omit it. */
495 10           (void)hv_stores(opts, "plugin", newSVpvs("hash"));
496 32 100         for (i = 1; i < items; i += 2) {
497             STRLEN klen;
498 22           const char *kp = SvPV(ST(i), klen);
499 22           SV *vp = SvREFCNT_inc(ST(i + 1));
500 22           (void)hv_store(opts, kp, (I32)klen, vp, 0);
501             }
502 10           result = file_plugin_dispatch_record(aTHX_ opts, NULL, record_sv);
503 8           SvREFCNT_dec((SV *)opts);
504 8 50         if (!result) {
505 0           RETVAL = &PL_sv_undef;
506 0           SvREFCNT_inc(RETVAL);
507             } else {
508 8           RETVAL = result;
509             }
510             OUTPUT:
511             RETVAL