File Coverage

file.c
Criterion Covered Total %
statement 2495 3242 76.9
branch 1065 1866 57.0
condition n/a
subroutine n/a
pod n/a
total 3560 5108 69.6


line stmt bran cond sub pod time code
1             /*
2             * file.c - Fast IO operations using direct system calls
3             *
4             * Features:
5             * - slurp/spew with minimal overhead
6             * - Memory-mapped file access (mmap)
7             * - Efficient line iteration
8             * - Direct stat access
9             * - Windows and POSIX support
10             */
11              
12             #define PERL_NO_GET_CONTEXT
13             #include "EXTERN.h"
14             #include "perl.h"
15             #include "XSUB.h"
16             #include "include/file_compat.h"
17             #include "include/file_plugin.h"
18              
19             #include
20             #include
21             #include
22             #include
23              
24             #ifdef _WIN32
25             #include
26             #include
27             #include
28             /*
29             * Windows compatibility - use Perl's wrapper functions
30             * We DON'T redefine open/read/write/close/stat/fstat/access here
31             * because Perl's XSUB.h already defines them to work correctly.
32             * Just define the flags and other missing bits.
33             */
34             #define O_RDONLY _O_RDONLY
35             #define O_WRONLY _O_WRONLY
36             #define O_RDWR _O_RDWR
37             #define O_CREAT _O_CREAT
38             #define O_TRUNC _O_TRUNC
39             #define O_APPEND _O_APPEND
40             #define O_BINARY _O_BINARY
41             #ifndef S_ISREG
42             #define S_ISREG(m) (((m) & _S_IFMT) == _S_IFREG)
43             #endif
44             #ifndef S_ISDIR
45             #define S_ISDIR(m) (((m) & _S_IFMT) == _S_IFDIR)
46             #endif
47             #define R_OK 4
48             #define W_OK 2
49             /* ssize_t for Windows */
50             #ifndef ssize_t
51             #ifdef _WIN64
52             typedef __int64 ssize_t;
53             #else
54             typedef int ssize_t;
55             #endif
56             #endif
57             /* Windows doesn't have real uid/gid - use dummy values */
58             #define FILE_FAKE_UID 1000
59             #define FILE_FAKE_GID 1000
60             /*
61             * On Windows with PERL_IMPLICIT_SYS, Perl redefines open() to
62             * PerlLIO_open() which only accepts 2 args. Use _open() directly
63             * for the 3-arg form (path, flags, mode).
64             */
65             #define file_open3(path, flags, mode) _open(path, flags, mode)
66             #else
67             #define file_open3(path, flags, mode) open(path, flags, mode)
68             #include
69             #include
70             #include /* For utime - more portable than utimes */
71             #include /* For readdir */
72             #if defined(__linux__)
73             #include /* Zero-copy file transfer */
74             #endif
75             #if defined(__APPLE__)
76             #include /* macOS native file copy */
77             #endif
78             #endif
79              
80             /* Default buffer size for reads - 64KB is optimal for most systems */
81             #define FILE_BUFFER_SIZE 65536
82              
83             /* Larger buffer for bulk operations */
84             #define FILE_BULK_BUFFER_SIZE 262144
85              
86             /* Threshold for mmap-based slurp (4MB) */
87             #define MMAP_SLURP_THRESHOLD (4 * 1024 * 1024)
88              
89             /* Branch prediction hints */
90             #ifndef LIKELY
91             #if defined(__GNUC__) || defined(__clang__)
92             #define LIKELY(x) __builtin_expect(!!(x), 1)
93             #define UNLIKELY(x) __builtin_expect(!!(x), 0)
94             #else
95             #define LIKELY(x) (x)
96             #define UNLIKELY(x) (x)
97             #endif
98             #endif
99              
100             /* posix_fadvise hints for kernel optimization */
101             #if defined(__linux__) || defined(__FreeBSD__) || defined(__NetBSD__)
102             #define HAVE_POSIX_FADVISE 1
103             #define advise_sequential(fd, len) posix_fadvise(fd, 0, len, POSIX_FADV_SEQUENTIAL)
104             #define advise_dontneed(fd, len) posix_fadvise(fd, 0, len, POSIX_FADV_DONTNEED)
105             #else
106             #define HAVE_POSIX_FADVISE 0
107             #define advise_sequential(fd, len) ((void)0)
108             #define advise_dontneed(fd, len) ((void)0)
109             #endif
110              
111             /* ============================================
112             Plugin registry + dispatch helpers.
113             Storage and definitions live here in the Raw.so TU; downstream XS
114             modules see only file_plugin.h and resolve the function symbols at
115             load time via RTLD_GLOBAL.
116             ============================================ */
117              
118             /* Registry: name (PV) -> FilePlugin* (stored via PTR2IV in the SV). */
119             static HV *g_file_plugin_registry = NULL;
120              
121             /* One-pointer cache: short-circuits HV lookup when the same plugin is
122             * used in tight loops (e.g. each_line). */
123             static const FilePlugin *g_file_last_plugin = NULL;
124              
125 56           static void file_plugin_registry_init(pTHX) {
126 56 100         if (!g_file_plugin_registry)
127 28           g_file_plugin_registry = newHV();
128 56           }
129              
130 56           int file_register_plugin(pTHX_ const FilePlugin *plugin) {
131             SV *entry;
132             STRLEN name_len;
133              
134 56 50         if (!plugin || !plugin->name || !*plugin->name) return -1;
    50          
    50          
135              
136 56           file_plugin_registry_init(aTHX);
137 56           name_len = strlen(plugin->name);
138 56 100         if (hv_exists(g_file_plugin_registry, plugin->name, name_len))
139 1           return 0;
140              
141 55           entry = newSViv(PTR2IV(plugin));
142 55 50         if (!hv_store(g_file_plugin_registry, plugin->name, name_len, entry, 0)) {
143 0           SvREFCNT_dec(entry);
144 0           return -1;
145             }
146 55           return 1;
147             }
148              
149 12           int file_unregister_plugin(pTHX_ const char *name) {
150 12 50         if (!g_file_plugin_registry || !name) return 0;
    50          
151 12 100         if (g_file_last_plugin && strcmp(g_file_last_plugin->name, name) == 0)
    50          
152 7           g_file_last_plugin = NULL;
153 12           return hv_delete(g_file_plugin_registry, name, strlen(name), G_DISCARD)
154 12           ? 1 : 0;
155             }
156              
157 66           const FilePlugin *file_lookup_plugin(pTHX_ const char *name) {
158             SV **svp;
159             const FilePlugin *p;
160              
161 66 50         if (!name) return NULL;
162 66 100         if (g_file_last_plugin && strcmp(g_file_last_plugin->name, name) == 0)
    100          
163 21           return g_file_last_plugin;
164 45 50         if (!g_file_plugin_registry) return NULL;
165              
166 45           svp = hv_fetch(g_file_plugin_registry, name, strlen(name), 0);
167 45 100         if (!svp || !*svp) return NULL;
    50          
168 40           p = INT2PTR(const FilePlugin*, SvIV(*svp));
169 40           g_file_last_plugin = p;
170 40           return p;
171             }
172              
173             /* ---- option-HV builder ---- */
174              
175 60           HV* file_plugin_build_opts(pTHX_ SV **stack, int start, int items,
176             const char *fn_name) {
177             HV *opts;
178             int i;
179 60           int has_plugin = 0;
180              
181 60 50         if (start >= items) return NULL;
182 60 100         if ((items - start) % 2 != 0)
183 2           croak("File::Raw::%s: odd number of options (expected key => value pairs)",
184             fn_name);
185              
186 58           opts = newHV();
187 131 100         for (i = start; i < items; i += 2) {
188 73           SV *key_sv = stack[i];
189 73           SV *val_sv = stack[i + 1];
190             STRLEN key_len;
191             const char *key;
192              
193 73 50         if (!SvOK(key_sv))
194 0           croak("File::Raw::%s: option key at position %d is undef",
195             fn_name, i);
196 73           key = SvPV(key_sv, key_len);
197 73 50         if (!hv_store(opts, key, (I32)key_len, SvREFCNT_inc(val_sv), 0)) {
198 0           SvREFCNT_dec(val_sv);
199 0           SvREFCNT_dec((SV*)opts);
200 0           croak("File::Raw::%s: failed to store option '%s'", fn_name, key);
201             }
202 73 100         if (key_len == 6 && memcmp(key, "plugin", 6) == 0)
    50          
203 55           has_plugin = 1;
204             }
205              
206 58 100         if (!has_plugin) {
207 3           SvREFCNT_dec((SV*)opts);
208 3           croak("File::Raw::%s: options passed without 'plugin' key", fn_name);
209             }
210 55           return opts;
211             }
212              
213             /* ---- dispatch helpers ---- */
214              
215             /* FilePluginChain — internal-only resolution result for a single dispatch
216             * call.
217             *
218             * Two shapes:
219             *
220             * Single-plugin (fast path, count == 1, shared == NULL):
221             * plugins[0] is the resolved plugin; opts is passed to it directly
222             * (no per-plugin slicing). This is the only path used when the
223             * caller passed `plugin => 'name'` as a scalar string — preserves
224             * today's behaviour byte-for-byte.
225             *
226             * Chain (count >= 1, shared != NULL):
227             * `plugin => [a, b, c]` (arrayref). `shared` holds top-level keys
228             * that did not match any plugin name's per-plugin sub-hash;
229             * `per_plugin[i]` (may be NULL) holds the sub-hashref the user
230             * gave for plugins[i]. `file_plugin_chain_iter_opts` builds a
231             * fresh per-iteration HV from these. */
232             typedef struct {
233             const FilePlugin **plugins; /* count slots */
234             int count;
235             HV *shared; /* NULL on single-plugin fast path */
236             HV **per_plugin; /* count slots, each may be NULL */
237             } FilePluginChain;
238              
239             static void
240 51           file_plugin_chain_init(FilePluginChain *chain) {
241 51           memset(chain, 0, sizeof *chain);
242 51           }
243              
244             static void
245 46           file_plugin_chain_free(pTHX_ FilePluginChain *chain) {
246             int i;
247 46 100         if (chain->per_plugin) {
248 44 100         for (i = 0; i < chain->count; i++) {
249 28 100         if (chain->per_plugin[i])
250 3           SvREFCNT_dec((SV *)chain->per_plugin[i]);
251             }
252 16           Safefree(chain->per_plugin);
253 16           chain->per_plugin = NULL;
254             }
255 46 100         if (chain->shared) {
256 16           SvREFCNT_dec((SV *)chain->shared);
257 16           chain->shared = NULL;
258             }
259 46 50         if (chain->plugins) {
260 46           Safefree(chain->plugins);
261 46           chain->plugins = NULL;
262             }
263 46           chain->count = 0;
264 46           }
265              
266             /* Resolve plugin chain. Accepts scalar (single-plugin fast path) or
267             * arrayref (chain). Croaks on undef, empty arrayref, unknown plugin
268             * name, or wrong-shape value. Caller must call file_plugin_chain_free
269             * on `out` before returning. */
270             static void
271 51           file_plugin_resolve_chain(pTHX_ HV *opts, const char *fn_name,
272             FilePluginChain *out)
273             {
274             SV **slot;
275             SV *plugin_sv;
276             AV *plugins_av;
277             SSize_t n;
278             SSize_t i;
279              
280 51           file_plugin_chain_init(out);
281              
282 51           slot = hv_fetchs(opts, "plugin", 0);
283 51 50         if (!slot || !*slot || !SvOK(*slot))
    50          
    50          
284 0           croak("File::Raw::%s: missing 'plugin' option", fn_name);
285 51           plugin_sv = *slot;
286              
287             /* Scalar fast path — single-plugin call, opts passed straight through. */
288 51 100         if (!SvROK(plugin_sv)) {
289 34           const char *name = SvPV_nolen(plugin_sv);
290 34           const FilePlugin *p = file_lookup_plugin(aTHX_ name);
291 34 100         if (!p) croak("File::Raw::%s: unknown plugin '%s'", fn_name, name);
292 30           Newx(out->plugins, 1, const FilePlugin *);
293 30           out->plugins[0] = p;
294 30           out->count = 1;
295 30           return;
296             }
297              
298 17 50         if (SvTYPE(SvRV(plugin_sv)) != SVt_PVAV)
299 0           croak("File::Raw::%s: 'plugin' must be a string or arrayref of "
300             "plugin names", fn_name);
301              
302 17           plugins_av = (AV *)SvRV(plugin_sv);
303 17           n = av_len(plugins_av) + 1;
304 17 50         if (n <= 0)
305 0           croak("File::Raw::%s: empty plugin chain", fn_name);
306              
307 17 50         Newx(out->plugins, n, const FilePlugin *);
308 17           out->count = (int)n;
309              
310 47 100         for (i = 0; i < n; i++) {
311 30           SV **np = av_fetch(plugins_av, i, 0);
312             const char *name;
313             const FilePlugin *p;
314 30 50         if (!np || !*np || !SvOK(*np))
    50          
    50          
315 0           croak("File::Raw::%s: undef plugin name at chain index %ld",
316             fn_name, (long)i);
317 30 50         if (SvROK(*np))
318 0           croak("File::Raw::%s: plugin name at chain index %ld must "
319             "be a string", fn_name, (long)i);
320 30           name = SvPV_nolen(*np);
321 30           p = file_lookup_plugin(aTHX_ name);
322 30 50         if (!p)
323 0           croak("File::Raw::%s: unknown plugin '%s' (chain index %ld)",
324             fn_name, name, (long)i);
325 30           out->plugins[i] = p;
326             }
327              
328             /* Build shared HV + per-plugin slots. Walk every key in opts:
329             * - 'plugin' → skip (already consumed)
330             * - matches a plugin name AND is a hashref → per-plugin sub-hash
331             * - otherwise → shared bag, visible to every iteration */
332 17           out->shared = newHV();
333 17 50         Newxz(out->per_plugin, n, HV *);
334              
335             {
336             HE *he;
337 17           hv_iterinit(opts);
338 39 100         while ((he = hv_iternext(opts))) {
339             I32 klen_i;
340 22           const char *key = hv_iterkey(he, &klen_i);
341 22           STRLEN klen = (STRLEN)klen_i;
342 22           SV *val = hv_iterval(opts, he);
343 22           int matched = -1;
344              
345 22 100         if (klen == 6 && memcmp(key, "plugin", 6) == 0) continue;
    50          
346              
347 5 100         if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVHV) {
    50          
348             int j;
349 4 50         for (j = 0; j < (int)n; j++) {
350 4           const char *pn = out->plugins[j]->name;
351 4           STRLEN plen = strlen(pn);
352 4 100         if (plen == klen && memcmp(key, pn, klen) == 0) {
    50          
353 3           matched = j;
354 3           break;
355             }
356             }
357             }
358              
359 5 100         if (matched >= 0) {
360 3           out->per_plugin[matched] = (HV *)SvRV(val);
361 3           SvREFCNT_inc((SV *)out->per_plugin[matched]);
362             } else {
363 2           (void)hv_store(out->shared, key, klen,
364             SvREFCNT_inc(val), 0);
365             }
366             }
367             }
368             }
369              
370             /* Resolve for phases that don't support chaining (RECORD, STREAM).
371             * Croaks if the user passed an arrayref. */
372             static const FilePlugin *
373 4           file_plugin_resolve_single(pTHX_ HV *opts, const char *fn_name)
374             {
375 4           SV **slot = hv_fetchs(opts, "plugin", 0);
376             const char *name;
377             const FilePlugin *p;
378 4 50         if (!slot || !*slot || !SvOK(*slot))
    50          
    50          
379 0           croak("File::Raw::%s: missing 'plugin' option", fn_name);
380 4 100         if (SvROK(*slot))
381 2           croak("File::Raw::%s: plugin chains are not supported for the "
382             "'%s' phase (record/stream); pass a single plugin name "
383             "instead", fn_name, fn_name);
384 2           name = SvPV_nolen(*slot);
385 2           p = file_lookup_plugin(aTHX_ name);
386 2 100         if (!p) croak("File::Raw::%s: unknown plugin '%s'", fn_name, name);
387 1           return p;
388             }
389              
390             /* Build the per-iteration ctx->options HV for chain mode: shared bag
391             * with the indexed plugin's sub-hash overlaid on top (sub-hash wins on
392             * conflict). The 'plugin' key is set to the iterating plugin's own
393             * name, so plugins that read it (e.g. Separated's known_opt list)
394             * don't see something stale or array-shaped. Returns a fresh HV the
395             * dispatcher must SvREFCNT_dec after the iteration. */
396             static HV *
397 28           file_plugin_chain_iter_opts(pTHX_ FilePluginChain *chain, int idx)
398             {
399 28           HV *iter = newHV();
400             HE *he;
401              
402 28 50         if (chain->shared) {
403 28           hv_iterinit(chain->shared);
404 32 100         while ((he = hv_iternext(chain->shared))) {
405             I32 klen_i;
406 4           const char *key = hv_iterkey(he, &klen_i);
407 4           SV *val = hv_iterval(chain->shared, he);
408 4           (void)hv_store(iter, key, klen_i, SvREFCNT_inc(val), 0);
409             }
410             }
411 28 50         if (chain->per_plugin && chain->per_plugin[idx]) {
    100          
412 3           HV *pp = chain->per_plugin[idx];
413 3           hv_iterinit(pp);
414 7 100         while ((he = hv_iternext(pp))) {
415             I32 klen_i;
416 4           const char *key = hv_iterkey(he, &klen_i);
417 4           SV *val = hv_iterval(pp, he);
418 4           (void)hv_store(iter, key, klen_i, SvREFCNT_inc(val), 0);
419             }
420             }
421 28           (void)hv_store(iter, "plugin", 6,
422             newSVpv(chain->plugins[idx]->name, 0), 0);
423 28           return iter;
424             }
425              
426             /* READ dispatcher.
427             *
428             * Single-plugin scalar path: identical to today — opts goes straight
429             * through, plugin's return SV is returned bare, caller manages
430             * `if (out != bytes)` decref.
431             *
432             * Chain path: walks the resolved plugin list left-to-right, threading
433             * each plugin's return SV into the next call's ctx->data. Refcount
434             * discipline tracks "do we own the current SV?" so that whether the
435             * chain returns the input bytes unchanged or a fresh SV, the contract
436             * the caller sees is identical to the single-plugin path. */
437 36           SV* file_plugin_dispatch_read(pTHX_ HV *opts, const char *path, SV *bytes) {
438             FilePluginChain chain;
439             SV *current;
440             int we_own;
441             int i;
442              
443 36           file_plugin_resolve_chain(aTHX_ opts, "slurp", &chain);
444              
445             /* Scalar fast path: zero allocation overhead vs. today. */
446 33 100         if (chain.shared == NULL) {
447 22           const FilePlugin *p = chain.plugins[0];
448             FilePluginContext ctx;
449             SV *out;
450 22 50         if (!p->read_fn) {
451 0           file_plugin_chain_free(aTHX_ &chain);
452 0           croak("File::Raw: plugin '%s' has no read phase", p->name);
453             }
454 22           ctx.path = path;
455 22           ctx.data = bytes;
456 22           ctx.callback = NULL;
457 22           ctx.options = opts;
458 22           ctx.phase = FILE_PLUGIN_PHASE_READ;
459 22           ctx.cancel = 0;
460 22           ctx.plugin_state = p->state;
461 22           ctx.call_state = NULL;
462 22           out = p->read_fn(aTHX_ &ctx);
463 22           file_plugin_chain_free(aTHX_ &chain);
464 22 100         if (ctx.cancel) return NULL;
465 21           return out;
466             }
467              
468             /* Chain path. */
469 11           current = bytes;
470 11           we_own = 0;
471              
472 29 100         for (i = 0; i < chain.count; i++) {
473 20           const FilePlugin *p = chain.plugins[i];
474             FilePluginContext ctx;
475             HV *iter_opts;
476             SV *next;
477              
478 20 50         if (!p->read_fn) {
479 0 0         if (we_own) SvREFCNT_dec(current);
480 0           file_plugin_chain_free(aTHX_ &chain);
481 0           croak("File::Raw: plugin '%s' has no read phase "
482             "(chain index %d)", p->name, i);
483             }
484              
485 20           iter_opts = file_plugin_chain_iter_opts(aTHX_ &chain, i);
486 20           ctx.path = path;
487 20           ctx.data = current;
488 20           ctx.callback = NULL;
489 20           ctx.options = iter_opts;
490 20           ctx.phase = FILE_PLUGIN_PHASE_READ;
491 20           ctx.cancel = 0;
492 20           ctx.plugin_state = p->state;
493 20           ctx.call_state = NULL;
494              
495 20           next = p->read_fn(aTHX_ &ctx);
496 19           SvREFCNT_dec((SV *)iter_opts);
497              
498 19 100         if (ctx.cancel || !next) {
    50          
499 1 50         if (we_own) SvREFCNT_dec(current);
500 1           file_plugin_chain_free(aTHX_ &chain);
501 1           return NULL;
502             }
503              
504 18 50         if (next != current) {
505 18 100         if (we_own) SvREFCNT_dec(current);
506 18           current = next;
507 18           we_own = 1; /* per existing convention, plugin gives us +1 */
508             }
509             /* else: plugin returned the same SV; ownership unchanged. */
510             }
511              
512 9           file_plugin_chain_free(aTHX_ &chain);
513             /* Contract matches today's: if we never replaced bytes, we return
514             * bytes (caller still owns the +1 we never touched). If we replaced
515             * it, we return the fresh SV with +1 from the last plugin, exactly
516             * as today's single-plugin path would. */
517 9           return current;
518             }
519              
520             /* WRITE dispatcher. Mirror image of READ — same chain mechanics, but
521             * iterates RIGHT TO LEFT. The user's payload (which can be structured —
522             * AoA, AoH, etc.) flows into the *last* plugin first; that plugin emits
523             * bytes; subsequent (earlier-listed) plugins wrap those bytes. The byte
524             * stream produced by the FIRST plugin is what gets written to disk.
525             *
526             * Mnemonic: same array spelling for read and write — the array describes
527             * the encoding stack from outermost wrapper to innermost format. */
528 15           SV* file_plugin_dispatch_write(pTHX_ HV *opts, const char *path, SV *payload) {
529             FilePluginChain chain;
530             SV *current;
531             int we_own;
532             int i;
533              
534 15           file_plugin_resolve_chain(aTHX_ opts, "spew", &chain);
535              
536 14 100         if (chain.shared == NULL) {
537 8           const FilePlugin *p = chain.plugins[0];
538             FilePluginContext ctx;
539             SV *out;
540 8 100         if (!p->write_fn) {
541 1           file_plugin_chain_free(aTHX_ &chain);
542 1           croak("File::Raw: plugin '%s' has no write phase", p->name);
543             }
544 7           ctx.path = path;
545 7           ctx.data = payload;
546 7           ctx.callback = NULL;
547 7           ctx.options = opts;
548 7           ctx.phase = FILE_PLUGIN_PHASE_WRITE;
549 7           ctx.cancel = 0;
550 7           ctx.plugin_state = p->state;
551 7           ctx.call_state = NULL;
552 7           out = p->write_fn(aTHX_ &ctx);
553 7           file_plugin_chain_free(aTHX_ &chain);
554 7 100         if (ctx.cancel) return NULL;
555 6           return out;
556             }
557              
558 6           current = payload;
559 6           we_own = 0;
560              
561 13 100         for (i = chain.count - 1; i >= 0; i--) {
562 8           const FilePlugin *p = chain.plugins[i];
563             FilePluginContext ctx;
564             HV *iter_opts;
565             SV *next;
566              
567 8 50         if (!p->write_fn) {
568 0 0         if (we_own) SvREFCNT_dec(current);
569 0           file_plugin_chain_free(aTHX_ &chain);
570 0           croak("File::Raw: plugin '%s' has no write phase "
571             "(chain index %d)", p->name, i);
572             }
573              
574 8           iter_opts = file_plugin_chain_iter_opts(aTHX_ &chain, i);
575 8           ctx.path = path;
576 8           ctx.data = current;
577 8           ctx.callback = NULL;
578 8           ctx.options = iter_opts;
579 8           ctx.phase = FILE_PLUGIN_PHASE_WRITE;
580 8           ctx.cancel = 0;
581 8           ctx.plugin_state = p->state;
582 8           ctx.call_state = NULL;
583              
584 8           next = p->write_fn(aTHX_ &ctx);
585 8           SvREFCNT_dec((SV *)iter_opts);
586              
587 8 100         if (ctx.cancel || !next) {
    50          
588 1 50         if (we_own) SvREFCNT_dec(current);
589 1           file_plugin_chain_free(aTHX_ &chain);
590 1           return NULL;
591             }
592              
593 7 50         if (next != current) {
594 7 100         if (we_own) SvREFCNT_dec(current);
595 7           current = next;
596 7           we_own = 1;
597             }
598             }
599              
600 5           file_plugin_chain_free(aTHX_ &chain);
601 5           return current;
602             }
603              
604             /* RECORD dispatcher — single-plugin only. Chains are rejected because
605             * a "record" is one already-parsed unit; threading it through multiple
606             * record fns would require the records to remain the same shape across
607             * links, which collapses the abstraction. */
608 0           SV* file_plugin_dispatch_record(pTHX_ HV *opts, const char *path, SV *record) {
609 0           const FilePlugin *p = file_plugin_resolve_single(aTHX_ opts, "record");
610             FilePluginContext ctx;
611             SV *out;
612              
613 0 0         if (!p->record_fn)
614 0           croak("File::Raw: plugin '%s' has no record phase", p->name);
615              
616 0           ctx.path = path;
617 0           ctx.data = NULL;
618 0           ctx.callback = NULL;
619 0           ctx.options = opts;
620 0           ctx.phase = FILE_PLUGIN_PHASE_RECORD;
621 0           ctx.cancel = 0;
622 0           ctx.plugin_state = p->state;
623 0           ctx.call_state = NULL;
624              
625 0           out = p->record_fn(aTHX_ &ctx, record);
626 0 0         if (ctx.cancel) return NULL;
627 0           return out;
628             }
629              
630             /* file_plugin_dispatch_stream is defined below - it relies on
631             * FILE_BUFFER_SIZE and the platform open()/read() wrappers. */
632 4           SV* file_plugin_dispatch_stream(pTHX_ HV *opts, const char *path, SV *cb) {
633 4           const FilePlugin *p = file_plugin_resolve_single(aTHX_ opts, "each_line");
634             FilePluginContext ctx;
635             char buf[FILE_BUFFER_SIZE];
636             int fd;
637             ssize_t n;
638 1           int cancelled = 0;
639              
640 1 50         if (!p->stream_fn)
641 1           croak("File::Raw: plugin '%s' has no stream phase", p->name);
642              
643 0           fd = file_open3(path, O_RDONLY, 0);
644 0 0         if (fd < 0) return NULL;
645              
646 0           ctx.path = path;
647 0           ctx.data = NULL;
648 0           ctx.callback = cb;
649 0           ctx.options = opts;
650 0           ctx.phase = FILE_PLUGIN_PHASE_STREAM;
651 0           ctx.cancel = 0;
652 0           ctx.plugin_state = p->state;
653 0           ctx.call_state = NULL;
654              
655 0 0         while ((n = read(fd, buf, sizeof(buf))) > 0) {
656 0 0         if (p->stream_fn(aTHX_ &ctx, buf, (size_t)n, 0) || ctx.cancel) {
    0          
657 0           cancelled = 1;
658 0           break;
659             }
660             }
661 0 0         if (!cancelled) {
662             /* EOF flush so the plugin can emit any buffered final record. */
663 0           p->stream_fn(aTHX_ &ctx, NULL, 0, 1);
664             }
665 0           close(fd);
666 0 0         return (cancelled || ctx.cancel) ? NULL : &PL_sv_yes;
    0          
667             }
668              
669             /* ============================================
670             Stat cache - like Perl's _ special filehandle
671             ============================================ */
672             #define STAT_CACHE_PATH_MAX 1024
673              
674             static struct {
675             char path[STAT_CACHE_PATH_MAX];
676             Stat_t st;
677             int valid;
678             #ifdef _WIN32
679             int uid;
680             int gid;
681             #else
682             uid_t uid;
683             gid_t gid;
684             #endif
685             } g_stat_cache = { "", {0}, 0, 0, 0 };
686              
687             /* Get cached stat or perform new stat */
688 386           static int cached_stat(const char *path, Stat_t *st) {
689             dTHX;
690 386 100         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    100          
691 148           *st = g_stat_cache.st;
692 148           return 0;
693             }
694            
695 238 100         if (stat(path, st) < 0) {
696 32           g_stat_cache.valid = 0;
697 32           return -1;
698             }
699            
700             /* Cache the result */
701 206           size_t len = strlen(path);
702 206 50         if (len < STAT_CACHE_PATH_MAX) {
703 206           memcpy(g_stat_cache.path, path, len + 1);
704 206           g_stat_cache.st = *st;
705             #ifdef _WIN32
706             /* Windows doesn't have real uid/gid concepts */
707             g_stat_cache.uid = FILE_FAKE_UID;
708             g_stat_cache.gid = FILE_FAKE_GID;
709             #else
710 206           g_stat_cache.uid = geteuid();
711 206           g_stat_cache.gid = getegid();
712             #endif
713 206           g_stat_cache.valid = 1;
714             }
715            
716 206           return 0;
717             }
718              
719             /* Invalidate cache (call after write operations) */
720 1           static void invalidate_stat_cache(void) {
721 1           g_stat_cache.valid = 0;
722 1           }
723              
724             /* Invalidate cache for specific path */
725 1           static void invalidate_stat_cache_path(const char *path) {
726 1 50         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    50          
727 1           g_stat_cache.valid = 0;
728             }
729 1           }
730              
731             /* Check readable using cached stat */
732 9           static int file_is_readable_cached(const char *path) {
733             dTHX;
734             #ifdef _WIN32
735             return access(path, R_OK) == 0;
736             #else
737             Stat_t st;
738 9 100         if (cached_stat(path, &st) < 0) return 0;
739            
740 8 50         if (g_stat_cache.uid == 0) return 1; /* root can read anything */
741            
742 0 0         if (st.st_uid == g_stat_cache.uid) {
743 0           return (st.st_mode & S_IRUSR) != 0;
744 0 0         } else if (st.st_gid == g_stat_cache.gid) {
745 0           return (st.st_mode & S_IRGRP) != 0;
746             } else {
747 0           return (st.st_mode & S_IROTH) != 0;
748             }
749             #endif
750             }
751              
752             /* Check writable using cached stat */
753 9           static int file_is_writable_cached(const char *path) {
754             dTHX;
755             #ifdef _WIN32
756             return access(path, W_OK) == 0;
757             #else
758             Stat_t st;
759 9 100         if (cached_stat(path, &st) < 0) return 0;
760            
761 8 50         if (g_stat_cache.uid == 0) return 1; /* root can write anything */
762            
763 0 0         if (st.st_uid == g_stat_cache.uid) {
764 0           return (st.st_mode & S_IWUSR) != 0;
765 0 0         } else if (st.st_gid == g_stat_cache.gid) {
766 0           return (st.st_mode & S_IWGRP) != 0;
767             } else {
768 0           return (st.st_mode & S_IWOTH) != 0;
769             }
770             #endif
771             }
772              
773             /* Check executable using cached stat */
774 8           static int file_is_executable_cached(const char *path) {
775             dTHX;
776             #ifdef _WIN32
777             /* Windows: check file extension for executability */
778             const char *ext = strrchr(path, '.');
779             if (ext) {
780             if (_stricmp(ext, ".exe") == 0 || _stricmp(ext, ".bat") == 0 ||
781             _stricmp(ext, ".cmd") == 0 || _stricmp(ext, ".com") == 0) {
782             return access(path, R_OK) == 0;
783             }
784             }
785             return 0;
786             #else
787             Stat_t st;
788 8 50         if (cached_stat(path, &st) < 0) return 0;
789            
790 8 50         if (g_stat_cache.uid == 0) return 1; /* root can execute anything */
791            
792 0 0         if (st.st_uid == g_stat_cache.uid) {
793 0           return (st.st_mode & S_IXUSR) != 0;
794 0 0         } else if (st.st_gid == g_stat_cache.gid) {
795 0           return (st.st_mode & S_IXGRP) != 0;
796             } else {
797 0           return (st.st_mode & S_IXOTH) != 0;
798             }
799             #endif
800             }
801              
802              
803             /* ============================================
804             Custom op support for compile-time optimization
805             ============================================ */
806              
807             /* Custom op registrations */
808             static XOP file_slurp_xop;
809             static XOP file_spew_xop;
810             static XOP file_exists_xop;
811             static XOP file_size_xop;
812             static XOP file_is_file_xop;
813             static XOP file_is_dir_xop;
814             static XOP file_lines_xop;
815             static XOP file_unlink_xop;
816             static XOP file_mkdir_xop;
817             static XOP file_rmdir_xop;
818             static XOP file_basename_xop;
819             static XOP file_dirname_xop;
820             static XOP file_extname_xop;
821             static XOP file_touch_xop;
822             static XOP file_clear_stat_cache_xop;
823             static XOP file_mtime_xop;
824             static XOP file_atime_xop;
825             static XOP file_ctime_xop;
826             static XOP file_mode_xop;
827             static XOP file_is_link_xop;
828             static XOP file_is_readable_xop;
829             static XOP file_is_writable_xop;
830             static XOP file_is_executable_xop;
831             static XOP file_readdir_xop;
832             static XOP file_slurp_raw_xop;
833             static XOP file_copy_xop;
834             static XOP file_move_xop;
835             static XOP file_chmod_xop;
836             static XOP file_append_xop;
837             static XOP file_atomic_spew_xop;
838              
839             /* Forward declarations for internal functions */
840             static SV* file_slurp_internal(pTHX_ const char *path);
841             static SV* file_slurp_raw_internal(pTHX_ const char *path);
842             static int file_spew_internal(pTHX_ const char *path, SV *data);
843             static int file_append_internal(pTHX_ const char *path, SV *data);
844             static IV file_size_internal(const char *path);
845             static IV file_mtime_internal(const char *path);
846             static IV file_atime_internal(const char *path);
847             static IV file_ctime_internal(const char *path);
848             static IV file_mode_internal(const char *path);
849             static int file_exists_internal(const char *path);
850             static int file_is_file_internal(const char *path);
851             static int file_is_dir_internal(const char *path);
852             static int file_is_link_internal(const char *path);
853             static int file_is_readable_internal(const char *path);
854             static int file_is_writable_internal(const char *path);
855             static int file_is_executable_internal(const char *path);
856             static AV* file_split_lines(pTHX_ SV *content);
857             static int file_unlink_internal(const char *path);
858             static int file_copy_internal(pTHX_ const char *src, const char *dst);
859             static int file_move_internal(pTHX_ const char *src, const char *dst);
860             static int file_mkdir_internal(const char *path, int mode);
861             static int file_rmdir_internal(const char *path);
862             static int file_touch_internal(const char *path);
863             static int file_chmod_internal(const char *path, int mode);
864             static AV* file_readdir_internal(pTHX_ const char *path);
865             static int file_atomic_spew_internal(pTHX_ const char *path, SV *data);
866             static SV* file_basename_internal(pTHX_ const char *path);
867             static SV* file_dirname_internal(pTHX_ const char *path);
868             static SV* file_extname_internal(pTHX_ const char *path);
869              
870             /* Typedef for pp functions */
871             typedef OP* (*file_ppfunc)(pTHX);
872              
873             /* ============================================
874             Custom OP implementations - fastest path
875             ============================================ */
876              
877             /* pp_file_slurp: single path arg on stack - OPTIMIZED HOT PATH */
878 76           static OP* pp_file_slurp(pTHX) {
879 76           dSP;
880 76           SV *path_sv = POPs;
881 76           const char *path = SvPV_nolen(path_sv);
882             int fd;
883             Stat_t st;
884             SV *result;
885             char *buf;
886             ssize_t n, total;
887              
888             /* Fast path: direct syscalls. The custom op only fires when the
889             * call-checker accepted exactly one arg, so there is no plugin tail
890             * to dispatch here - the variadic XSUB owns that path. */
891             #ifdef _WIN32
892             fd = open(path, O_RDONLY | O_BINARY);
893             #else
894 76           fd = open(path, O_RDONLY);
895             #endif
896 76 50         if (fd < 0) {
897 0           PUSHs(&PL_sv_undef);
898 0           PUTBACK;
899 0           return NORMAL;
900             }
901              
902 76 50         if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
    50          
903 0           close(fd);
904 0           PUSHs(&PL_sv_undef);
905 0           PUTBACK;
906 0           return NORMAL;
907             }
908              
909             /* Empty file */
910 76 100         if (st.st_size == 0) {
911 1           close(fd);
912 1           result = newSVpvs("");
913 1           PUSHs(sv_2mortal(result));
914 1           PUTBACK;
915 1           return NORMAL;
916             }
917              
918             /* Hint to kernel: sequential read */
919 75           advise_sequential(fd, st.st_size);
920              
921             /* Pre-allocate exact size */
922 75           result = newSV(st.st_size + 1);
923 75           SvPOK_on(result);
924 75           buf = SvPVX(result);
925              
926             #ifndef _WIN32
927             /* Large files: use mmap for zero-copy */
928 75 50         if (st.st_size >= MMAP_SLURP_THRESHOLD) {
929 0           void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
930 0 0         if (map != MAP_FAILED) {
931             #ifdef MADV_SEQUENTIAL
932 0           madvise(map, st.st_size, MADV_SEQUENTIAL);
933             #endif
934 0           memcpy(buf, map, st.st_size);
935 0           buf[st.st_size] = '\0';
936 0           SvCUR_set(result, st.st_size);
937 0           munmap(map, st.st_size);
938 0           close(fd);
939 0           PUSHs(sv_2mortal(result));
940 0           PUTBACK;
941 0           return NORMAL;
942             }
943             /* mmap failed, fall through to read */
944             }
945             #endif
946              
947             /* Single read - common case for small/medium files */
948 75           n = read(fd, buf, st.st_size);
949 75 50         if (n == st.st_size) {
950             /* Got everything in one read - fast path */
951 75           close(fd);
952 75           buf[n] = '\0';
953 75           SvCUR_set(result, n);
954 75           PUSHs(sv_2mortal(result));
955 75           PUTBACK;
956 75           return NORMAL;
957             }
958            
959             /* Short read or error - need loop */
960 0 0         if (n < 0) {
961 0 0         if (errno == EINTR) {
962 0           n = 0; /* Start from beginning */
963             } else {
964 0           close(fd);
965 0           SvREFCNT_dec(result);
966 0           PUSHs(&PL_sv_undef);
967 0           PUTBACK;
968 0           return NORMAL;
969             }
970             }
971            
972 0           total = n;
973 0 0         while (total < st.st_size) {
974 0           n = read(fd, buf + total, st.st_size - total);
975 0 0         if (n < 0) {
976 0 0         if (errno == EINTR) continue;
977 0           close(fd);
978 0           SvREFCNT_dec(result);
979 0           PUSHs(&PL_sv_undef);
980 0           PUTBACK;
981 0           return NORMAL;
982             }
983 0 0         if (n == 0) break;
984 0           total += n;
985             }
986            
987 0           close(fd);
988 0           buf[total] = '\0';
989 0           SvCUR_set(result, total);
990            
991 0           PUSHs(sv_2mortal(result));
992 0           PUTBACK;
993 0           return NORMAL;
994             }
995              
996             /* pp_file_spew: path and data on stack */
997 86           static OP* pp_file_spew(pTHX) {
998 86           dSP;
999 86           SV *data = POPs;
1000 86           SV *path_sv = POPs;
1001 86           const char *path = SvPV_nolen(path_sv);
1002 86 50         PUSHs(file_spew_internal(aTHX_ path, data) ? &PL_sv_yes : &PL_sv_no);
1003 86           PUTBACK;
1004 86           return NORMAL;
1005             }
1006              
1007             /* pp_file_exists: single path arg on stack */
1008 147           static OP* pp_file_exists(pTHX) {
1009 147           dSP;
1010 147           SV *path_sv = POPs;
1011 147           const char *path = SvPV_nolen(path_sv);
1012 147 100         PUSHs(file_exists_internal(path) ? &PL_sv_yes : &PL_sv_no);
1013 147           PUTBACK;
1014 147           return NORMAL;
1015             }
1016              
1017             /* pp_file_size: single path arg on stack */
1018 54           static OP* pp_file_size(pTHX) {
1019 54           dSP;
1020 54           SV *path_sv = POPs;
1021 54           const char *path = SvPV_nolen(path_sv);
1022 54           PUSHs(sv_2mortal(newSViv(file_size_internal(path))));
1023 54           PUTBACK;
1024 54           return NORMAL;
1025             }
1026              
1027             /* pp_file_is_file: single path arg on stack */
1028 29           static OP* pp_file_is_file(pTHX) {
1029 29           dSP;
1030 29           SV *path_sv = POPs;
1031 29           const char *path = SvPV_nolen(path_sv);
1032 29 100         PUSHs(file_is_file_internal(path) ? &PL_sv_yes : &PL_sv_no);
1033 29           PUTBACK;
1034 29           return NORMAL;
1035             }
1036              
1037             /* pp_file_is_dir: single path arg on stack */
1038 24           static OP* pp_file_is_dir(pTHX) {
1039 24           dSP;
1040 24           SV *path_sv = POPs;
1041 24           const char *path = SvPV_nolen(path_sv);
1042 24 100         PUSHs(file_is_dir_internal(path) ? &PL_sv_yes : &PL_sv_no);
1043 24           PUTBACK;
1044 24           return NORMAL;
1045             }
1046              
1047             /* pp_file_lines: single path arg on stack */
1048 6           static OP* pp_file_lines(pTHX) {
1049 6           dSP;
1050 6           SV *path_sv = POPs;
1051 6           const char *path = SvPV_nolen(path_sv);
1052 6           SV *content = file_slurp_internal(aTHX_ path);
1053             AV *lines;
1054              
1055 6 50         if (content == &PL_sv_undef) {
1056 0           lines = newAV();
1057             } else {
1058 6           lines = file_split_lines(aTHX_ content);
1059 6           SvREFCNT_dec(content);
1060             }
1061              
1062 6           PUSHs(sv_2mortal(newRV_noinc((SV*)lines)));
1063 6           PUTBACK;
1064 6           return NORMAL;
1065             }
1066              
1067             /* pp_file_unlink: single path arg on stack */
1068 2           static OP* pp_file_unlink(pTHX) {
1069 2           dSP;
1070 2           SV *path_sv = POPs;
1071 2           const char *path = SvPV_nolen(path_sv);
1072 2 100         PUSHs(file_unlink_internal(path) ? &PL_sv_yes : &PL_sv_no);
1073 2           PUTBACK;
1074 2           return NORMAL;
1075             }
1076              
1077             /* pp_file_clear_stat_cache: optional path arg - clears stat cache */
1078 1           static OP* pp_file_clear_stat_cache(pTHX) {
1079 1           dSP;
1080 1           SV *path_sv = POPs;
1081            
1082 1 50         if (SvOK(path_sv)) {
1083             /* Clear cache for specific path */
1084 1           const char *path = SvPV_nolen(path_sv);
1085 1           invalidate_stat_cache_path(path);
1086             } else {
1087             /* Clear entire cache */
1088 0           invalidate_stat_cache();
1089             }
1090            
1091 1           PUSHs(&PL_sv_yes);
1092 1           PUTBACK;
1093 1           return NORMAL;
1094             }
1095              
1096             /* pp_file_mkdir: single path arg on stack (mode defaults to 0755) */
1097 4           static OP* pp_file_mkdir(pTHX) {
1098 4           dSP;
1099 4           SV *path_sv = POPs;
1100 4           const char *path = SvPV_nolen(path_sv);
1101 4 50         PUSHs(file_mkdir_internal(path, 0755) ? &PL_sv_yes : &PL_sv_no);
1102 4           PUTBACK;
1103 4           return NORMAL;
1104             }
1105              
1106             /* pp_file_rmdir: single path arg on stack */
1107 1           static OP* pp_file_rmdir(pTHX) {
1108 1           dSP;
1109 1           SV *path_sv = POPs;
1110 1           const char *path = SvPV_nolen(path_sv);
1111 1 50         PUSHs(file_rmdir_internal(path) ? &PL_sv_yes : &PL_sv_no);
1112 1           PUTBACK;
1113 1           return NORMAL;
1114             }
1115              
1116             /* pp_file_touch: single path arg on stack */
1117 1           static OP* pp_file_touch(pTHX) {
1118 1           dSP;
1119 1           SV *path_sv = POPs;
1120 1           const char *path = SvPV_nolen(path_sv);
1121 1 50         PUSHs(file_touch_internal(path) ? &PL_sv_yes : &PL_sv_no);
1122 1           PUTBACK;
1123 1           return NORMAL;
1124             }
1125              
1126             /* pp_file_basename: single path arg on stack */
1127 71           static OP* pp_file_basename(pTHX) {
1128 71           dSP;
1129 71           SV *path_sv = POPs;
1130 71           const char *path = SvPV_nolen(path_sv);
1131 71           PUSHs(sv_2mortal(file_basename_internal(aTHX_ path)));
1132 71           PUTBACK;
1133 71           return NORMAL;
1134             }
1135              
1136             /* pp_file_dirname: single path arg on stack */
1137 10           static OP* pp_file_dirname(pTHX) {
1138 10           dSP;
1139 10           SV *path_sv = POPs;
1140 10           const char *path = SvPV_nolen(path_sv);
1141 10           PUSHs(sv_2mortal(file_dirname_internal(aTHX_ path)));
1142 10           PUTBACK;
1143 10           return NORMAL;
1144             }
1145              
1146             /* pp_file_extname: single path arg on stack */
1147 36           static OP* pp_file_extname(pTHX) {
1148 36           dSP;
1149 36           SV *path_sv = POPs;
1150 36           const char *path = SvPV_nolen(path_sv);
1151 36           PUSHs(sv_2mortal(file_extname_internal(aTHX_ path)));
1152 36           PUTBACK;
1153 36           return NORMAL;
1154             }
1155              
1156             /* pp_file_mtime: single path arg on stack */
1157 27           static OP* pp_file_mtime(pTHX) {
1158 27           dSP;
1159 27           SV *path_sv = POPs;
1160 27           const char *path = SvPV_nolen(path_sv);
1161 27           PUSHs(sv_2mortal(newSViv(file_mtime_internal(path))));
1162 27           PUTBACK;
1163 27           return NORMAL;
1164             }
1165              
1166             /* pp_file_atime: single path arg on stack */
1167 6           static OP* pp_file_atime(pTHX) {
1168 6           dSP;
1169 6           SV *path_sv = POPs;
1170 6           const char *path = SvPV_nolen(path_sv);
1171 6           PUSHs(sv_2mortal(newSViv(file_atime_internal(path))));
1172 6           PUTBACK;
1173 6           return NORMAL;
1174             }
1175              
1176             /* pp_file_ctime: single path arg on stack */
1177 6           static OP* pp_file_ctime(pTHX) {
1178 6           dSP;
1179 6           SV *path_sv = POPs;
1180 6           const char *path = SvPV_nolen(path_sv);
1181 6           PUSHs(sv_2mortal(newSViv(file_ctime_internal(path))));
1182 6           PUTBACK;
1183 6           return NORMAL;
1184             }
1185              
1186             /* pp_file_mode: single path arg on stack */
1187 3           static OP* pp_file_mode(pTHX) {
1188 3           dSP;
1189 3           SV *path_sv = POPs;
1190 3           const char *path = SvPV_nolen(path_sv);
1191 3           PUSHs(sv_2mortal(newSViv(file_mode_internal(path))));
1192 3           PUTBACK;
1193 3           return NORMAL;
1194             }
1195              
1196             /* pp_file_is_link: single path arg on stack */
1197 6           static OP* pp_file_is_link(pTHX) {
1198 6           dSP;
1199 6           SV *path_sv = POPs;
1200 6           const char *path = SvPV_nolen(path_sv);
1201 6 100         PUSHs(file_is_link_internal(path) ? &PL_sv_yes : &PL_sv_no);
1202 6           PUTBACK;
1203 6           return NORMAL;
1204             }
1205              
1206             /* pp_file_is_readable: single path arg on stack */
1207 7           static OP* pp_file_is_readable(pTHX) {
1208 7           dSP;
1209 7           SV *path_sv = POPs;
1210 7           const char *path = SvPV_nolen(path_sv);
1211 7 50         PUSHs(file_is_readable_internal(path) ? &PL_sv_yes : &PL_sv_no);
1212 7           PUTBACK;
1213 7           return NORMAL;
1214             }
1215              
1216             /* pp_file_is_writable: single path arg on stack */
1217 6           static OP* pp_file_is_writable(pTHX) {
1218 6           dSP;
1219 6           SV *path_sv = POPs;
1220 6           const char *path = SvPV_nolen(path_sv);
1221 6 50         PUSHs(file_is_writable_internal(path) ? &PL_sv_yes : &PL_sv_no);
1222 6           PUTBACK;
1223 6           return NORMAL;
1224             }
1225              
1226             /* pp_file_is_executable: single path arg on stack */
1227 8           static OP* pp_file_is_executable(pTHX) {
1228 8           dSP;
1229 8           SV *path_sv = POPs;
1230 8           const char *path = SvPV_nolen(path_sv);
1231 8 50         PUSHs(file_is_executable_internal(path) ? &PL_sv_yes : &PL_sv_no);
1232 8           PUTBACK;
1233 8           return NORMAL;
1234             }
1235              
1236             /* pp_file_readdir: single path arg on stack */
1237 0           static OP* pp_file_readdir(pTHX) {
1238 0           dSP;
1239 0           SV *path_sv = POPs;
1240 0           const char *path = SvPV_nolen(path_sv);
1241 0           AV *result = file_readdir_internal(aTHX_ path);
1242 0           PUSHs(sv_2mortal(newRV_noinc((SV*)result)));
1243 0           PUTBACK;
1244 0           return NORMAL;
1245             }
1246              
1247             /* pp_file_slurp_raw: single path arg on stack (bypasses hooks) */
1248 0           static OP* pp_file_slurp_raw(pTHX) {
1249 0           dSP;
1250 0           SV *path_sv = POPs;
1251 0           const char *path = SvPV_nolen(path_sv);
1252 0           SV *result = file_slurp_raw_internal(aTHX_ path);
1253 0           PUSHs(sv_2mortal(result));
1254 0           PUTBACK;
1255 0           return NORMAL;
1256             }
1257              
1258             /* pp_file_copy: src and dst on stack */
1259 1           static OP* pp_file_copy(pTHX) {
1260 1           dSP;
1261 1           SV *dst_sv = POPs;
1262 1           SV *src_sv = POPs;
1263 1           const char *src = SvPV_nolen(src_sv);
1264 1           const char *dst = SvPV_nolen(dst_sv);
1265 1 50         PUSHs(file_copy_internal(aTHX_ src, dst) ? &PL_sv_yes : &PL_sv_no);
1266 1           PUTBACK;
1267 1           return NORMAL;
1268             }
1269              
1270             /* pp_file_move: src and dst on stack */
1271 0           static OP* pp_file_move(pTHX) {
1272 0           dSP;
1273 0           SV *dst_sv = POPs;
1274 0           SV *src_sv = POPs;
1275 0           const char *src = SvPV_nolen(src_sv);
1276 0           const char *dst = SvPV_nolen(dst_sv);
1277 0 0         PUSHs(file_move_internal(aTHX_ src, dst) ? &PL_sv_yes : &PL_sv_no);
1278 0           PUTBACK;
1279 0           return NORMAL;
1280             }
1281              
1282             /* pp_file_chmod: path and mode on stack */
1283 0           static OP* pp_file_chmod(pTHX) {
1284 0           dSP;
1285 0           SV *mode_sv = POPs;
1286 0           SV *path_sv = POPs;
1287 0           const char *path = SvPV_nolen(path_sv);
1288 0           int mode = SvIV(mode_sv);
1289 0 0         PUSHs(file_chmod_internal(path, mode) ? &PL_sv_yes : &PL_sv_no);
1290 0           PUTBACK;
1291 0           return NORMAL;
1292             }
1293              
1294             /* pp_file_append: path and data on stack */
1295 2           static OP* pp_file_append(pTHX) {
1296 2           dSP;
1297 2           SV *data = POPs;
1298 2           SV *path_sv = POPs;
1299 2           const char *path = SvPV_nolen(path_sv);
1300 2 50         PUSHs(file_append_internal(aTHX_ path, data) ? &PL_sv_yes : &PL_sv_no);
1301 2           PUTBACK;
1302 2           return NORMAL;
1303             }
1304              
1305             /* pp_file_atomic_spew: path and data on stack */
1306 0           static OP* pp_file_atomic_spew(pTHX) {
1307 0           dSP;
1308 0           SV *data = POPs;
1309 0           SV *path_sv = POPs;
1310 0           const char *path = SvPV_nolen(path_sv);
1311 0 0         PUSHs(file_atomic_spew_internal(aTHX_ path, data) ? &PL_sv_yes : &PL_sv_no);
1312 0           PUTBACK;
1313 0           return NORMAL;
1314             }
1315              
1316             /* ============================================
1317             Call checkers for compile-time optimization
1318             ============================================ */
1319              
1320             /* Count args between pushop's first sibling and the trailing cv op.
1321             * The last sibling in the chain is the cv (we don't replace it), so the
1322             * arg count is (total siblings) - 1. Returns -1 if the chain is shorter
1323             * than expected (no args at all). */
1324 265           static int file_count_call_args(OP *pushop) {
1325 265 50         OP *o = OpSIBLING(pushop);
1326 265           int n = 0;
1327 949 100         while (o) {
1328 684           n++;
1329 684 100         o = OpSIBLING(o);
1330             }
1331 265           return n > 0 ? n - 1 : -1;
1332             }
1333              
1334             /* 1-arg call checker (slurp, exists, size, is_file, is_dir, lines).
1335             * Bails when items != 1 so the regular XSUB sees the full arg list -
1336             * critical for the plugin tail (slurp($p, plugin => ..., key => val)). */
1337 189           static OP* file_call_checker_1arg(pTHX_ OP *entersubop, GV *namegv, SV *ckobj) {
1338 189           file_ppfunc ppfunc = (file_ppfunc)SvIVX(ckobj);
1339             OP *pushop, *cvop, *argop;
1340             OP *newop;
1341              
1342             PERL_UNUSED_ARG(namegv);
1343              
1344             /* Navigate to first child */
1345 189           pushop = cUNOPx(entersubop)->op_first;
1346 189 50         if (!OpHAS_SIBLING(pushop)) {
1347 189           pushop = cUNOPx(pushop)->op_first;
1348             }
1349              
1350 189 100         if (file_count_call_args(pushop) != 1) return entersubop;
1351              
1352             /* Get the args: pushmark -> arg -> cv */
1353 169 50         argop = OpSIBLING(pushop);
1354 169 50         if (!argop) return entersubop;
1355              
1356 169 50         cvop = OpSIBLING(argop);
1357 169 50         if (!cvop) return entersubop;
1358              
1359             /* Detach arg from tree */
1360 169           OpMORESIB_set(pushop, cvop);
1361 169           OpLASTSIB_set(argop, NULL);
1362              
1363             /* Force scalar context so function calls return exactly one value */
1364 169           argop = op_contextualize(argop, G_SCALAR);
1365              
1366             /* Create as OP_NULL first to avoid -DDEBUGGING assertion in newUNOP,
1367             then convert to OP_CUSTOM */
1368 169           newop = newUNOP(OP_NULL, 0, argop);
1369 169           newop->op_type = OP_CUSTOM;
1370 169           newop->op_ppaddr = ppfunc;
1371              
1372 169           op_free(entersubop);
1373 169           return newop;
1374             }
1375              
1376             /* 2-arg call checker (spew, append).
1377             * Bails when items != 2 so the regular XSUB sees the full arg list -
1378             * critical for the plugin tail (spew($p, $data, plugin => ..., ...)). */
1379 76           static OP* file_call_checker_2arg(pTHX_ OP *entersubop, GV *namegv, SV *ckobj) {
1380 76           file_ppfunc ppfunc = (file_ppfunc)SvIVX(ckobj);
1381             OP *pushop, *cvop, *pathop, *dataop;
1382             OP *newop;
1383              
1384             PERL_UNUSED_ARG(namegv);
1385              
1386             /* Navigate to first child */
1387 76           pushop = cUNOPx(entersubop)->op_first;
1388 76 50         if (!OpHAS_SIBLING(pushop)) {
1389 76           pushop = cUNOPx(pushop)->op_first;
1390             }
1391              
1392 76 100         if (file_count_call_args(pushop) != 2) return entersubop;
1393              
1394             /* Get the args: pushmark -> path -> data -> cv */
1395 61 50         pathop = OpSIBLING(pushop);
1396 61 50         if (!pathop) return entersubop;
1397              
1398 61 50         dataop = OpSIBLING(pathop);
1399 61 50         if (!dataop) return entersubop;
1400              
1401 61 50         cvop = OpSIBLING(dataop);
1402 61 50         if (!cvop) return entersubop;
1403              
1404             /* Detach args from tree */
1405 61           OpMORESIB_set(pushop, cvop);
1406 61           OpLASTSIB_set(pathop, NULL);
1407 61           OpLASTSIB_set(dataop, NULL);
1408              
1409             /* Force scalar context on both args so function calls
1410             return exactly one value on the stack */
1411 61           pathop = op_contextualize(pathop, G_SCALAR);
1412 61           dataop = op_contextualize(dataop, G_SCALAR);
1413              
1414             /* Create as OP_NULL first to avoid -DDEBUGGING assertion in newBINOP,
1415             then convert to OP_CUSTOM */
1416 61           newop = newBINOP(OP_NULL, 0, pathop, dataop);
1417 61           newop->op_type = OP_CUSTOM;
1418 61           newop->op_ppaddr = ppfunc;
1419              
1420 61           op_free(entersubop);
1421 61           return newop;
1422             }
1423              
1424             /* Install 1-arg function with call checker */
1425 105           static void install_file_func_1arg(pTHX_ const char *pkg, const char *name,
1426             XSUBADDR_t xsub, file_ppfunc ppfunc) {
1427             char full_name[256];
1428             CV *cv;
1429             SV *ckobj;
1430              
1431 105           snprintf(full_name, sizeof(full_name), "%s::%s", pkg, name);
1432 105           cv = newXS(full_name, xsub, __FILE__);
1433              
1434 105           ckobj = newSViv(PTR2IV(ppfunc));
1435 105           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
1436 105           }
1437              
1438             /* Install 2-arg function with call checker */
1439 30           static void install_file_func_2arg(pTHX_ const char *pkg, const char *name,
1440             XSUBADDR_t xsub, file_ppfunc ppfunc) {
1441             char full_name[256];
1442             CV *cv;
1443             SV *ckobj;
1444              
1445 30           snprintf(full_name, sizeof(full_name), "%s::%s", pkg, name);
1446 30           cv = newXS(full_name, xsub, __FILE__);
1447              
1448 30           ckobj = newSViv(PTR2IV(ppfunc));
1449 30           cv_set_call_checker(cv, file_call_checker_2arg, ckobj);
1450 30           }
1451              
1452             /* ============================================
1453             Memory-mapped file registry
1454             ============================================ */
1455              
1456             typedef struct {
1457             void *addr; /* Mapped address */
1458             size_t len; /* Mapped length */
1459             int refcount; /* Reference count */
1460             #ifdef _WIN32
1461             HANDLE file_handle; /* Windows file handle */
1462             HANDLE map_handle; /* Windows mapping handle */
1463             #else
1464             int fd; /* File descriptor (POSIX) */
1465             #endif
1466             } MmapEntry;
1467              
1468             static MmapEntry *g_mmaps = NULL;
1469             static IV g_mmaps_size = 0;
1470             static IV g_mmaps_count = 0;
1471              
1472             /* Free list for mmap reuse */
1473             static IV *g_free_mmaps = NULL;
1474             static IV g_free_mmaps_size = 0;
1475             static IV g_free_mmaps_count = 0;
1476              
1477             /* ============================================
1478             Line iterator registry
1479             ============================================ */
1480              
1481             typedef struct {
1482             int fd; /* File descriptor (-1 in record-iter mode) */
1483             char *buffer; /* Read buffer (NULL in record-iter mode) */
1484             size_t buf_size; /* Buffer size */
1485             size_t buf_pos; /* Current position in buffer */
1486             size_t buf_len; /* Valid data length in buffer */
1487             int eof; /* End of file reached */
1488             int refcount; /* Reference count */
1489             char *path; /* File path (for reopening) */
1490             /* Record-iterator mode (set when lines_iter was called with a
1491             * plugin tail). When records is non-NULL, next/eof/close walk the
1492             * AoA instead of reading bytes from fd. */
1493             AV *records;
1494             SSize_t records_idx;
1495             } LineIterEntry;
1496              
1497             static LineIterEntry *g_iters = NULL;
1498             static IV g_iters_size = 0;
1499             static IV g_iters_count = 0;
1500              
1501             static IV *g_free_iters = NULL;
1502             static IV g_free_iters_size = 0;
1503             static IV g_free_iters_count = 0;
1504              
1505             /* ============================================
1506             Initialization
1507             ============================================ */
1508              
1509             static int file_initialized = 0;
1510              
1511             /* Forward declaration for callback registry init */
1512             static void file_init_callback_registry(pTHX);
1513              
1514 28           static void file_init(pTHX) {
1515 28 50         if (file_initialized) return;
1516              
1517 28           g_mmaps_size = 16;
1518 28 50         Newxz(g_mmaps, g_mmaps_size, MmapEntry);
1519 28           g_free_mmaps_size = 16;
1520 28 50         Newxz(g_free_mmaps, g_free_mmaps_size, IV);
1521              
1522 28           g_iters_size = 16;
1523 28 50         Newxz(g_iters, g_iters_size, LineIterEntry);
1524 28           g_free_iters_size = 16;
1525 28 50         Newxz(g_free_iters, g_free_iters_size, IV);
1526              
1527             /* Initialize callback registry with built-in predicates */
1528 28           file_init_callback_registry(aTHX);
1529              
1530 28           file_initialized = 1;
1531             }
1532              
1533             /* ============================================
1534             Fast slurp - read entire file into SV
1535             ============================================ */
1536              
1537 73           static SV* file_slurp_internal(pTHX_ const char *path) {
1538             int fd;
1539             Stat_t st;
1540             SV *result;
1541             char *buf;
1542 73           ssize_t total = 0, n;
1543             #ifdef _WIN32
1544             int open_flags = O_RDONLY | O_BINARY;
1545             #else
1546             /* O_NOATIME avoids updating access time - reduces disk writes */
1547             #ifdef __linux__
1548 73           int open_flags = O_RDONLY | O_NOATIME;
1549             #else
1550             int open_flags = O_RDONLY;
1551             #endif
1552             #endif
1553              
1554 73           fd = open(path, open_flags);
1555             #ifdef __linux__
1556             /* Fallback if O_NOATIME fails (not owner) */
1557 73 100         if (fd < 0 && errno == EPERM) {
    50          
1558 0           fd = open(path, O_RDONLY);
1559             }
1560             #endif
1561 73 100         if (fd < 0) {
1562 3           return &PL_sv_undef;
1563             }
1564              
1565 70 50         if (fstat(fd, &st) < 0) {
1566 0           close(fd);
1567 0           return &PL_sv_undef;
1568             }
1569              
1570             /* Hint to kernel: sequential read pattern */
1571 70           advise_sequential(fd, st.st_size);
1572              
1573             /* Pre-allocate exact size for regular files */
1574 70 50         if (S_ISREG(st.st_mode) && st.st_size > 0) {
    100          
1575             #ifndef _WIN32
1576             /* For large files, use mmap + memcpy - faster than read() syscalls */
1577 65 50         if (st.st_size >= MMAP_SLURP_THRESHOLD) {
1578 0           void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
1579 0 0         if (map != MAP_FAILED) {
1580             /* Hint: we'll read sequentially */
1581             #ifdef MADV_SEQUENTIAL
1582 0           madvise(map, st.st_size, MADV_SEQUENTIAL);
1583             #endif
1584            
1585 0           result = newSV(st.st_size + 1);
1586 0           SvPOK_on(result);
1587 0           buf = SvPVX(result);
1588 0           memcpy(buf, map, st.st_size);
1589 0           buf[st.st_size] = '\0';
1590 0           SvCUR_set(result, st.st_size);
1591            
1592 0           munmap(map, st.st_size);
1593 0           close(fd);
1594 0           goto done;
1595             }
1596             /* mmap failed, fall through to read() */
1597             }
1598             #endif
1599 65           result = newSV(st.st_size + 1);
1600 65           SvPOK_on(result);
1601 65           buf = SvPVX(result);
1602              
1603             /* Read in one shot if possible */
1604 130 100         while (total < st.st_size) {
1605 65           n = read(fd, buf + total, st.st_size - total);
1606 65 50         if (n < 0) {
1607 0 0         if (errno == EINTR) continue;
1608 0           close(fd);
1609 0           SvREFCNT_dec(result);
1610 0           return &PL_sv_undef;
1611             }
1612 65 50         if (n == 0) break;
1613 65           total += n;
1614             }
1615              
1616 65           buf[total] = '\0';
1617 65           SvCUR_set(result, total);
1618             } else {
1619             /* Stream or unknown size - read in chunks */
1620 5           size_t capacity = FILE_BUFFER_SIZE;
1621 5           result = newSV(capacity);
1622 5           SvPOK_on(result);
1623 5           buf = SvPVX(result);
1624              
1625             while (1) {
1626 5 50         if (total >= (ssize_t)capacity - 1) {
1627 0           capacity *= 2;
1628 0 0         SvGROW(result, capacity);
    0          
1629 0           buf = SvPVX(result);
1630             }
1631              
1632 5           n = read(fd, buf + total, capacity - total - 1);
1633 5 50         if (n < 0) {
1634 0 0         if (errno == EINTR) continue;
1635 0           close(fd);
1636 0           SvREFCNT_dec(result);
1637 0           return &PL_sv_undef;
1638             }
1639 5 50         if (n == 0) break;
1640 0           total += n;
1641             }
1642              
1643 5           buf[total] = '\0';
1644 5           SvCUR_set(result, total);
1645             }
1646              
1647 70           close(fd);
1648              
1649 70           done:
1650 70           return result;
1651             }
1652              
1653             /* ============================================
1654             Fast slurp binary - same as slurp but explicit
1655             (bypasses hooks - for raw binary data)
1656             ============================================ */
1657              
1658 2           static SV* file_slurp_raw_internal(pTHX_ const char *path) {
1659             int fd;
1660             Stat_t st;
1661             SV *result;
1662             char *buf;
1663 2           ssize_t total = 0, n;
1664             #ifdef _WIN32
1665             int open_flags = O_RDONLY | O_BINARY;
1666             #else
1667             #ifdef __linux__
1668 2           int open_flags = O_RDONLY | O_NOATIME;
1669             #else
1670             int open_flags = O_RDONLY;
1671             #endif
1672             #endif
1673              
1674 2           fd = open(path, open_flags);
1675             #ifdef __linux__
1676 2 50         if (fd < 0 && errno == EPERM) {
    0          
1677 0           fd = open(path, O_RDONLY);
1678             }
1679             #endif
1680 2 50         if (fd < 0) {
1681 0           return &PL_sv_undef;
1682             }
1683              
1684 2 50         if (fstat(fd, &st) < 0) {
1685 0           close(fd);
1686 0           return &PL_sv_undef;
1687             }
1688              
1689             /* Hint to kernel: sequential read pattern */
1690 2           advise_sequential(fd, st.st_size);
1691              
1692 2 50         if (S_ISREG(st.st_mode) && st.st_size > 0) {
    50          
1693             #ifndef _WIN32
1694             /* For large files, use mmap + memcpy */
1695 2 50         if (st.st_size >= MMAP_SLURP_THRESHOLD) {
1696 0           void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
1697 0 0         if (map != MAP_FAILED) {
1698             #ifdef MADV_SEQUENTIAL
1699 0           madvise(map, st.st_size, MADV_SEQUENTIAL);
1700             #endif
1701            
1702 0           result = newSV(st.st_size + 1);
1703 0           SvPOK_on(result);
1704 0           buf = SvPVX(result);
1705 0           memcpy(buf, map, st.st_size);
1706 0           buf[st.st_size] = '\0';
1707 0           SvCUR_set(result, st.st_size);
1708            
1709 0           munmap(map, st.st_size);
1710 0           close(fd);
1711 0           return result;
1712             }
1713             }
1714             #endif
1715 2           result = newSV(st.st_size + 1);
1716 2           SvPOK_on(result);
1717 2           buf = SvPVX(result);
1718              
1719 4 100         while (total < st.st_size) {
1720 2           n = read(fd, buf + total, st.st_size - total);
1721 2 50         if (n < 0) {
1722 0 0         if (errno == EINTR) continue;
1723 0           close(fd);
1724 0           SvREFCNT_dec(result);
1725 0           return &PL_sv_undef;
1726             }
1727 2 50         if (n == 0) break;
1728 2           total += n;
1729             }
1730              
1731 2           buf[total] = '\0';
1732 2           SvCUR_set(result, total);
1733             } else {
1734 0           size_t capacity = FILE_BUFFER_SIZE;
1735 0           result = newSV(capacity);
1736 0           SvPOK_on(result);
1737 0           buf = SvPVX(result);
1738              
1739             while (1) {
1740 0 0         if (total >= (ssize_t)capacity - 1) {
1741 0           capacity *= 2;
1742 0 0         SvGROW(result, capacity);
    0          
1743 0           buf = SvPVX(result);
1744             }
1745              
1746 0           n = read(fd, buf + total, capacity - total - 1);
1747 0 0         if (n < 0) {
1748 0 0         if (errno == EINTR) continue;
1749 0           close(fd);
1750 0           SvREFCNT_dec(result);
1751 0           return &PL_sv_undef;
1752             }
1753 0 0         if (n == 0) break;
1754 0           total += n;
1755             }
1756              
1757 0           buf[total] = '\0';
1758 0           SvCUR_set(result, total);
1759             }
1760              
1761 2           close(fd);
1762 2           return result; /* No hooks for raw */
1763             }
1764              
1765 2           static SV* file_slurp_raw(pTHX_ const char *path) {
1766 2           return file_slurp_raw_internal(aTHX_ path);
1767             }
1768              
1769             /* ============================================
1770             Fast spew - write SV to file
1771             ============================================ */
1772              
1773 177           static int file_spew_internal(pTHX_ const char *path, SV *data) {
1774             int fd;
1775             const char *buf;
1776             STRLEN len;
1777             ssize_t n;
1778 177           SV *write_data = data;
1779 177           int free_write_data = 0;
1780             #ifdef _WIN32
1781             int open_flags = O_WRONLY | O_CREAT | O_TRUNC | O_BINARY;
1782             #else
1783 177           int open_flags = O_WRONLY | O_CREAT | O_TRUNC;
1784             #endif
1785              
1786 177           buf = SvPV(write_data, len);
1787              
1788 177           fd = file_open3(path, open_flags, 0644);
1789 177 50         if (UNLIKELY(fd < 0)) {
1790 0 0         if (free_write_data) SvREFCNT_dec(write_data);
1791 0           return 0;
1792             }
1793              
1794             #if defined(__linux__)
1795             /* Pre-allocate space for large files to avoid fragmentation */
1796 177 100         if (len >= 65536) {
1797 4           posix_fallocate(fd, 0, len);
1798             }
1799             #endif
1800              
1801             /* Fast path: single write for common case */
1802 177           n = write(fd, buf, len);
1803 177 50         if (LIKELY(n == (ssize_t)len)) {
1804 177           close(fd);
1805 177 50         if (free_write_data) SvREFCNT_dec(write_data);
1806             /* Invalidate cache for this path */
1807 177 100         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    100          
1808 2           g_stat_cache.valid = 0;
1809             }
1810 177           return 1;
1811             }
1812              
1813             /* Handle partial write or error */
1814 0 0         if (n < 0) {
1815 0 0         if (errno != EINTR) {
1816 0           close(fd);
1817 0 0         if (free_write_data) SvREFCNT_dec(write_data);
1818 0           return 0;
1819             }
1820 0           n = 0;
1821             }
1822              
1823             /* Loop for remaining data (rare) */
1824             {
1825 0           ssize_t written = n;
1826 0 0         while ((size_t)written < len) {
1827 0           n = write(fd, buf + written, len - written);
1828 0 0         if (n < 0) {
1829 0 0         if (errno == EINTR) continue;
1830 0           close(fd);
1831 0 0         if (free_write_data) SvREFCNT_dec(write_data);
1832 0           return 0;
1833             }
1834 0           written += n;
1835             }
1836             }
1837              
1838 0           close(fd);
1839 0 0         if (free_write_data) SvREFCNT_dec(write_data);
1840             /* Invalidate cache for this path */
1841 0 0         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    0          
1842 0           g_stat_cache.valid = 0;
1843             }
1844 0           return 1;
1845             }
1846              
1847             /* ============================================
1848             Fast append - append SV to file
1849             ============================================ */
1850              
1851 7           static int file_append_internal(pTHX_ const char *path, SV *data) {
1852             int fd;
1853             const char *buf;
1854             STRLEN len;
1855             ssize_t n;
1856             #ifdef _WIN32
1857             int open_flags = O_WRONLY | O_CREAT | O_APPEND | O_BINARY;
1858             #else
1859 7           int open_flags = O_WRONLY | O_CREAT | O_APPEND;
1860             #endif
1861              
1862 7           buf = SvPV(data, len);
1863              
1864 7           fd = file_open3(path, open_flags, 0644);
1865 7 50         if (UNLIKELY(fd < 0)) {
1866 0           return 0;
1867             }
1868              
1869             /* Fast path: single write for common case */
1870 7           n = write(fd, buf, len);
1871 7 50         if (LIKELY(n == (ssize_t)len)) {
1872 7           close(fd);
1873             /* Invalidate cache for this path */
1874 7 100         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    100          
1875 1           g_stat_cache.valid = 0;
1876             }
1877 7           return 1;
1878             }
1879              
1880             /* Handle partial write or error */
1881 0 0         if (n < 0) {
1882 0 0         if (errno != EINTR) {
1883 0           close(fd);
1884 0           return 0;
1885             }
1886 0           n = 0;
1887             }
1888              
1889             /* Loop for remaining data (rare) */
1890             {
1891 0           ssize_t written = n;
1892 0 0         while ((size_t)written < len) {
1893 0           n = write(fd, buf + written, len - written);
1894 0 0         if (n < 0) {
1895 0 0         if (errno == EINTR) continue;
1896 0           close(fd);
1897 0           return 0;
1898             }
1899 0           written += n;
1900             }
1901             }
1902              
1903 0           close(fd);
1904             /* Invalidate cache for this path */
1905 0 0         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    0          
1906 0           g_stat_cache.valid = 0;
1907             }
1908 0           return 1;
1909             }
1910              
1911             /* ============================================
1912             Memory-mapped file operations
1913             ============================================ */
1914              
1915 4           static void ensure_mmaps_capacity(IV needed) {
1916 4 50         if (needed >= g_mmaps_size) {
1917 0 0         IV new_size = g_mmaps_size ? g_mmaps_size * 2 : 16;
1918             IV i;
1919 0 0         while (new_size <= needed) new_size *= 2;
1920 0 0         Renew(g_mmaps, new_size, MmapEntry);
1921 0 0         for (i = g_mmaps_size; i < new_size; i++) {
1922 0           g_mmaps[i].addr = NULL;
1923 0           g_mmaps[i].len = 0;
1924 0           g_mmaps[i].refcount = 0;
1925             #ifdef _WIN32
1926             g_mmaps[i].file_handle = INVALID_HANDLE_VALUE;
1927             g_mmaps[i].map_handle = INVALID_HANDLE_VALUE;
1928             #else
1929 0           g_mmaps[i].fd = -1;
1930             #endif
1931             }
1932 0           g_mmaps_size = new_size;
1933             }
1934 4           }
1935              
1936 14           static IV alloc_mmap_slot(void) {
1937             IV idx;
1938              
1939 14 100         if (g_free_mmaps_count > 0) {
1940 10           return g_free_mmaps[--g_free_mmaps_count];
1941             }
1942              
1943 4           ensure_mmaps_capacity(g_mmaps_count);
1944 4           idx = g_mmaps_count++;
1945 4           return idx;
1946             }
1947              
1948 14           static void free_mmap_slot(IV idx) {
1949             dTHX;
1950             MmapEntry *entry;
1951              
1952 14 50         if (idx < 0 || idx >= g_mmaps_count) return;
    50          
1953              
1954 14           entry = &g_mmaps[idx];
1955             #ifdef _WIN32
1956             if (entry->addr) {
1957             UnmapViewOfFile(entry->addr);
1958             }
1959             if (entry->map_handle != INVALID_HANDLE_VALUE) {
1960             CloseHandle(entry->map_handle);
1961             }
1962             if (entry->file_handle != INVALID_HANDLE_VALUE) {
1963             CloseHandle(entry->file_handle);
1964             }
1965             entry->file_handle = INVALID_HANDLE_VALUE;
1966             entry->map_handle = INVALID_HANDLE_VALUE;
1967             #else
1968 14 50         if (entry->addr && entry->addr != MAP_FAILED) {
    50          
1969 14           munmap(entry->addr, entry->len);
1970             }
1971 14 50         if (entry->fd >= 0) {
1972 14           close(entry->fd);
1973             }
1974 14           entry->fd = -1;
1975             #endif
1976 14           entry->addr = NULL;
1977 14           entry->len = 0;
1978 14           entry->refcount = 0;
1979              
1980 14 50         if (g_free_mmaps_count >= g_free_mmaps_size) {
1981 0           g_free_mmaps_size *= 2;
1982 0 0         Renew(g_free_mmaps, g_free_mmaps_size, IV);
1983             }
1984 14           g_free_mmaps[g_free_mmaps_count++] = idx;
1985             }
1986              
1987 16           static IV file_mmap_open(pTHX_ const char *path, int writable) {
1988             IV idx;
1989             void *addr;
1990             size_t file_size;
1991              
1992             #ifdef _WIN32
1993             HANDLE file_handle;
1994             HANDLE map_handle;
1995             LARGE_INTEGER size;
1996             DWORD access = writable ? GENERIC_READ | GENERIC_WRITE : GENERIC_READ;
1997             DWORD share = FILE_SHARE_READ;
1998             DWORD protect = writable ? PAGE_READWRITE : PAGE_READONLY;
1999             DWORD map_access = writable ? FILE_MAP_WRITE : FILE_MAP_READ;
2000              
2001             file_handle = CreateFileA(path, access, share, NULL, OPEN_EXISTING,
2002             FILE_ATTRIBUTE_NORMAL, NULL);
2003             if (file_handle == INVALID_HANDLE_VALUE) {
2004             return -1;
2005             }
2006              
2007             if (!GetFileSizeEx(file_handle, &size)) {
2008             CloseHandle(file_handle);
2009             return -1;
2010             }
2011              
2012             if (size.QuadPart == 0) {
2013             CloseHandle(file_handle);
2014             return -1;
2015             }
2016              
2017             file_size = (size_t)size.QuadPart;
2018              
2019             map_handle = CreateFileMappingA(file_handle, NULL, protect, 0, 0, NULL);
2020             if (map_handle == NULL) {
2021             CloseHandle(file_handle);
2022             return -1;
2023             }
2024              
2025             addr = MapViewOfFile(map_handle, map_access, 0, 0, 0);
2026             if (addr == NULL) {
2027             CloseHandle(map_handle);
2028             CloseHandle(file_handle);
2029             return -1;
2030             }
2031              
2032             idx = alloc_mmap_slot();
2033             g_mmaps[idx].addr = addr;
2034             g_mmaps[idx].len = file_size;
2035             g_mmaps[idx].file_handle = file_handle;
2036             g_mmaps[idx].map_handle = map_handle;
2037             g_mmaps[idx].refcount = 1;
2038              
2039             #else
2040             int fd;
2041             Stat_t st;
2042 16 100         int flags = writable ? O_RDWR : O_RDONLY;
2043 16 100         int prot = writable ? (PROT_READ | PROT_WRITE) : PROT_READ;
2044              
2045 16           fd = open(path, flags);
2046 16 100         if (fd < 0) {
2047 1           return -1;
2048             }
2049              
2050 15 50         if (fstat(fd, &st) < 0) {
2051 0           close(fd);
2052 0           return -1;
2053             }
2054              
2055 15 100         if (st.st_size == 0) {
2056             /* Can't mmap empty file */
2057 1           close(fd);
2058 1           return -1;
2059             }
2060              
2061 14           file_size = st.st_size;
2062              
2063 14           addr = mmap(NULL, st.st_size, prot, MAP_SHARED, fd, 0);
2064 14 50         if (addr == MAP_FAILED) {
2065 0           close(fd);
2066 0           return -1;
2067             }
2068              
2069 14           idx = alloc_mmap_slot();
2070 14           g_mmaps[idx].addr = addr;
2071 14           g_mmaps[idx].len = file_size;
2072 14           g_mmaps[idx].fd = fd;
2073 14           g_mmaps[idx].refcount = 1;
2074             #endif
2075              
2076 14           return idx;
2077             }
2078              
2079 14           static SV* file_mmap_get_sv(pTHX_ IV idx) {
2080             MmapEntry *entry;
2081             SV *sv;
2082              
2083 14 50         if (idx < 0 || idx >= g_mmaps_count) {
    50          
2084 0           return &PL_sv_undef;
2085             }
2086              
2087 14           entry = &g_mmaps[idx];
2088             #ifdef _WIN32
2089             if (!entry->addr) {
2090             return &PL_sv_undef;
2091             }
2092             #else
2093 14 50         if (!entry->addr || entry->addr == MAP_FAILED) {
    50          
2094 0           return &PL_sv_undef;
2095             }
2096             #endif
2097              
2098             /* Create an SV that points directly to the mapped memory */
2099 14           sv = newSV(0);
2100 14 50         SvUPGRADE(sv, SVt_PV);
2101 14           SvPV_set(sv, (char*)entry->addr);
2102 14           SvCUR_set(sv, entry->len);
2103 14           SvLEN_set(sv, 0); /* Don't free this memory! */
2104 14           SvPOK_on(sv);
2105 14           SvREADONLY_on(sv);
2106              
2107 14           return sv;
2108             }
2109              
2110 16           static void file_mmap_close(IV idx) {
2111             dTHX;
2112 16 100         if (idx < 0 || idx >= g_mmaps_count) return;
    50          
2113              
2114 14           MmapEntry *entry = &g_mmaps[idx];
2115 14           entry->refcount--;
2116 14 50         if (entry->refcount <= 0) {
2117 14           free_mmap_slot(idx);
2118             }
2119             }
2120              
2121 2           static void file_mmap_sync(IV idx) {
2122             dTHX;
2123             MmapEntry *entry;
2124              
2125 2 50         if (idx < 0 || idx >= g_mmaps_count) return;
    50          
2126              
2127 2           entry = &g_mmaps[idx];
2128             #ifdef _WIN32
2129             if (entry->addr) {
2130             FlushViewOfFile(entry->addr, entry->len);
2131             }
2132             #else
2133 2 50         if (entry->addr && entry->addr != MAP_FAILED) {
    50          
2134 2           msync(entry->addr, entry->len, MS_SYNC);
2135             }
2136             #endif
2137             }
2138              
2139             /* ============================================
2140             Line iterator operations
2141             ============================================ */
2142              
2143 12           static void ensure_iters_capacity(IV needed) {
2144 12 50         if (needed >= g_iters_size) {
2145 0 0         IV new_size = g_iters_size ? g_iters_size * 2 : 16;
2146             IV i;
2147 0 0         while (new_size <= needed) new_size *= 2;
2148 0 0         Renew(g_iters, new_size, LineIterEntry);
2149 0 0         for (i = g_iters_size; i < new_size; i++) {
2150 0           g_iters[i].fd = -1;
2151 0           g_iters[i].buffer = NULL;
2152 0           g_iters[i].buf_size = 0;
2153 0           g_iters[i].buf_pos = 0;
2154 0           g_iters[i].buf_len = 0;
2155 0           g_iters[i].eof = 0;
2156 0           g_iters[i].refcount = 0;
2157 0           g_iters[i].path = NULL;
2158             }
2159 0           g_iters_size = new_size;
2160             }
2161 12           }
2162              
2163 73           static IV alloc_iter_slot(void) {
2164             IV idx;
2165              
2166 73 100         if (g_free_iters_count > 0) {
2167 61           return g_free_iters[--g_free_iters_count];
2168             }
2169              
2170 12           ensure_iters_capacity(g_iters_count);
2171 12           idx = g_iters_count++;
2172 12           return idx;
2173             }
2174              
2175 72           static void free_iter_slot(IV idx) {
2176             dTHX;
2177             LineIterEntry *entry;
2178              
2179 72 50         if (idx < 0 || idx >= g_iters_count) return;
    50          
2180              
2181 72           entry = &g_iters[idx];
2182 72 100         if (entry->fd >= 0) {
2183 70           close(entry->fd);
2184             }
2185 72 100         if (entry->buffer) {
2186 70           Safefree(entry->buffer);
2187             }
2188 72 100         if (entry->path) {
2189 70           Safefree(entry->path);
2190             }
2191 72 100         if (entry->records) {
2192 2           SvREFCNT_dec((SV *)entry->records);
2193             }
2194              
2195 72           entry->fd = -1;
2196 72           entry->buffer = NULL;
2197 72           entry->buf_size = 0;
2198 72           entry->buf_pos = 0;
2199 72           entry->buf_len = 0;
2200 72           entry->eof = 0;
2201 72           entry->refcount = 0;
2202 72           entry->path = NULL;
2203 72           entry->records = NULL;
2204 72           entry->records_idx = 0;
2205              
2206 72 50         if (g_free_iters_count >= g_free_iters_size) {
2207 0           g_free_iters_size *= 2;
2208 0 0         Renew(g_free_iters, g_free_iters_size, IV);
2209             }
2210 72           g_free_iters[g_free_iters_count++] = idx;
2211             }
2212              
2213 75           static IV file_lines_open(pTHX_ const char *path) {
2214             int fd;
2215             IV idx;
2216             LineIterEntry *entry;
2217             size_t path_len;
2218             #ifdef _WIN32
2219             int open_flags = O_RDONLY | O_BINARY;
2220             #else
2221 75           int open_flags = O_RDONLY;
2222             #endif
2223              
2224 75           fd = open(path, open_flags);
2225 75 100         if (fd < 0) {
2226 4           return -1;
2227             }
2228              
2229 71           idx = alloc_iter_slot();
2230 71           entry = &g_iters[idx];
2231              
2232 71           entry->fd = fd;
2233 71           entry->buf_size = FILE_BUFFER_SIZE;
2234 71           Newx(entry->buffer, entry->buf_size, char);
2235 71           entry->buf_pos = 0;
2236 71           entry->buf_len = 0;
2237 71           entry->eof = 0;
2238 71           entry->refcount = 1;
2239              
2240 71           path_len = strlen(path);
2241 71           Newx(entry->path, path_len + 1, char);
2242 71           memcpy(entry->path, path, path_len + 1);
2243              
2244             /* Byte-line mode: ensure record-mode fields stay NULL even if
2245             * alloc_iter_slot reused a slot whose previous owner was a
2246             * record-iter (free_iter_slot already clears them, but be explicit
2247             * - this was overlooked before the field was added). */
2248 71           entry->records = NULL;
2249 71           entry->records_idx = 0;
2250              
2251 71           return idx;
2252             }
2253              
2254 1395           static SV* file_lines_next(pTHX_ IV idx) {
2255             LineIterEntry *entry;
2256             char *line_start;
2257             char *newline;
2258             size_t line_len;
2259             SV *result;
2260             ssize_t n;
2261              
2262 1395 50         if (idx < 0 || idx >= g_iters_count) {
    50          
2263 0           return &PL_sv_undef;
2264             }
2265              
2266 1395           entry = &g_iters[idx];
2267 1395 50         if (entry->fd < 0) {
2268 0           return &PL_sv_undef;
2269             }
2270              
2271             while (1) {
2272             /* Look for newline in current buffer */
2273 1474 100         if (entry->buf_pos < entry->buf_len) {
2274 1365           line_start = entry->buffer + entry->buf_pos;
2275 1365           newline = memchr(line_start, '\n', entry->buf_len - entry->buf_pos);
2276              
2277 1365 100         if (newline) {
2278 1353           line_len = newline - line_start;
2279 1353           result = newSVpvn(line_start, line_len);
2280 1353           entry->buf_pos += line_len + 1;
2281 1353           return result;
2282             }
2283             }
2284              
2285             /* No newline found, need more data */
2286 121 100         if (entry->eof) {
2287             /* Return remaining data if any */
2288 42 100         if (entry->buf_pos < entry->buf_len) {
2289 6           line_len = entry->buf_len - entry->buf_pos;
2290 6           result = newSVpvn(entry->buffer + entry->buf_pos, line_len);
2291 6           entry->buf_pos = entry->buf_len;
2292 6           return result;
2293             }
2294 36           return &PL_sv_undef;
2295             }
2296              
2297             /* Move remaining data to start of buffer */
2298 79 100         if (entry->buf_pos > 0) {
2299 33           size_t remaining = entry->buf_len - entry->buf_pos;
2300 33 100         if (remaining > 0) {
2301 6           memmove(entry->buffer, entry->buffer + entry->buf_pos, remaining);
2302             }
2303 33           entry->buf_len = remaining;
2304 33           entry->buf_pos = 0;
2305             }
2306              
2307             /* Expand buffer if needed */
2308 79 50         if (entry->buf_len >= entry->buf_size - 1) {
2309 0           entry->buf_size *= 2;
2310 0           Renew(entry->buffer, entry->buf_size, char);
2311             }
2312              
2313             /* Read more data */
2314 79           n = read(entry->fd, entry->buffer + entry->buf_len,
2315 79           entry->buf_size - entry->buf_len - 1);
2316 79 50         if (n < 0) {
2317 0 0         if (errno == EINTR) continue;
2318 0           return &PL_sv_undef;
2319             }
2320 79 100         if (n == 0) {
2321 36           entry->eof = 1;
2322             } else {
2323 43           entry->buf_len += n;
2324             }
2325             }
2326             }
2327              
2328 0           static int file_lines_eof(IV idx) {
2329             dTHX;
2330             LineIterEntry *entry;
2331              
2332 0 0         if (idx < 0 || idx >= g_iters_count) {
    0          
2333 0           return 1;
2334             }
2335              
2336 0           entry = &g_iters[idx];
2337 0 0         return entry->eof && entry->buf_pos >= entry->buf_len;
    0          
2338             }
2339              
2340 73           static void file_lines_close(IV idx) {
2341             dTHX;
2342 73 100         if (idx < 0 || idx >= g_iters_count) return;
    50          
2343              
2344 72           LineIterEntry *entry = &g_iters[idx];
2345 72           entry->refcount--;
2346 72 50         if (entry->refcount <= 0) {
2347 72           free_iter_slot(idx);
2348             }
2349             }
2350              
2351             /* ============================================
2352             Fast stat operations
2353             ============================================ */
2354              
2355 65           static IV file_size_internal(const char *path) {
2356             dTHX;
2357             Stat_t st;
2358 65 100         if (cached_stat(path, &st) < 0) {
2359 2           return -1;
2360             }
2361 63           return st.st_size;
2362             }
2363              
2364 168           static int file_exists_internal(const char *path) {
2365             dTHX;
2366             Stat_t st;
2367 168           return cached_stat(path, &st) == 0;
2368             }
2369              
2370 33           static int file_is_file_internal(const char *path) {
2371             dTHX;
2372             Stat_t st;
2373 33 100         if (cached_stat(path, &st) < 0) return 0;
2374 31           return S_ISREG(st.st_mode);
2375             }
2376              
2377 38           static int file_is_dir_internal(const char *path) {
2378             dTHX;
2379             Stat_t st;
2380 38 100         if (cached_stat(path, &st) < 0) return 0;
2381 31           return S_ISDIR(st.st_mode);
2382             }
2383              
2384 9           static int file_is_readable_internal(const char *path) {
2385             dTHX;
2386 9           return file_is_readable_cached(path);
2387             }
2388              
2389 9           static int file_is_writable_internal(const char *path) {
2390             dTHX;
2391 9           return file_is_writable_cached(path);
2392             }
2393              
2394 31           static IV file_mtime_internal(const char *path) {
2395             dTHX;
2396             Stat_t st;
2397 31 100         if (cached_stat(path, &st) < 0) {
2398 1           return -1;
2399             }
2400 30           return st.st_mtime;
2401             }
2402              
2403 8           static IV file_atime_internal(const char *path) {
2404             dTHX;
2405             Stat_t st;
2406 8 100         if (cached_stat(path, &st) < 0) {
2407 1           return -1;
2408             }
2409 7           return st.st_atime;
2410             }
2411              
2412 8           static IV file_ctime_internal(const char *path) {
2413             dTHX;
2414             Stat_t st;
2415 8 100         if (cached_stat(path, &st) < 0) {
2416 1           return -1;
2417             }
2418 7           return st.st_ctime;
2419             }
2420              
2421 5           static IV file_mode_internal(const char *path) {
2422             dTHX;
2423             Stat_t st;
2424 5 100         if (cached_stat(path, &st) < 0) {
2425 1           return -1;
2426             }
2427 4           return st.st_mode & 07777; /* Return permission bits only */
2428             }
2429              
2430             /* Combined stat - returns all attributes in one syscall */
2431 4           static HV* file_stat_all_internal(pTHX_ const char *path) {
2432             Stat_t st;
2433             HV *result;
2434              
2435 4 100         if (cached_stat(path, &st) < 0) {
2436 1           return NULL;
2437             }
2438              
2439 3           result = newHV();
2440 3           hv_store(result, "size", 4, newSViv(st.st_size), 0);
2441 3           hv_store(result, "mtime", 5, newSViv(st.st_mtime), 0);
2442 3           hv_store(result, "atime", 5, newSViv(st.st_atime), 0);
2443 3           hv_store(result, "ctime", 5, newSViv(st.st_ctime), 0);
2444 3           hv_store(result, "mode", 4, newSViv(st.st_mode & 07777), 0);
2445 3 100         hv_store(result, "is_file", 7, S_ISREG(st.st_mode) ? &PL_sv_yes : &PL_sv_no, 0);
2446 3 100         hv_store(result, "is_dir", 6, S_ISDIR(st.st_mode) ? &PL_sv_yes : &PL_sv_no, 0);
2447 3           hv_store(result, "dev", 3, newSViv(st.st_dev), 0);
2448 3           hv_store(result, "ino", 3, newSViv(st.st_ino), 0);
2449 3           hv_store(result, "nlink", 5, newSViv(st.st_nlink), 0);
2450 3           hv_store(result, "uid", 3, newSViv(st.st_uid), 0);
2451 3           hv_store(result, "gid", 3, newSViv(st.st_gid), 0);
2452              
2453 3           return result;
2454             }
2455              
2456 10           static int file_is_link_internal(const char *path) {
2457             dTHX;
2458             #ifdef _WIN32
2459             /* Windows: check for reparse point */
2460             DWORD attrs = GetFileAttributesA(path);
2461             if (attrs == INVALID_FILE_ATTRIBUTES) return 0;
2462             return (attrs & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
2463             #else
2464             Stat_t st;
2465 10 100         if (lstat(path, &st) < 0) return 0;
2466 9           return S_ISLNK(st.st_mode);
2467             #endif
2468             }
2469              
2470 8           static int file_is_executable_internal(const char *path) {
2471             dTHX;
2472             #ifdef _WIN32
2473             /* Windows: check file extension */
2474             const char *ext = strrchr(path, '.');
2475             if (ext) {
2476             if (_stricmp(ext, ".exe") == 0 || _stricmp(ext, ".bat") == 0 ||
2477             _stricmp(ext, ".cmd") == 0 || _stricmp(ext, ".com") == 0) {
2478             return 1;
2479             }
2480             }
2481             return 0;
2482             #else
2483 8           return file_is_executable_cached(path);
2484             #endif
2485             }
2486              
2487             /* ============================================
2488             File manipulation operations
2489             ============================================ */
2490              
2491 6           static int file_unlink_internal(const char *path) {
2492             dTHX;
2493             int result;
2494             #ifdef _WIN32
2495             result = _unlink(path) == 0;
2496             #else
2497 6           result = unlink(path) == 0;
2498             #endif
2499             /* Invalidate cache if this path was cached */
2500 6 100         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    50          
2501 2           g_stat_cache.valid = 0;
2502             }
2503 6           return result;
2504             }
2505              
2506 4           static int file_copy_internal(pTHX_ const char *src, const char *dst) {
2507             #if defined(__APPLE__)
2508             /* macOS: Use native copyfile() for best performance and metadata */
2509             Stat_t st;
2510             int result;
2511             if (stat(src, &st) < 0) return 0;
2512             result = copyfile(src, dst, NULL, COPYFILE_DATA) == 0;
2513             if (result && g_stat_cache.valid && strcmp(dst, g_stat_cache.path) == 0) {
2514             g_stat_cache.valid = 0;
2515             }
2516             return result;
2517             #elif defined(__linux__)
2518             /* Linux: Use sendfile() for zero-copy transfer */
2519             int fd_src, fd_dst;
2520             Stat_t st;
2521 4           off_t offset = 0;
2522             ssize_t sent;
2523              
2524 4           fd_src = open(src, O_RDONLY);
2525 4 100         if (fd_src < 0) return 0;
2526              
2527 3 50         if (fstat(fd_src, &st) < 0 || !S_ISREG(st.st_mode)) {
    50          
2528 0           close(fd_src);
2529 0           return 0;
2530             }
2531              
2532 3           fd_dst = file_open3(dst, O_WRONLY | O_CREAT | O_TRUNC, st.st_mode & 07777);
2533 3 50         if (fd_dst < 0) {
2534 0           close(fd_src);
2535 0           return 0;
2536             }
2537              
2538             /* sendfile() for zero-copy - much faster than read/write */
2539 5 100         while (offset < st.st_size) {
2540 2           sent = sendfile(fd_dst, fd_src, &offset, st.st_size - offset);
2541 2 50         if (sent < 0) {
2542 0 0         if (errno == EINTR) continue;
2543 0 0         if (errno == EINVAL || errno == ENOSYS) {
    0          
2544             /* sendfile not supported, fallback to read/write */
2545             char *buffer;
2546             ssize_t n_read, n_written, written;
2547 0           int result = 0;
2548              
2549             /* Reposition to where we left off */
2550 0           lseek(fd_src, offset, SEEK_SET);
2551              
2552 0           Newx(buffer, FILE_BULK_BUFFER_SIZE, char);
2553             while (1) {
2554 0           n_read = read(fd_src, buffer, FILE_BULK_BUFFER_SIZE);
2555 0 0         if (n_read < 0) {
2556 0 0         if (errno == EINTR) continue;
2557 0           break;
2558             }
2559 0 0         if (n_read == 0) { result = 1; break; }
2560              
2561 0           written = 0;
2562 0 0         while (written < n_read) {
2563 0           n_written = write(fd_dst, buffer + written, n_read - written);
2564 0 0         if (n_written < 0) {
2565 0 0         if (errno == EINTR) continue;
2566 0           goto fallback_cleanup;
2567             }
2568 0           written += n_written;
2569             }
2570             }
2571 0           fallback_cleanup:
2572 0           Safefree(buffer);
2573 0           close(fd_src);
2574 0           close(fd_dst);
2575 0 0         if (result && g_stat_cache.valid && strcmp(dst, g_stat_cache.path) == 0) {
    0          
    0          
2576 0           g_stat_cache.valid = 0;
2577             }
2578 0           return result;
2579             }
2580 0           close(fd_src);
2581 0           close(fd_dst);
2582 0           return 0;
2583             }
2584 2 50         if (sent == 0) break;
2585             }
2586              
2587 3           close(fd_src);
2588 3           close(fd_dst);
2589             /* Invalidate cache for dst */
2590 3 100         if (g_stat_cache.valid && strcmp(dst, g_stat_cache.path) == 0) {
    50          
2591 0           g_stat_cache.valid = 0;
2592             }
2593 3           return 1;
2594             #else
2595             /* Portable fallback: read/write loop */
2596             int fd_src, fd_dst;
2597             char *buffer;
2598             ssize_t n_read, n_written, written;
2599             Stat_t st;
2600             int result = 0;
2601             #ifdef _WIN32
2602             int open_flags_r = O_RDONLY | O_BINARY;
2603             int open_flags_w = O_WRONLY | O_CREAT | O_TRUNC | O_BINARY;
2604             #else
2605             int open_flags_r = O_RDONLY;
2606             int open_flags_w = O_WRONLY | O_CREAT | O_TRUNC;
2607             #endif
2608              
2609             fd_src = open(src, open_flags_r);
2610             if (fd_src < 0) return 0;
2611              
2612             if (fstat(fd_src, &st) < 0) {
2613             close(fd_src);
2614             return 0;
2615             }
2616              
2617             fd_dst = file_open3(dst, open_flags_w, st.st_mode & 07777);
2618             if (fd_dst < 0) {
2619             close(fd_src);
2620             return 0;
2621             }
2622              
2623             Newx(buffer, FILE_BULK_BUFFER_SIZE, char);
2624              
2625             while (1) {
2626             n_read = read(fd_src, buffer, FILE_BULK_BUFFER_SIZE);
2627             if (n_read < 0) {
2628             if (errno == EINTR) continue;
2629             goto cleanup;
2630             }
2631             if (n_read == 0) break;
2632              
2633             written = 0;
2634             while (written < n_read) {
2635             n_written = write(fd_dst, buffer + written, n_read - written);
2636             if (n_written < 0) {
2637             if (errno == EINTR) continue;
2638             goto cleanup;
2639             }
2640             written += n_written;
2641             }
2642             }
2643              
2644             result = 1;
2645              
2646             cleanup:
2647             Safefree(buffer);
2648             close(fd_src);
2649             close(fd_dst);
2650             if (result && g_stat_cache.valid && strcmp(dst, g_stat_cache.path) == 0) {
2651             g_stat_cache.valid = 0;
2652             }
2653             return result;
2654             #endif
2655             }
2656              
2657 3           static int file_move_internal(pTHX_ const char *src, const char *dst) {
2658             int result;
2659            
2660             /* Try rename first (fast path for same filesystem) */
2661 3 100         if (rename(src, dst) == 0) {
2662 2           result = 1;
2663             }
2664             /* If EXDEV, copy then delete (cross-device move) */
2665 1 50         else if (errno == EXDEV) {
2666 0 0         if (file_copy_internal(aTHX_ src, dst)) {
2667 0           result = file_unlink_internal(src);
2668             } else {
2669 0           return 0;
2670             }
2671             } else {
2672 1           return 0;
2673             }
2674            
2675             /* Invalidate cache for both paths */
2676 2 100         if (g_stat_cache.valid) {
2677 1 50         if (strcmp(src, g_stat_cache.path) == 0 || strcmp(dst, g_stat_cache.path) == 0) {
    50          
2678 0           g_stat_cache.valid = 0;
2679             }
2680             }
2681 2           return result;
2682             }
2683              
2684 3           static int file_touch_internal(const char *path) {
2685             dTHX;
2686             int result;
2687             #ifdef _WIN32
2688             HANDLE h;
2689             FILETIME ft;
2690             SYSTEMTIME st;
2691             result = 0;
2692              
2693             /* Try to open existing file */
2694             h = CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
2695             NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
2696             if (h == INVALID_HANDLE_VALUE) {
2697             return 0;
2698             }
2699              
2700             GetSystemTime(&st);
2701             SystemTimeToFileTime(&st, &ft);
2702             result = SetFileTime(h, NULL, &ft, &ft) != 0;
2703             CloseHandle(h);
2704             #else
2705             int fd;
2706             /* Try to update times on existing file - utime(path, NULL) sets to current time */
2707 3 100         if (utime(path, NULL) == 0) {
2708 1           result = 1;
2709             } else {
2710             /* File doesn't exist, create it */
2711 2           fd = file_open3(path, O_WRONLY | O_CREAT, 0644);
2712 2 50         if (fd < 0) {
2713 0           return 0;
2714             }
2715 2           close(fd);
2716 2           result = 1;
2717             }
2718             #endif
2719             /* Invalidate cache if this path was cached */
2720 3 100         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    100          
2721 1           g_stat_cache.valid = 0;
2722             }
2723 3           return result;
2724             }
2725              
2726 1           static int file_chmod_internal(const char *path, int mode) {
2727             dTHX;
2728             int result;
2729             #ifdef _WIN32
2730             result = _chmod(path, mode) == 0;
2731             #else
2732 1           result = chmod(path, mode) == 0;
2733             #endif
2734             /* Invalidate cache if this path was cached */
2735 1 50         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    50          
2736 0           g_stat_cache.valid = 0;
2737             }
2738 1           return result;
2739             }
2740              
2741 18           static int file_mkdir_internal(const char *path, int mode) {
2742             dTHX;
2743             int result;
2744             #ifdef _WIN32
2745             PERL_UNUSED_VAR(mode);
2746             result = _mkdir(path) == 0;
2747             #else
2748 18           result = mkdir(path, mode) == 0;
2749             #endif
2750             /* Invalidate cache if this path was cached */
2751 18 100         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    100          
2752 1           g_stat_cache.valid = 0;
2753             }
2754 18           return result;
2755             }
2756              
2757 9           static int file_rmdir_internal(const char *path) {
2758             dTHX;
2759             int result;
2760             #ifdef _WIN32
2761             result = _rmdir(path) == 0;
2762             #else
2763 9           result = rmdir(path) == 0;
2764             #endif
2765             /* Invalidate cache if this path was cached */
2766 9 100         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    50          
2767 2           g_stat_cache.valid = 0;
2768             }
2769 9           return result;
2770             }
2771              
2772             /* ============================================
2773             Directory listing
2774             ============================================ */
2775              
2776 7           static AV* file_readdir_internal(pTHX_ const char *path) {
2777 7           AV *result = newAV();
2778              
2779             #ifdef _WIN32
2780             WIN32_FIND_DATAA fd;
2781             HANDLE h;
2782             char pattern[MAX_PATH];
2783             size_t len = strlen(path);
2784              
2785             if (len + 3 > MAX_PATH) return result;
2786              
2787             memcpy(pattern, path, len);
2788             if (len > 0 && path[len-1] != '\\' && path[len-1] != '/') {
2789             pattern[len++] = '\\';
2790             }
2791             pattern[len++] = '*';
2792             pattern[len] = '\0';
2793              
2794             h = FindFirstFileA(pattern, &fd);
2795             if (h == INVALID_HANDLE_VALUE) return result;
2796              
2797             do {
2798             /* Skip . and .. */
2799             if (strcmp(fd.cFileName, ".") != 0 && strcmp(fd.cFileName, "..") != 0) {
2800             av_push(result, newSVpv(fd.cFileName, 0));
2801             }
2802             } while (FindNextFileA(h, &fd));
2803              
2804             FindClose(h);
2805             #else
2806             DIR *dir;
2807             struct dirent *entry;
2808              
2809 7           dir = opendir(path);
2810 7 100         if (!dir) return result;
2811              
2812 32 100         while ((entry = readdir(dir)) != NULL) {
2813             /* Skip . and .. */
2814 27 100         if (strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) {
    100          
2815 17           av_push(result, newSVpv(entry->d_name, 0));
2816             }
2817             }
2818              
2819 5           closedir(dir);
2820             #endif
2821              
2822 5           return result;
2823             }
2824              
2825             /* ============================================
2826             Path manipulation
2827             ============================================ */
2828              
2829 83           static SV* file_basename_internal(pTHX_ const char *path) {
2830             const char *p;
2831 83           size_t len = strlen(path);
2832              
2833 83 100         if (len == 0) return newSVpvs("");
2834              
2835             /* Skip trailing slashes */
2836 88 100         while (len > 0 && (path[len-1] == '/' || path[len-1] == '\\')) {
    100          
    50          
2837 7           len--;
2838             }
2839 81 100         if (len == 0) return newSVpvs("");
2840              
2841             /* Find last separator */
2842 78           p = path + len - 1;
2843 735 100         while (p > path && *p != '/' && *p != '\\') {
    100          
    50          
2844 657           p--;
2845             }
2846 78 100         if (*p == '/' || *p == '\\') p++;
    50          
2847              
2848 78           return newSVpvn(p, (path + len) - p);
2849             }
2850              
2851 20           static SV* file_dirname_internal(pTHX_ const char *path) {
2852             const char *end;
2853 20           size_t len = strlen(path);
2854              
2855 20 100         if (len == 0) return newSVpvs(".");
2856              
2857             /* Skip trailing slashes */
2858 19           end = path + len - 1;
2859 21 100         while (end > path && (*end == '/' || *end == '\\')) {
    100          
    50          
2860 2           end--;
2861             }
2862              
2863             /* Find last separator */
2864 137 100         while (end > path && *end != '/' && *end != '\\') {
    100          
    50          
2865 118           end--;
2866             }
2867              
2868 19 100         if (end == path) {
2869 6 100         if (*end == '/' || *end == '\\') {
    50          
2870 4           return newSVpvn(path, 1);
2871             }
2872 2           return newSVpvs(".");
2873             }
2874              
2875             /* Skip multiple trailing slashes in dirname */
2876 14 50         while (end > path && (*(end-1) == '/' || *(end-1) == '\\')) {
    100          
    50          
2877 1           end--;
2878             }
2879              
2880 13           return newSVpvn(path, end - path);
2881             }
2882              
2883 49           static SV* file_extname_internal(pTHX_ const char *path) {
2884             const char *dot;
2885             const char *basename;
2886 49           size_t len = strlen(path);
2887              
2888 49 100         if (len == 0) return newSVpvs("");
2889              
2890             /* Find basename first */
2891 48           basename = path + len - 1;
2892 452 100         while (basename > path && *basename != '/' && *basename != '\\') {
    100          
    50          
2893 404           basename--;
2894             }
2895 48 100         if (*basename == '/' || *basename == '\\') basename++;
    50          
2896              
2897             /* Find last dot in basename */
2898 48           dot = strrchr(basename, '.');
2899 48 100         if (!dot || dot == basename) return newSVpvs("");
    100          
2900              
2901 42           return newSVpv(dot, 0);
2902             }
2903              
2904 12           static SV* file_join_internal(pTHX_ AV *parts) {
2905             SV *result;
2906             SSize_t i, len;
2907 12           STRLEN total_len = 0;
2908             char *buf, *p;
2909             int need_sep;
2910              
2911 12           len = av_len(parts) + 1;
2912 12 50         if (len == 0) return newSVpvs("");
2913              
2914             /* Calculate total length */
2915 40 100         for (i = 0; i < len; i++) {
2916 28           SV **sv = av_fetch(parts, i, 0);
2917 28 50         if (sv && SvPOK(*sv)) {
    50          
2918 28           total_len += SvCUR(*sv) + 1; /* +1 for separator */
2919             }
2920             }
2921              
2922 12           result = newSV(total_len + 1);
2923 12           SvPOK_on(result);
2924 12           buf = SvPVX(result);
2925 12           p = buf;
2926 12           need_sep = 0;
2927              
2928 40 100         for (i = 0; i < len; i++) {
2929 28           SV **sv = av_fetch(parts, i, 0);
2930 28 50         if (sv && SvPOK(*sv)) {
    50          
2931             STRLEN part_len;
2932 28           const char *part = SvPV(*sv, part_len);
2933              
2934 28 100         if (part_len == 0) continue;
2935              
2936             /* Skip leading separator if we already have one */
2937 29 50         while (part_len > 0 && (*part == '/' || *part == '\\')) {
    100          
    50          
2938 9 50         if (!need_sep && p == buf) break; /* Keep root slash */
    100          
2939 2           part++;
2940 2           part_len--;
2941             }
2942              
2943 27 100         if (need_sep && part_len > 0) {
    50          
2944             #ifdef _WIN32
2945             *p++ = '\\';
2946             #else
2947 12           *p++ = '/';
2948             #endif
2949             }
2950              
2951 27 50         if (part_len > 0) {
2952 27           memcpy(p, part, part_len);
2953 27           p += part_len;
2954              
2955             /* Check if ends with separator */
2956 27 100         need_sep = (*(p-1) != '/' && *(p-1) != '\\');
    50          
2957             }
2958             }
2959             }
2960              
2961 12           *p = '\0';
2962 12           SvCUR_set(result, p - buf);
2963 12           return result;
2964             }
2965              
2966             /* ============================================
2967             Head and Tail operations
2968             ============================================ */
2969              
2970 6           static AV* file_head_internal(pTHX_ const char *path, IV n) {
2971 6           AV *result = newAV();
2972             IV idx;
2973             SV *line;
2974 6           IV count = 0;
2975              
2976 6 100         if (n <= 0) return result;
2977              
2978 5           idx = file_lines_open(aTHX_ path);
2979 5 100         if (idx < 0) return result;
2980              
2981 22 100         while (count < n && (line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
    100          
2982 18           av_push(result, line);
2983 18           count++;
2984             }
2985              
2986 4           file_lines_close(idx);
2987 4           return result;
2988             }
2989              
2990 6           static AV* file_tail_internal(pTHX_ const char *path, IV n) {
2991 6           AV *result = newAV();
2992             AV *buffer;
2993             SV *line;
2994             IV idx;
2995             SSize_t i, buf_len;
2996              
2997 6 100         if (n <= 0) return result;
2998              
2999 5           idx = file_lines_open(aTHX_ path);
3000 5 100         if (idx < 0) return result;
3001              
3002             /* Use circular buffer to keep last N lines */
3003 4           buffer = newAV();
3004 4           av_extend(buffer, n - 1);
3005              
3006 47 100         while ((line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
3007 43 100         if (av_len(buffer) + 1 >= n) {
3008 25           SV *old = av_shift(buffer);
3009 25           SvREFCNT_dec(old);
3010             }
3011 43           av_push(buffer, line);
3012             }
3013              
3014 4           file_lines_close(idx);
3015              
3016             /* Copy buffer to result */
3017 4           buf_len = av_len(buffer) + 1;
3018 22 100         for (i = 0; i < buf_len; i++) {
3019 18           SV **sv = av_fetch(buffer, i, 0);
3020 18 50         if (sv) {
3021 18           av_push(result, newSVsv(*sv));
3022             }
3023             }
3024              
3025 4           SvREFCNT_dec((SV*)buffer);
3026 4           return result;
3027             }
3028              
3029             /* range_lines: 1-based, half-open in count style.
3030             * range_lines($p, 1, 10) -> first 10 lines (same as head($p, 10))
3031             * range_lines($p, 5, 3) -> lines 5, 6, 7
3032             * If `from` is past EOF or `count <= 0`, returns an empty AV (no error).
3033             * `from < 1` is also treated as empty (caller error - documented).
3034             *
3035             * Implementation: skip-then-take using the existing line iterator. SVs
3036             * for the skipped lines are allocated and immediately freed; that's
3037             * O(skip) cheap work but bounded by line size, not file size. For very
3038             * large skips (millions of lines) a buffer-scan-without-allocation
3039             * variant would help; deferred until benchmarks demand it. */
3040 9           static AV* file_range_internal(pTHX_ const char *path, IV from, IV count) {
3041 9           AV *result = newAV();
3042             IV idx, i;
3043             SV *line;
3044              
3045 9 100         if (count <= 0 || from < 1) return result;
    100          
3046              
3047 5           idx = file_lines_open(aTHX_ path);
3048 5 50         if (idx < 0) return result;
3049              
3050             /* Skip lines 1 .. from-1 */
3051 60 100         for (i = 0; i < from - 1; i++) {
3052 56           line = file_lines_next(aTHX_ idx);
3053 56 100         if (line == &PL_sv_undef) {
3054 1           file_lines_close(idx);
3055 1           return result;
3056             }
3057 55           SvREFCNT_dec(line);
3058             }
3059              
3060             /* Take `count` lines starting at line `from` */
3061 4           av_extend(result, count - 1);
3062 19 100         for (i = 0; i < count; i++) {
3063 16           line = file_lines_next(aTHX_ idx);
3064 16 100         if (line == &PL_sv_undef) break;
3065 15           av_push(result, line);
3066             }
3067              
3068 4           file_lines_close(idx);
3069 4           return result;
3070             }
3071              
3072             /* ============================================
3073             Atomic spew - write to temp file then rename
3074             ============================================ */
3075              
3076 17           static int file_atomic_spew_internal(pTHX_ const char *path, SV *data) {
3077             char temp_path[4096];
3078             int fd;
3079             const char *buf;
3080             STRLEN len;
3081 17           ssize_t written = 0, n;
3082             static int counter = 0;
3083             #ifdef _WIN32
3084             int open_flags = O_WRONLY | O_CREAT | O_TRUNC | O_BINARY;
3085             int pid = (int)GetCurrentProcessId();
3086             #else
3087 17           int open_flags = O_WRONLY | O_CREAT | O_TRUNC;
3088 17           int pid = (int)getpid();
3089             #endif
3090              
3091             /* Create temp file name in same directory */
3092 17           snprintf(temp_path, sizeof(temp_path), "%s.tmp.%d.%d", path, pid, counter++);
3093              
3094 17           buf = SvPV(data, len);
3095              
3096 17           fd = file_open3(temp_path, open_flags, 0644);
3097 17 50         if (fd < 0) {
3098 0           return 0;
3099             }
3100              
3101 33 100         while ((size_t)written < len) {
3102 16           n = write(fd, buf + written, len - written);
3103 16 50         if (n < 0) {
3104 0 0         if (errno == EINTR) continue;
3105 0           close(fd);
3106 0           file_unlink_internal(temp_path);
3107 0           return 0;
3108             }
3109 16           written += n;
3110             }
3111              
3112             #ifdef _WIN32
3113             /* Sync to disk on Windows */
3114             _commit(fd);
3115             #else
3116             /* Sync to disk on POSIX */
3117 17           fsync(fd);
3118             #endif
3119              
3120 17           close(fd);
3121              
3122             /* Atomic rename */
3123 17 50         if (rename(temp_path, path) != 0) {
3124 0           file_unlink_internal(temp_path);
3125 0           return 0;
3126             }
3127              
3128             /* Invalidate cache for this path */
3129 17 100         if (g_stat_cache.valid && strcmp(path, g_stat_cache.path) == 0) {
    50          
3130 0           g_stat_cache.valid = 0;
3131             }
3132 17           return 1;
3133             }
3134              
3135             /* ============================================
3136             Split lines utility
3137             ============================================ */
3138              
3139 6           static AV* file_split_lines(pTHX_ SV *content) {
3140             AV *lines;
3141             const char *start, *end, *p;
3142             STRLEN len;
3143              
3144 6           start = SvPV(content, len);
3145 6           end = start + len;
3146 6           lines = newAV();
3147              
3148 16 100         while (start < end) {
3149 14           p = memchr(start, '\n', end - start);
3150 14 100         if (p) {
3151 10           av_push(lines, newSVpvn(start, p - start));
3152 10           start = p + 1;
3153             } else {
3154 4 50         if (start < end) {
3155 4           av_push(lines, newSVpvn(start, end - start));
3156             }
3157 4           break;
3158             }
3159             }
3160              
3161 6           return lines;
3162             }
3163              
3164             /* ============================================
3165             XS Functions
3166             ============================================ */
3167              
3168 46           XS_INTERNAL(xs_slurp) {
3169 46           dXSARGS;
3170             const char *path;
3171             SV *bytes;
3172              
3173 46 50         if (items < 1)
3174 0           croak("Usage: file::slurp(path [, plugin => ..., key => value ...])");
3175              
3176 46           path = SvPV_nolen(ST(0));
3177 46           bytes = file_slurp_internal(aTHX_ path);
3178              
3179 46 100         if (items > 1) {
3180 17           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 1, items, "slurp");
3181 15           SV *out = file_plugin_dispatch_read(aTHX_ opts, path, bytes);
3182 13           SvREFCNT_dec((SV *)opts);
3183 13 100         if (!out) {
3184 2           SvREFCNT_dec(bytes);
3185 2           ST(0) = &PL_sv_undef;
3186 2           XSRETURN(1);
3187             }
3188 11 50         if (out != bytes) {
3189 11           SvREFCNT_dec(bytes);
3190 11           bytes = out;
3191             }
3192             }
3193              
3194 40           ST(0) = sv_2mortal(bytes);
3195 40           XSRETURN(1);
3196             }
3197              
3198 2           XS_INTERNAL(xs_slurp_raw) {
3199 2           dXSARGS;
3200             const char *path;
3201              
3202 2 50         if (items != 1) croak("Usage: file::slurp_raw(path)");
3203              
3204 2           path = SvPV_nolen(ST(0));
3205 2           ST(0) = sv_2mortal(file_slurp_raw(aTHX_ path));
3206 2           XSRETURN(1);
3207             }
3208              
3209 94           XS_INTERNAL(xs_spew) {
3210 94           dXSARGS;
3211             const char *path;
3212             SV *payload;
3213 94           SV *bytes_to_write = NULL;
3214              
3215 94 50         if (items < 2)
3216 0           croak("Usage: file::spew(path, data [, plugin => ..., key => value ...])");
3217              
3218 94           path = SvPV_nolen(ST(0));
3219 94           payload = ST(1);
3220              
3221 94 100         if (items > 2) {
3222 9           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "spew");
3223 9           bytes_to_write = file_plugin_dispatch_write(aTHX_ opts, path, payload);
3224 7           SvREFCNT_dec((SV *)opts);
3225 7 100         if (!bytes_to_write) {
3226 2           ST(0) = &PL_sv_no;
3227 2           XSRETURN(1);
3228             }
3229 5           payload = sv_2mortal(bytes_to_write);
3230             }
3231              
3232 90 50         ST(0) = file_spew_internal(aTHX_ path, payload) ? &PL_sv_yes : &PL_sv_no;
3233 90           XSRETURN(1);
3234             }
3235              
3236 5           XS_INTERNAL(xs_append) {
3237 5           dXSARGS;
3238             const char *path;
3239             SV *payload;
3240 5           SV *bytes_to_write = NULL;
3241              
3242 5 50         if (items < 2)
3243 0           croak("Usage: file::append(path, data [, plugin => ..., key => value ...])");
3244              
3245 5           path = SvPV_nolen(ST(0));
3246 5           payload = ST(1);
3247              
3248 5 100         if (items > 2) {
3249 3           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "append");
3250 3           bytes_to_write = file_plugin_dispatch_write(aTHX_ opts, path, payload);
3251 3           SvREFCNT_dec((SV *)opts);
3252 3 50         if (!bytes_to_write) {
3253 0           ST(0) = &PL_sv_no;
3254 0           XSRETURN(1);
3255             }
3256 3           payload = sv_2mortal(bytes_to_write);
3257             }
3258              
3259 5 50         ST(0) = file_append_internal(aTHX_ path, payload) ? &PL_sv_yes : &PL_sv_no;
3260 5           XSRETURN(1);
3261             }
3262              
3263 11           XS_INTERNAL(xs_size) {
3264 11           dXSARGS;
3265             const char *path;
3266             IV size;
3267              
3268 11 50         if (items != 1) croak("Usage: file::size(path)");
3269              
3270 11           path = SvPV_nolen(ST(0));
3271 11           size = file_size_internal(path);
3272 11           ST(0) = sv_2mortal(newSViv(size));
3273 11           XSRETURN(1);
3274             }
3275              
3276 4           XS_INTERNAL(xs_mtime) {
3277 4           dXSARGS;
3278             const char *path;
3279             IV mtime;
3280              
3281 4 50         if (items != 1) croak("Usage: file::mtime(path)");
3282              
3283 4           path = SvPV_nolen(ST(0));
3284 4           mtime = file_mtime_internal(path);
3285 4           ST(0) = sv_2mortal(newSViv(mtime));
3286 4           XSRETURN(1);
3287             }
3288              
3289 21           XS_INTERNAL(xs_exists) {
3290 21           dXSARGS;
3291             const char *path;
3292              
3293 21 50         if (items != 1) croak("Usage: file::exists(path)");
3294              
3295 21           path = SvPV_nolen(ST(0));
3296 21 100         ST(0) = file_exists_internal(path) ? &PL_sv_yes : &PL_sv_no;
3297 21           XSRETURN(1);
3298             }
3299              
3300 4           XS_INTERNAL(xs_is_file) {
3301 4           dXSARGS;
3302             const char *path;
3303              
3304 4 50         if (items != 1) croak("Usage: file::is_file(path)");
3305              
3306 4           path = SvPV_nolen(ST(0));
3307 4 100         ST(0) = file_is_file_internal(path) ? &PL_sv_yes : &PL_sv_no;
3308 4           XSRETURN(1);
3309             }
3310              
3311 7           XS_INTERNAL(xs_is_dir) {
3312 7           dXSARGS;
3313             const char *path;
3314              
3315 7 50         if (items != 1) croak("Usage: file::is_dir(path)");
3316              
3317 7           path = SvPV_nolen(ST(0));
3318 7 100         ST(0) = file_is_dir_internal(path) ? &PL_sv_yes : &PL_sv_no;
3319 7           XSRETURN(1);
3320             }
3321              
3322 2           XS_INTERNAL(xs_is_readable) {
3323 2           dXSARGS;
3324             const char *path;
3325              
3326 2 50         if (items != 1) croak("Usage: file::is_readable(path)");
3327              
3328 2           path = SvPV_nolen(ST(0));
3329 2 100         ST(0) = file_is_readable_internal(path) ? &PL_sv_yes : &PL_sv_no;
3330 2           XSRETURN(1);
3331             }
3332              
3333 3           XS_INTERNAL(xs_is_writable) {
3334 3           dXSARGS;
3335             const char *path;
3336              
3337 3 50         if (items != 1) croak("Usage: file::is_writable(path)");
3338              
3339 3           path = SvPV_nolen(ST(0));
3340 3 100         ST(0) = file_is_writable_internal(path) ? &PL_sv_yes : &PL_sv_no;
3341 3           XSRETURN(1);
3342             }
3343              
3344 9           XS_INTERNAL(xs_lines) {
3345 9           dXSARGS;
3346             const char *path;
3347             AV *lines;
3348             int fd;
3349             Stat_t st;
3350             char *buffer;
3351             char *p, *end, *line_start;
3352             size_t file_size;
3353             ssize_t total_read, n;
3354             #ifdef _WIN32
3355             int open_flags = O_RDONLY | O_BINARY;
3356             #else
3357 9           int open_flags = O_RDONLY;
3358             #endif
3359              
3360 9 50         if (items < 1)
3361 0           croak("Usage: file::lines(path [, plugin => ..., key => value ...])");
3362              
3363 9           path = SvPV_nolen(ST(0));
3364              
3365             /* Plugin path: route the slurp through plugin READ. If the plugin
3366             * returns an arrayref we hand that back unchanged (each element is
3367             * a record). If it returns bytes we fall through to the byte-split
3368             * helper below by stashing them in `buffer`. */
3369 9 100         if (items > 1) {
3370 2           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 1, items, "lines");
3371 2           SV *bytes = file_slurp_internal(aTHX_ path);
3372 2           SV *out = file_plugin_dispatch_read(aTHX_ opts, path, bytes);
3373 2           SvREFCNT_dec((SV *)opts);
3374 2 50         if (!out) {
3375 0           SvREFCNT_dec(bytes);
3376 0           ST(0) = sv_2mortal(newRV_noinc((SV *)newAV()));
3377 0           XSRETURN(1);
3378             }
3379 2 50         if (out != bytes) SvREFCNT_dec(bytes);
3380 2 100         if (SvROK(out) && SvTYPE(SvRV(out)) == SVt_PVAV) {
    50          
3381 1           ST(0) = sv_2mortal(out);
3382 1           XSRETURN(1);
3383             }
3384             /* Plugin returned bytes - reuse the byte-split path below. */
3385             {
3386             STRLEN len;
3387 1           const char *pv = SvPV(out, len);
3388 1           AV *result = newAV();
3389 1           const char *cursor = pv;
3390 1           const char *bend = pv + len;
3391             const char *nl;
3392 1           av_extend(result, len / 40);
3393 3 50         while (cursor < bend) {
3394 3           nl = (const char *)memchr(cursor, '\n', bend - cursor);
3395 3 100         if (nl) {
3396 2           av_push(result, newSVpvn(cursor, nl - cursor));
3397 2           cursor = nl + 1;
3398             } else {
3399 1 50         if (cursor < bend)
3400 1           av_push(result, newSVpvn(cursor, bend - cursor));
3401 1           break;
3402             }
3403             }
3404 1           SvREFCNT_dec(out);
3405 1           ST(0) = sv_2mortal(newRV_noinc((SV *)result));
3406 1           XSRETURN(1);
3407             }
3408             }
3409              
3410 7           fd = open(path, open_flags);
3411 7 100         if (UNLIKELY(fd < 0)) {
3412 1           ST(0) = sv_2mortal(newRV_noinc((SV*)newAV()));
3413 1           XSRETURN(1);
3414             }
3415              
3416             /* Get file size for single-read optimization */
3417 6 50         if (UNLIKELY(fstat(fd, &st) < 0 || st.st_size == 0)) {
    100          
3418 1           close(fd);
3419 1           ST(0) = sv_2mortal(newRV_noinc((SV*)newAV()));
3420 1           XSRETURN(1);
3421             }
3422              
3423 5           file_size = st.st_size;
3424 5           Newx(buffer, file_size + 1, char);
3425              
3426             /* Read entire file in one syscall when possible */
3427 5           total_read = 0;
3428 10 100         while ((size_t)total_read < file_size) {
3429 5           n = read(fd, buffer + total_read, file_size - total_read);
3430 5 50         if (UNLIKELY(n < 0)) {
3431 0 0         if (errno == EINTR) continue;
3432 0           break;
3433             }
3434 5 50         if (n == 0) break;
3435 5           total_read += n;
3436             }
3437 5           close(fd);
3438              
3439 5 50         if (UNLIKELY(total_read == 0)) {
3440 0           Safefree(buffer);
3441 0           ST(0) = sv_2mortal(newRV_noinc((SV*)newAV()));
3442 0           XSRETURN(1);
3443             }
3444              
3445 5           lines = newAV();
3446             /* Pre-extend array based on estimated lines */
3447 5           av_extend(lines, total_read / 40);
3448              
3449             /* Single scan through buffer - no memmove, no buffer resize */
3450 5           line_start = buffer;
3451 5           end = buffer + total_read;
3452 5           p = buffer;
3453              
3454 15 50         while (p < end) {
3455 15           p = memchr(p, '\n', end - p);
3456 15 100         if (LIKELY(p != NULL)) {
3457 10           av_push(lines, newSVpvn(line_start, p - line_start));
3458 10           p++;
3459 10           line_start = p;
3460             } else {
3461             /* Last line without trailing newline */
3462 5 50         if (line_start < end) {
3463 5           av_push(lines, newSVpvn(line_start, end - line_start));
3464             }
3465 5           break;
3466             }
3467             }
3468              
3469 5           Safefree(buffer);
3470              
3471 5           ST(0) = sv_2mortal(newRV_noinc((SV*)lines));
3472 5           XSRETURN(1);
3473             }
3474              
3475 16           XS_INTERNAL(xs_mmap_open) {
3476 16           dXSARGS;
3477             const char *path;
3478             int writable;
3479             IV idx;
3480             HV *hash;
3481              
3482 16 50         if (items < 1 || items > 2) croak("Usage: file::mmap_open(path, [writable])");
    50          
3483              
3484 16           path = SvPV_nolen(ST(0));
3485 16 100         writable = (items > 1 && SvTRUE(ST(1))) ? 1 : 0;
    50          
3486              
3487 16           idx = file_mmap_open(aTHX_ path, writable);
3488 16 100         if (idx < 0) {
3489 2           ST(0) = &PL_sv_undef;
3490 2           XSRETURN(1);
3491             }
3492              
3493 14           hash = newHV();
3494 14           hv_store(hash, "_idx", 4, newSViv(idx), 0);
3495 14           hv_store(hash, "_writable", 9, newSViv(writable), 0);
3496              
3497 14           ST(0) = sv_2mortal(sv_bless(newRV_noinc((SV*)hash), gv_stashpv("File::Raw::mmap", GV_ADD)));
3498 14           XSRETURN(1);
3499             }
3500              
3501 14           XS_INTERNAL(xs_mmap_data) {
3502 14           dXSARGS;
3503             HV *hash;
3504             SV **idx_sv;
3505             IV idx;
3506              
3507 14 50         if (items != 1) croak("Usage: $mmap->data");
3508              
3509 14 50         if (!SvROK(ST(0)) || SvTYPE(SvRV(ST(0))) != SVt_PVHV) {
    50          
3510 0           croak("Invalid mmap object");
3511             }
3512              
3513 14           hash = (HV*)SvRV(ST(0));
3514 14           idx_sv = hv_fetch(hash, "_idx", 4, 0);
3515 14 50         idx = idx_sv ? SvIV(*idx_sv) : -1;
3516              
3517 14           ST(0) = sv_2mortal(file_mmap_get_sv(aTHX_ idx));
3518 14           XSRETURN(1);
3519             }
3520              
3521 2           XS_INTERNAL(xs_mmap_sync) {
3522 2           dXSARGS;
3523             HV *hash;
3524             SV **idx_sv;
3525             IV idx;
3526              
3527 2 50         if (items != 1) croak("Usage: $mmap->sync");
3528              
3529 2 50         if (!SvROK(ST(0)) || SvTYPE(SvRV(ST(0))) != SVt_PVHV) {
    50          
3530 0           croak("Invalid mmap object");
3531             }
3532              
3533 2           hash = (HV*)SvRV(ST(0));
3534 2           idx_sv = hv_fetch(hash, "_idx", 4, 0);
3535 2 50         idx = idx_sv ? SvIV(*idx_sv) : -1;
3536              
3537 2           file_mmap_sync(idx);
3538 2           XSRETURN_EMPTY;
3539             }
3540              
3541 16           XS_INTERNAL(xs_mmap_close) {
3542 16           dXSARGS;
3543             HV *hash;
3544             SV **idx_sv;
3545             IV idx;
3546              
3547 16 50         if (items != 1) croak("Usage: $mmap->close");
3548              
3549 16 50         if (!SvROK(ST(0)) || SvTYPE(SvRV(ST(0))) != SVt_PVHV) {
    50          
3550 0           croak("Invalid mmap object");
3551             }
3552              
3553 16           hash = (HV*)SvRV(ST(0));
3554 16           idx_sv = hv_fetch(hash, "_idx", 4, 0);
3555 16 50         idx = idx_sv ? SvIV(*idx_sv) : -1;
3556              
3557 16           file_mmap_close(idx);
3558 16           hv_store(hash, "_idx", 4, newSViv(-1), 0);
3559 16           XSRETURN_EMPTY;
3560             }
3561              
3562 14           XS_INTERNAL(xs_mmap_DESTROY) {
3563 14           dXSARGS;
3564             HV *hash;
3565             SV **idx_sv;
3566             IV idx;
3567              
3568             PERL_UNUSED_VAR(items);
3569              
3570 14 50         if (PL_dirty) XSRETURN_EMPTY;
3571              
3572 14 50         if (!SvROK(ST(0)) || SvTYPE(SvRV(ST(0))) != SVt_PVHV) {
    50          
3573 0           XSRETURN_EMPTY;
3574             }
3575              
3576 14           hash = (HV*)SvRV(ST(0));
3577 14           idx_sv = hv_fetch(hash, "_idx", 4, 0);
3578 14 50         idx = idx_sv ? SvIV(*idx_sv) : -1;
3579              
3580 14 50         if (idx >= 0) {
3581 0           file_mmap_close(idx);
3582             }
3583 14           XSRETURN_EMPTY;
3584             }
3585              
3586 24           XS_INTERNAL(xs_lines_iter) {
3587 24           dXSARGS;
3588             const char *path;
3589             IV idx;
3590             SV *idx_sv;
3591              
3592 24 50         if (items < 1)
3593 0           croak("Usage: file::lines_iter(path [, plugin => ..., key => value ...])");
3594              
3595 24           path = SvPV_nolen(ST(0));
3596              
3597             /* Plugin path: slurp + dispatch READ, wrap the resulting AoA in an
3598             * iterator that walks records in order. This is eager (whole AoA
3599             * held in memory) - for true streaming use each_line($p, $cb,
3600             * plugin => ...). The iterator interface itself is preserved so
3601             * code that stores the iterator handle still composes. */
3602 24 100         if (items > 1) {
3603             HV *opts;
3604             SV *bytes;
3605             SV *out;
3606             AV *records;
3607             LineIterEntry *entry;
3608              
3609 5           opts = file_plugin_build_opts(aTHX_ &ST(0), 1, items, "lines_iter");
3610 4           bytes = file_slurp_internal(aTHX_ path);
3611 4           out = file_plugin_dispatch_read(aTHX_ opts, path, bytes);
3612 3           SvREFCNT_dec((SV *)opts);
3613 3 50         if (!out) {
3614 0           SvREFCNT_dec(bytes);
3615 0           ST(0) = &PL_sv_undef;
3616 0           XSRETURN(1);
3617             }
3618 3 50         if (out != bytes) SvREFCNT_dec(bytes);
3619 3 100         if (!SvROK(out) || SvTYPE(SvRV(out)) != SVt_PVAV) {
    50          
3620 1           SvREFCNT_dec(out);
3621 1           croak("File::Raw::lines_iter: plugin must return an arrayref of records");
3622             }
3623 2           records = (AV *)SvRV(out);
3624 2           SvREFCNT_inc(records); /* keep the AV alive on its own */
3625 2           SvREFCNT_dec(out); /* drop the RV wrapper */
3626              
3627 2           idx = alloc_iter_slot();
3628 2           entry = &g_iters[idx];
3629 2           entry->fd = -1; /* sentinel: no file behind us */
3630 2           entry->buffer = NULL;
3631 2           entry->buf_size = 0;
3632 2           entry->buf_pos = 0;
3633 2           entry->buf_len = 0;
3634 2           entry->eof = 0;
3635 2           entry->refcount = 1;
3636 2           entry->path = NULL;
3637 2           entry->records = records;
3638 2           entry->records_idx = 0;
3639              
3640 2           idx_sv = newSViv(idx);
3641 2           ST(0) = sv_2mortal(sv_bless(newRV_noinc(idx_sv),
3642             gv_stashpv("File::Raw::lines", GV_ADD)));
3643 2           XSRETURN(1);
3644             }
3645              
3646 19           idx = file_lines_open(aTHX_ path);
3647              
3648 19 100         if (idx < 0) {
3649 1           ST(0) = &PL_sv_undef;
3650 1           XSRETURN(1);
3651             }
3652              
3653             /* Use simple IV reference - much faster than hash */
3654 18           idx_sv = newSViv(idx);
3655 18           ST(0) = sv_2mortal(sv_bless(newRV_noinc(idx_sv), gv_stashpv("File::Raw::lines", GV_ADD)));
3656 18           XSRETURN(1);
3657             }
3658              
3659 161           XS_INTERNAL(xs_lines_iter_next) {
3660 161           dXSARGS;
3661             SV *rv;
3662             IV idx;
3663             LineIterEntry *entry;
3664             char *line_start;
3665             char *newline;
3666             size_t line_len;
3667             SV *result;
3668             ssize_t n;
3669              
3670 161 50         if (items != 1) croak("Usage: $iter->next");
3671              
3672 161           rv = ST(0);
3673 161 50         if (UNLIKELY(!SvROK(rv))) {
3674 0           croak("Invalid lines iterator object");
3675             }
3676              
3677             /* Direct IV access - no hash lookup */
3678 161           idx = SvIV(SvRV(rv));
3679              
3680 161 50         if (UNLIKELY(idx < 0 || idx >= g_iters_count)) {
    50          
3681 0           ST(0) = &PL_sv_undef;
3682 0           XSRETURN(1);
3683             }
3684              
3685 161           entry = &g_iters[idx];
3686              
3687             /* Record-iter mode: walk the AoA we collected at lines_iter() time. */
3688 161 100         if (entry->records) {
3689 4           SSize_t total = av_len(entry->records) + 1;
3690 4 50         if (entry->records_idx >= total) {
3691 0           ST(0) = &PL_sv_undef;
3692 0           XSRETURN(1);
3693             }
3694 4           SV **rp = av_fetch(entry->records, entry->records_idx++, 0);
3695 4 50         ST(0) = (rp && *rp) ? sv_2mortal(newSVsv(*rp)) : &PL_sv_undef;
    50          
3696 4           XSRETURN(1);
3697             }
3698              
3699 157 50         if (UNLIKELY(entry->fd < 0)) {
3700 0           ST(0) = &PL_sv_undef;
3701 0           XSRETURN(1);
3702             }
3703              
3704             /* Inline buffer parsing for speed */
3705             while (1) {
3706             /* Look for newline in current buffer */
3707 191 100         if (entry->buf_pos < entry->buf_len) {
3708 166           line_start = entry->buffer + entry->buf_pos;
3709 166           newline = memchr(line_start, '\n', entry->buf_len - entry->buf_pos);
3710              
3711 166 100         if (newline) {
3712 142           line_len = newline - line_start;
3713 142           result = newSVpvn(line_start, line_len);
3714 142           entry->buf_pos += line_len + 1;
3715 142           ST(0) = sv_2mortal(result);
3716 142           XSRETURN(1);
3717             }
3718             }
3719              
3720             /* No newline found, need more data */
3721 49 100         if (entry->eof) {
3722             /* Return remaining data if any */
3723 15 100         if (entry->buf_pos < entry->buf_len) {
3724 11           line_len = entry->buf_len - entry->buf_pos;
3725 11           result = newSVpvn(entry->buffer + entry->buf_pos, line_len);
3726 11           entry->buf_pos = entry->buf_len;
3727 11           ST(0) = sv_2mortal(result);
3728 11           XSRETURN(1);
3729             }
3730 4           ST(0) = &PL_sv_undef;
3731 4           XSRETURN(1);
3732             }
3733              
3734             /* Move remaining data to start of buffer */
3735 34 100         if (entry->buf_pos > 0) {
3736 13           size_t remaining = entry->buf_len - entry->buf_pos;
3737 13 100         if (remaining > 0) {
3738 10           memmove(entry->buffer, entry->buffer + entry->buf_pos, remaining);
3739             }
3740 13           entry->buf_len = remaining;
3741 13           entry->buf_pos = 0;
3742             }
3743              
3744             /* Expand buffer if needed */
3745 34 100         if (entry->buf_len >= entry->buf_size - 1) {
3746 1           entry->buf_size *= 2;
3747 1           Renew(entry->buffer, entry->buf_size, char);
3748             }
3749              
3750             /* Read more data */
3751 34           n = read(entry->fd, entry->buffer + entry->buf_len,
3752 34           entry->buf_size - entry->buf_len - 1);
3753 34 50         if (n < 0) {
3754 0 0         if (errno == EINTR) continue;
3755 0           ST(0) = &PL_sv_undef;
3756 0           XSRETURN(1);
3757             }
3758 34 100         if (n == 0) {
3759 15           entry->eof = 1;
3760             } else {
3761 19           entry->buf_len += n;
3762             }
3763             }
3764             }
3765              
3766 131           XS_INTERNAL(xs_lines_iter_eof) {
3767 131           dXSARGS;
3768             SV *rv;
3769             IV idx;
3770             LineIterEntry *entry;
3771              
3772 131 50         if (items != 1) croak("Usage: $iter->eof");
3773              
3774 131           rv = ST(0);
3775 131 50         if (UNLIKELY(!SvROK(rv))) {
3776 0           croak("Invalid lines iterator object");
3777             }
3778              
3779             /* Direct IV access and inline eof check */
3780 131           idx = SvIV(SvRV(rv));
3781              
3782 131 50         if (UNLIKELY(idx < 0 || idx >= g_iters_count)) {
    50          
3783 0           ST(0) = &PL_sv_yes;
3784 0           XSRETURN(1);
3785             }
3786              
3787 131           entry = &g_iters[idx];
3788 131 100         if (entry->records) {
3789 4           ST(0) = (entry->records_idx >= (av_len(entry->records) + 1))
3790 4 100         ? &PL_sv_yes : &PL_sv_no;
3791 4           XSRETURN(1);
3792             }
3793 127 100         ST(0) = (entry->eof && entry->buf_pos >= entry->buf_len) ? &PL_sv_yes : &PL_sv_no;
    50          
3794 127           XSRETURN(1);
3795             }
3796              
3797 19           XS_INTERNAL(xs_lines_iter_close) {
3798 19           dXSARGS;
3799             SV *rv, *inner;
3800             IV idx;
3801              
3802 19 50         if (items != 1) croak("Usage: $iter->close");
3803              
3804 19           rv = ST(0);
3805 19 50         if (UNLIKELY(!SvROK(rv))) {
3806 0           croak("Invalid lines iterator object");
3807             }
3808              
3809 19           inner = SvRV(rv);
3810 19           idx = SvIV(inner);
3811              
3812 19           file_lines_close(idx);
3813 19           sv_setiv(inner, -1); /* Mark as closed */
3814 19           XSRETURN_EMPTY;
3815             }
3816              
3817 20           XS_INTERNAL(xs_lines_iter_DESTROY) {
3818 20           dXSARGS;
3819             SV *rv;
3820             IV idx;
3821              
3822             PERL_UNUSED_VAR(items);
3823              
3824 20 50         if (PL_dirty) XSRETURN_EMPTY;
3825              
3826 20           rv = ST(0);
3827 20 50         if (UNLIKELY(!SvROK(rv))) {
3828 0           XSRETURN_EMPTY;
3829             }
3830              
3831 20           idx = SvIV(SvRV(rv));
3832              
3833 20 100         if (idx >= 0) {
3834 2           file_lines_close(idx);
3835             }
3836 20           XSRETURN_EMPTY;
3837             }
3838              
3839             /* ============================================
3840             Callback registry for line processing
3841             Allows C-level predicates for maximum speed
3842             ============================================ */
3843              
3844             /* Predicate function type for line processing */
3845             typedef bool (*file_line_predicate)(pTHX_ SV *line);
3846              
3847             /* Registered callback entry */
3848             typedef struct {
3849             file_line_predicate predicate; /* C function pointer (NULL for Perl-only) */
3850             SV *perl_callback; /* Perl callback (for fallback or custom) */
3851             } FileLineCallback;
3852              
3853             /* Global callback registry */
3854             static HV *g_file_callback_registry = NULL;
3855              
3856             /* Built-in C predicates */
3857 59           static bool pred_is_blank(pTHX_ SV *line) {
3858             STRLEN len;
3859 59           const char *s = SvPV(line, len);
3860             STRLEN i;
3861 80 100         for (i = 0; i < len; i++) {
3862 64 100         if (s[i] != ' ' && s[i] != '\t' && s[i] != '\r' && s[i] != '\n') {
    50          
    50          
    50          
3863 43           return FALSE;
3864             }
3865             }
3866 16           return TRUE;
3867             }
3868              
3869 41           static bool pred_is_not_blank(pTHX_ SV *line) {
3870 41           return !pred_is_blank(aTHX_ line);
3871             }
3872              
3873 9           static bool pred_is_empty(pTHX_ SV *line) {
3874 9           return SvCUR(line) == 0;
3875             }
3876              
3877 9           static bool pred_is_not_empty(pTHX_ SV *line) {
3878 9           return SvCUR(line) > 0;
3879             }
3880              
3881 29           static bool pred_is_comment(pTHX_ SV *line) {
3882             STRLEN len;
3883 29           const char *s = SvPV(line, len);
3884             /* Skip leading whitespace */
3885 40 100         while (len > 0 && (*s == ' ' || *s == '\t')) {
    100          
    50          
3886 11           s++;
3887 11           len--;
3888             }
3889 29 100         return len > 0 && *s == '#';
    100          
3890             }
3891              
3892 9           static bool pred_is_not_comment(pTHX_ SV *line) {
3893 9           return !pred_is_comment(aTHX_ line);
3894             }
3895              
3896             /* Cleanup callback registry during global destruction */
3897 28           static void file_cleanup_callback_registry(pTHX_ void *data) {
3898             PERL_UNUSED_ARG(data);
3899              
3900             /* During global destruction, just NULL out pointers.
3901             * Perl handles SV cleanup; trying to free them ourselves
3902             * can cause crashes due to destruction order. */
3903 28 50         if (PL_dirty) {
3904 28           g_file_callback_registry = NULL;
3905 28           return;
3906             }
3907              
3908             /* Normal cleanup - not during global destruction */
3909 0           g_file_callback_registry = NULL;
3910             }
3911              
3912 61           static void file_init_callback_registry(pTHX) {
3913             SV *sv;
3914             FileLineCallback *cb;
3915              
3916 61 100         if (g_file_callback_registry) return;
3917 28           g_file_callback_registry = newHV();
3918              
3919             /* Register built-in predicates with both naming conventions */
3920             /* blank / is_blank */
3921 28           Newxz(cb, 1, FileLineCallback);
3922 28           cb->predicate = pred_is_blank;
3923 28           cb->perl_callback = NULL;
3924 28           sv = newSViv(PTR2IV(cb));
3925 28           hv_store(g_file_callback_registry, "blank", 5, sv, 0);
3926 28           hv_store(g_file_callback_registry, "is_blank", 8, SvREFCNT_inc(sv), 0);
3927              
3928             /* not_blank / is_not_blank */
3929 28           Newxz(cb, 1, FileLineCallback);
3930 28           cb->predicate = pred_is_not_blank;
3931 28           cb->perl_callback = NULL;
3932 28           sv = newSViv(PTR2IV(cb));
3933 28           hv_store(g_file_callback_registry, "not_blank", 9, sv, 0);
3934 28           hv_store(g_file_callback_registry, "is_not_blank", 12, SvREFCNT_inc(sv), 0);
3935              
3936             /* empty / is_empty */
3937 28           Newxz(cb, 1, FileLineCallback);
3938 28           cb->predicate = pred_is_empty;
3939 28           cb->perl_callback = NULL;
3940 28           sv = newSViv(PTR2IV(cb));
3941 28           hv_store(g_file_callback_registry, "empty", 5, sv, 0);
3942 28           hv_store(g_file_callback_registry, "is_empty", 8, SvREFCNT_inc(sv), 0);
3943              
3944             /* not_empty / is_not_empty */
3945 28           Newxz(cb, 1, FileLineCallback);
3946 28           cb->predicate = pred_is_not_empty;
3947 28           cb->perl_callback = NULL;
3948 28           sv = newSViv(PTR2IV(cb));
3949 28           hv_store(g_file_callback_registry, "not_empty", 9, sv, 0);
3950 28           hv_store(g_file_callback_registry, "is_not_empty", 12, SvREFCNT_inc(sv), 0);
3951              
3952             /* comment / is_comment */
3953 28           Newxz(cb, 1, FileLineCallback);
3954 28           cb->predicate = pred_is_comment;
3955 28           cb->perl_callback = NULL;
3956 28           sv = newSViv(PTR2IV(cb));
3957 28           hv_store(g_file_callback_registry, "comment", 7, sv, 0);
3958 28           hv_store(g_file_callback_registry, "is_comment", 10, SvREFCNT_inc(sv), 0);
3959              
3960             /* not_comment / is_not_comment */
3961 28           Newxz(cb, 1, FileLineCallback);
3962 28           cb->predicate = pred_is_not_comment;
3963 28           cb->perl_callback = NULL;
3964 28           sv = newSViv(PTR2IV(cb));
3965 28           hv_store(g_file_callback_registry, "not_comment", 11, sv, 0);
3966 28           hv_store(g_file_callback_registry, "is_not_comment", 14, SvREFCNT_inc(sv), 0);
3967             }
3968              
3969 23           static FileLineCallback* file_get_callback(pTHX_ const char *name) {
3970             SV **svp;
3971 23 50         if (!g_file_callback_registry) return NULL;
3972 23           svp = hv_fetch(g_file_callback_registry, name, strlen(name), 0);
3973 23 100         if (svp && SvIOK(*svp)) {
    50          
3974 19           return INT2PTR(FileLineCallback*, SvIVX(*svp));
3975             }
3976 4           return NULL;
3977             }
3978              
3979             /* Process lines with callback - MULTICALL optimized (Perl >= 5.14 only) */
3980 14           XS_INTERNAL(xs_each_line) {
3981 14           dXSARGS;
3982             #if PERL_VERSION >= 14
3983             dMULTICALL;
3984             #endif
3985             const char *path;
3986             SV *callback;
3987             IV idx;
3988             CV *block_cv;
3989             SV *old_defsv;
3990             SV *line_sv;
3991             LineIterEntry *entry;
3992             char *line_start;
3993             char *newline;
3994             size_t line_len;
3995             ssize_t n;
3996             #if PERL_VERSION >= 14
3997 14           U8 gimme = G_VOID;
3998             #endif
3999              
4000 14 100         if (items < 2)
4001 1           croak("Usage: file::each_line(path, callback [, plugin => ..., key => value ...])");
4002              
4003 13           path = SvPV_nolen(ST(0));
4004 13           callback = ST(1);
4005              
4006 13 100         if (!SvROK(callback) || SvTYPE(SvRV(callback)) != SVt_PVCV) {
    50          
4007 1           croak("Second argument must be a code reference");
4008             }
4009              
4010             /* Plugin path: route through streaming dispatch. The plugin's
4011             * stream fn owns the record emission and calls back to `callback`
4012             * per record (typically once for each parsed CSV row, etc.). */
4013 12 100         if (items > 2) {
4014 4           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "each_line");
4015 4           (void)file_plugin_dispatch_stream(aTHX_ opts, path, callback);
4016 0           SvREFCNT_dec((SV *)opts);
4017 0           XSRETURN_EMPTY;
4018             }
4019              
4020 8           block_cv = (CV*)SvRV(callback);
4021 8           idx = file_lines_open(aTHX_ path);
4022 8 100         if (idx < 0) {
4023 1           XSRETURN_EMPTY;
4024             }
4025              
4026 7           entry = &g_iters[idx];
4027              
4028 7 50         old_defsv = DEFSV;
4029 7           line_sv = newSV(256);
4030 7 50         DEFSV = line_sv;
4031              
4032             #if PERL_VERSION >= 14
4033 7 50         PUSH_MULTICALL(block_cv);
4034             #endif
4035              
4036             while (1) {
4037             /* Look for newline in current buffer */
4038 1039 100         if (entry->buf_pos < entry->buf_len) {
4039 1027           line_start = entry->buffer + entry->buf_pos;
4040 1027           newline = memchr(line_start, '\n', entry->buf_len - entry->buf_pos);
4041              
4042 1027 100         if (newline) {
4043 1021           line_len = newline - line_start;
4044 1021           sv_setpvn(line_sv, line_start, line_len);
4045 1021           entry->buf_pos += line_len + 1;
4046             #if PERL_VERSION >= 14
4047 1021           MULTICALL;
4048             #else
4049             { dSP; PUSHMARK(SP); call_sv((SV*)block_cv, G_VOID|G_DISCARD); }
4050             #endif
4051 1020           continue;
4052             }
4053             }
4054              
4055             /* No newline found, need more data */
4056 18 100         if (entry->eof) {
4057             /* Return remaining data if any */
4058 6 100         if (entry->buf_pos < entry->buf_len) {
4059 3           line_len = entry->buf_len - entry->buf_pos;
4060 3           sv_setpvn(line_sv, entry->buffer + entry->buf_pos, line_len);
4061 3           entry->buf_pos = entry->buf_len;
4062             #if PERL_VERSION >= 14
4063 3           MULTICALL;
4064             #else
4065             { dSP; PUSHMARK(SP); call_sv((SV*)block_cv, G_VOID|G_DISCARD); }
4066             #endif
4067             }
4068 6           break;
4069             }
4070              
4071             /* Move remaining data to start of buffer */
4072 12 100         if (entry->buf_pos > 0) {
4073 5           size_t remaining = entry->buf_len - entry->buf_pos;
4074 5 100         if (remaining > 0) {
4075 3           memmove(entry->buffer, entry->buffer + entry->buf_pos, remaining);
4076             }
4077 5           entry->buf_len = remaining;
4078 5           entry->buf_pos = 0;
4079             }
4080              
4081             /* Expand buffer if needed */
4082 12 50         if (entry->buf_len >= entry->buf_size - 1) {
4083 0           entry->buf_size *= 2;
4084 0           Renew(entry->buffer, entry->buf_size, char);
4085             }
4086              
4087             /* Read more data */
4088 12           n = read(entry->fd, entry->buffer + entry->buf_len,
4089 12           entry->buf_size - entry->buf_len - 1);
4090 12 50         if (n < 0) {
4091 0 0         if (errno == EINTR) continue;
4092 0           break;
4093             }
4094 12 100         if (n == 0) {
4095 6           entry->eof = 1;
4096             } else {
4097 6           entry->buf_len += n;
4098             }
4099             }
4100              
4101             #if PERL_VERSION >= 14
4102 6 50         POP_MULTICALL;
4103             #endif
4104 6           SvREFCNT_dec(line_sv);
4105 6 50         DEFSV = old_defsv;
4106 6           file_lines_close(idx);
4107 6           XSRETURN_EMPTY;
4108             }
4109              
4110             /* Run plugin READ, expect arrayref result. Returns the underlying AV*
4111             * (whose RV is stored in *out_holder for the caller to refcount-dec).
4112             * Returns NULL if the plugin cancelled. Croaks if the plugin returned
4113             * a non-arrayref (predicate-style XSUBs need an iterable). */
4114 14           static AV *file_records_via_plugin(pTHX_ HV *opts, const char *path,
4115             SV **out_holder)
4116             {
4117 14           SV *bytes = file_slurp_internal(aTHX_ path);
4118 14           SV *out = file_plugin_dispatch_read(aTHX_ opts, path, bytes);
4119 13 50         if (!out) {
4120 0           SvREFCNT_dec(bytes);
4121 0           return NULL;
4122             }
4123 13 50         if (out != bytes) SvREFCNT_dec(bytes);
4124 13 100         if (!SvROK(out) || SvTYPE(SvRV(out)) != SVt_PVAV) {
    50          
4125 1           SvREFCNT_dec(out);
4126 1           croak("File::Raw: plugin must return an arrayref of records "
4127             "for predicate-style operations");
4128             }
4129 12           *out_holder = out;
4130 12           return (AV *)SvRV(out);
4131             }
4132              
4133             /* Apply a coderef predicate to one record. Returns 1 if matched, 0 otherwise.
4134             * Used by the plugin path of grep/count/find. */
4135 21           static int file_call_predicate_cv(pTHX_ CV *cv, SV *record) {
4136 21           int matches = 0;
4137             int n;
4138             SV *r;
4139 21           dSP;
4140 21 50         PUSHMARK(SP);
4141 21 50         XPUSHs(record);
4142 21           PUTBACK;
4143 21           n = call_sv((SV *)cv, G_SCALAR);
4144 21           SPAGAIN;
4145 21 50         if (n > 0) {
4146 21           r = POPs;
4147 21           matches = SvTRUE(r) ? 1 : 0;
4148             }
4149 21           PUTBACK;
4150 21           return matches;
4151             }
4152              
4153             /* Grep lines with callback or registered predicate name */
4154 25           XS_INTERNAL(xs_grep_lines) {
4155 25           dXSARGS;
4156             const char *path;
4157             SV *predicate;
4158             IV idx;
4159             SV *line;
4160             AV *result;
4161 25           CV *block_cv = NULL;
4162 25           FileLineCallback *fcb = NULL;
4163              
4164 25 50         if (items < 2)
4165 0           croak("Usage: file::grep_lines(path, &predicate or $name [, plugin => ..., key => value ...])");
4166              
4167 25           path = SvPV_nolen(ST(0));
4168 25           predicate = ST(1);
4169              
4170             /* Plugin path: records come from plugin READ; predicate must be a coderef. */
4171 25 100         if (items > 2) {
4172             HV *opts;
4173 4           SV *holder = NULL;
4174             AV *records;
4175             SSize_t i, n;
4176             AV *matched;
4177              
4178 4 100         if (!SvROK(predicate) || SvTYPE(SvRV(predicate)) != SVt_PVCV)
    50          
4179 1           croak("File::Raw::grep_lines: predicate must be a coderef when "
4180             "a plugin is in use (predicate-name sugar is legacy 2-arg only)");
4181              
4182 3           opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "grep_lines");
4183 3           records = file_records_via_plugin(aTHX_ opts, path, &holder);
4184 2           SvREFCNT_dec((SV *)opts);
4185 2 50         if (!records) {
4186 0           ST(0) = sv_2mortal(newRV_noinc((SV *)newAV()));
4187 0           XSRETURN(1);
4188             }
4189              
4190 2           matched = newAV();
4191 2           n = av_len(records) + 1;
4192 10 100         for (i = 0; i < n; i++) {
4193 8           SV **rp = av_fetch(records, i, 0);
4194 8 50         SV *rec = (rp && *rp) ? *rp : &PL_sv_undef;
    50          
4195 8 100         if (file_call_predicate_cv(aTHX_ (CV *)SvRV(predicate), rec))
4196 7           av_push(matched, SvREFCNT_inc(rec));
4197             }
4198 2           SvREFCNT_dec(holder);
4199 2           ST(0) = sv_2mortal(newRV_noinc((SV *)matched));
4200 2           XSRETURN(1);
4201             }
4202 21           result = newAV();
4203              
4204             /* Check if predicate is a name or coderef */
4205 21 100         if (SvROK(predicate) && SvTYPE(SvRV(predicate)) == SVt_PVCV) {
    50          
4206 5           block_cv = (CV*)SvRV(predicate);
4207             } else {
4208 16           const char *name = SvPV_nolen(predicate);
4209 16           fcb = file_get_callback(aTHX_ name);
4210 16 100         if (!fcb) {
4211 1           croak("File::Raw::grep_lines: unknown predicate '%s'", name);
4212             }
4213             }
4214              
4215 20           idx = file_lines_open(aTHX_ path);
4216 20 50         if (idx < 0) {
4217 0           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
4218 0           XSRETURN(1);
4219             }
4220              
4221             /* C predicate path - fastest */
4222 20 100         if (fcb && fcb->predicate) {
    100          
4223 103 100         while ((line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
4224 91 100         if (fcb->predicate(aTHX_ line)) {
4225 49           av_push(result, line);
4226             } else {
4227 42           SvREFCNT_dec(line);
4228             }
4229             }
4230 12           file_lines_close(idx);
4231 12           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
4232 12           XSRETURN(1);
4233             }
4234              
4235             /* Call Perl callback */
4236             {
4237 8 100         SV *cb_sv = fcb ? fcb->perl_callback : (SV*)block_cv;
4238 1065 100         while ((line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
4239 1057           dSP;
4240             IV count;
4241             SV *result_sv;
4242 1057           bool matches = FALSE;
4243 1057 50         PUSHMARK(SP);
4244 1057 50         XPUSHs(line);
4245 1057           PUTBACK;
4246 1057           count = call_sv(cb_sv, G_SCALAR);
4247 1057           SPAGAIN;
4248 1057 50         if (count > 0) {
4249 1057           result_sv = POPs;
4250 1057           matches = SvTRUE(result_sv);
4251             }
4252 1057           PUTBACK;
4253 1057 100         if (matches) {
4254 26           av_push(result, line);
4255             } else {
4256 1031           SvREFCNT_dec(line);
4257             }
4258             }
4259             }
4260              
4261 8           file_lines_close(idx);
4262 8           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
4263 8           XSRETURN(1);
4264             }
4265              
4266             /* Count lines matching predicate */
4267 9           XS_INTERNAL(xs_count_lines) {
4268 9           dXSARGS;
4269             const char *path;
4270 9           SV *predicate = NULL;
4271             IV idx;
4272             SV *line;
4273 9           IV count = 0;
4274 9           CV *block_cv = NULL;
4275 9           FileLineCallback *fcb = NULL;
4276              
4277 9 50         if (items < 1)
4278 0           croak("Usage: file::count_lines(path [, &predicate or $name] [, plugin => ..., key => value ...])");
4279              
4280 9           path = SvPV_nolen(ST(0));
4281              
4282             /* Plugin path: if items > 2 we definitely have a plugin tail. Otherwise
4283             * if items == 2 and ST(1) looks like the start of options (a string key
4284             * with a value missing - i.e. items == 2 but ST(1) is a known options
4285             * key like "plugin"), users use the explicit form by passing a coderef
4286             * or undef as the predicate slot first. Keep it strict: plugin tail
4287             * begins at position 2, so items must be >= 3 (predicate may be undef). */
4288 9 100         if (items > 2) {
4289             HV *opts;
4290 2           SV *holder = NULL;
4291             AV *records;
4292             SSize_t i, n;
4293 2           IV matched = 0;
4294 2           int has_pred = SvOK(ST(1));
4295              
4296 2 100         if (has_pred && (!SvROK(ST(1)) || SvTYPE(SvRV(ST(1))) != SVt_PVCV))
    50          
    50          
4297 0           croak("File::Raw::count_lines: predicate must be a coderef or undef "
4298             "when a plugin is in use");
4299              
4300 2           opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "count_lines");
4301 2           records = file_records_via_plugin(aTHX_ opts, path, &holder);
4302 2           SvREFCNT_dec((SV *)opts);
4303 2 50         if (!records) {
4304 0           ST(0) = sv_2mortal(newSViv(0));
4305 0           XSRETURN(1);
4306             }
4307 2           n = av_len(records) + 1;
4308 2 100         if (!has_pred) {
4309 1           matched = n;
4310             } else {
4311 6 100         for (i = 0; i < n; i++) {
4312 5           SV **rp = av_fetch(records, i, 0);
4313 5 50         SV *rec = (rp && *rp) ? *rp : &PL_sv_undef;
    50          
4314 5 100         if (file_call_predicate_cv(aTHX_ (CV *)SvRV(ST(1)), rec))
4315 4           matched++;
4316             }
4317             }
4318 2           SvREFCNT_dec(holder);
4319 2           ST(0) = sv_2mortal(newSViv(matched));
4320 2           XSRETURN(1);
4321             }
4322              
4323             /* If no predicate, just count newlines - no SV creation needed */
4324 7 100         if (items == 1) {
4325             int fd;
4326             char *buffer;
4327 4           ssize_t n, total_read = 0;
4328             char *p, *end;
4329 4           char last_char = '\n'; /* Assume last char is newline (handles empty file) */
4330             #ifdef _WIN32
4331             int open_flags = O_RDONLY | O_BINARY;
4332             #else
4333 4           int open_flags = O_RDONLY;
4334             #endif
4335 4           fd = open(path, open_flags);
4336 4 100         if (UNLIKELY(fd < 0)) {
4337 1           ST(0) = sv_2mortal(newSViv(0));
4338 1           XSRETURN(1);
4339             }
4340              
4341 3           Newx(buffer, FILE_BUFFER_SIZE, char);
4342 3           count = 0;
4343              
4344 5 100         while ((n = read(fd, buffer, FILE_BUFFER_SIZE)) > 0) {
4345 2           p = buffer;
4346 2           end = buffer + n;
4347 1010 100         while ((p = memchr(p, '\n', end - p)) != NULL) {
4348 1008           count++;
4349 1008           p++;
4350             }
4351 2           total_read += n;
4352 2           last_char = buffer[n - 1];
4353             }
4354 3           close(fd);
4355 3           Safefree(buffer);
4356              
4357             /* If file doesn't end with newline, count the last line */
4358 3 100         if (total_read > 0 && last_char != '\n') {
    100          
4359 1           count++;
4360             }
4361              
4362 3           ST(0) = sv_2mortal(newSViv(count));
4363 3           XSRETURN(1);
4364             }
4365              
4366 3           predicate = ST(1);
4367              
4368             /* Check if predicate is a name or coderef */
4369 3 100         if (SvROK(predicate) && SvTYPE(SvRV(predicate)) == SVt_PVCV) {
    50          
4370 2           block_cv = (CV*)SvRV(predicate);
4371             } else {
4372 1           const char *name = SvPV_nolen(predicate);
4373 1           fcb = file_get_callback(aTHX_ name);
4374 1 50         if (!fcb) {
4375 0           croak("File::Raw::count_lines: unknown predicate '%s'", name);
4376             }
4377             }
4378              
4379 3           idx = file_lines_open(aTHX_ path);
4380 3 50         if (idx < 0) {
4381 0           ST(0) = sv_2mortal(newSViv(0));
4382 0           XSRETURN(1);
4383             }
4384              
4385             /* C predicate path - fastest */
4386 3 100         if (fcb && fcb->predicate) {
    50          
4387 10 100         while ((line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
4388 9 100         if (fcb->predicate(aTHX_ line)) {
4389 7           count++;
4390             }
4391 9           SvREFCNT_dec(line);
4392             }
4393 1           file_lines_close(idx);
4394 1           ST(0) = sv_2mortal(newSViv(count));
4395 1           XSRETURN(1);
4396             }
4397              
4398             /* Call Perl callback */
4399             {
4400 2 50         SV *cb_sv = fcb ? fcb->perl_callback : (SV*)block_cv;
4401 15 100         while ((line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
4402 13           dSP;
4403             IV n;
4404             SV *result_sv;
4405 13           bool matches = FALSE;
4406 13 50         PUSHMARK(SP);
4407 13 50         XPUSHs(line);
4408 13           PUTBACK;
4409 13           n = call_sv(cb_sv, G_SCALAR);
4410 13           SPAGAIN;
4411 13 50         if (n > 0) {
4412 13           result_sv = POPs;
4413 13           matches = SvTRUE(result_sv);
4414             }
4415 13           PUTBACK;
4416 13 100         if (matches) {
4417 10           count++;
4418             }
4419 13           SvREFCNT_dec(line);
4420             }
4421             }
4422              
4423 2           file_lines_close(idx);
4424 2           ST(0) = sv_2mortal(newSViv(count));
4425 2           XSRETURN(1);
4426             }
4427              
4428             /* Find first line matching predicate */
4429 8           XS_INTERNAL(xs_find_line) {
4430 8           dXSARGS;
4431             const char *path;
4432             SV *predicate;
4433             IV idx;
4434             SV *line;
4435 8           CV *block_cv = NULL;
4436 8           FileLineCallback *fcb = NULL;
4437              
4438 8 50         if (items < 2)
4439 0           croak("Usage: file::find_line(path, &predicate or $name [, plugin => ..., key => value ...])");
4440              
4441 8           path = SvPV_nolen(ST(0));
4442 8           predicate = ST(1);
4443              
4444 8 100         if (items > 2) {
4445             HV *opts;
4446 2           SV *holder = NULL;
4447             AV *records;
4448             SSize_t i, n;
4449              
4450 2 50         if (!SvROK(predicate) || SvTYPE(SvRV(predicate)) != SVt_PVCV)
    50          
4451 0           croak("File::Raw::find_line: predicate must be a coderef when "
4452             "a plugin is in use");
4453              
4454 2           opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "find_line");
4455 2           records = file_records_via_plugin(aTHX_ opts, path, &holder);
4456 2           SvREFCNT_dec((SV *)opts);
4457 2 50         if (!records) XSRETURN_UNDEF;
4458              
4459 2           n = av_len(records) + 1;
4460 9 100         for (i = 0; i < n; i++) {
4461 8           SV **rp = av_fetch(records, i, 0);
4462 8 50         SV *rec = (rp && *rp) ? *rp : &PL_sv_undef;
    50          
4463 8 100         if (file_call_predicate_cv(aTHX_ (CV *)SvRV(predicate), rec)) {
4464 1           SV *winner = newSVsv(rec);
4465 1           SvREFCNT_dec(holder);
4466 1           ST(0) = sv_2mortal(winner);
4467 1           XSRETURN(1);
4468             }
4469             }
4470 1           SvREFCNT_dec(holder);
4471 1           XSRETURN_UNDEF;
4472             }
4473              
4474             /* Check if predicate is a name or coderef */
4475 6 100         if (SvROK(predicate) && SvTYPE(SvRV(predicate)) == SVt_PVCV) {
    50          
4476 5           block_cv = (CV*)SvRV(predicate);
4477             } else {
4478 1           const char *name = SvPV_nolen(predicate);
4479 1           fcb = file_get_callback(aTHX_ name);
4480 1 50         if (!fcb) {
4481 0           croak("File::Raw::find_line: unknown predicate '%s'", name);
4482             }
4483             }
4484              
4485 6           idx = file_lines_open(aTHX_ path);
4486 6 50         if (idx < 0) {
4487 0           XSRETURN_UNDEF;
4488             }
4489              
4490             /* C predicate path - fastest */
4491 6 100         if (fcb && fcb->predicate) {
    50          
4492 6 50         while ((line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
4493 6 100         if (fcb->predicate(aTHX_ line)) {
4494 1           file_lines_close(idx);
4495 1           ST(0) = sv_2mortal(line);
4496 1           XSRETURN(1);
4497             }
4498 5           SvREFCNT_dec(line);
4499             }
4500 0           file_lines_close(idx);
4501 0           XSRETURN_UNDEF;
4502             }
4503              
4504             /* Call Perl callback */
4505             {
4506 5 50         SV *cb_sv = fcb ? fcb->perl_callback : (SV*)block_cv;
4507 26 100         while ((line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
4508 25           dSP;
4509             IV n;
4510             SV *result_sv;
4511 25           bool matches = FALSE;
4512 25 50         PUSHMARK(SP);
4513 25 50         XPUSHs(line);
4514 25           PUTBACK;
4515 25           n = call_sv(cb_sv, G_SCALAR);
4516 25           SPAGAIN;
4517 25 50         if (n > 0) {
4518 25           result_sv = POPs;
4519 25           matches = SvTRUE(result_sv);
4520             }
4521 25           PUTBACK;
4522 25 100         if (matches) {
4523 4           file_lines_close(idx);
4524 4           ST(0) = sv_2mortal(line);
4525 4           XSRETURN(1);
4526             }
4527 21           SvREFCNT_dec(line);
4528             }
4529             }
4530              
4531 1           file_lines_close(idx);
4532 1           XSRETURN_UNDEF;
4533             }
4534              
4535             /* Map lines with callback */
4536 5           XS_INTERNAL(xs_map_lines) {
4537 5           dXSARGS;
4538             const char *path;
4539             SV *callback;
4540             IV idx;
4541             SV *line;
4542             AV *result;
4543 5 50         if (items < 2)
4544 0           croak("Usage: file::map_lines(path, &callback [, plugin => ..., key => value ...])");
4545              
4546 5           path = SvPV_nolen(ST(0));
4547 5           callback = ST(1);
4548 5           result = newAV();
4549              
4550 5 50         if (!SvROK(callback) || SvTYPE(SvRV(callback)) != SVt_PVCV) {
    50          
4551 0           croak("Second argument must be a code reference");
4552             }
4553              
4554 5 100         if (items > 2) {
4555             HV *opts;
4556 1           SV *holder = NULL;
4557             AV *records;
4558             SSize_t i, n;
4559             AV *out;
4560              
4561 1           SvREFCNT_dec((SV *)result);
4562 1           opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "map_lines");
4563 1           records = file_records_via_plugin(aTHX_ opts, path, &holder);
4564 1           SvREFCNT_dec((SV *)opts);
4565 1 50         if (!records) {
4566 0           ST(0) = sv_2mortal(newRV_noinc((SV *)newAV()));
4567 0           XSRETURN(1);
4568             }
4569 1           out = newAV();
4570 1           n = av_len(records) + 1;
4571 1           av_extend(out, n);
4572 6 100         for (i = 0; i < n; i++) {
4573 5           SV **rp = av_fetch(records, i, 0);
4574 5 50         SV *rec = (rp && *rp) ? *rp : &PL_sv_undef;
    50          
4575             int rn;
4576             SV *rv;
4577 5           dSP;
4578 5 50         PUSHMARK(SP);
4579 5 50         XPUSHs(rec);
4580 5           PUTBACK;
4581 5           rn = call_sv(callback, G_SCALAR);
4582 5           SPAGAIN;
4583 5 50         if (rn > 0) {
4584 5           rv = POPs;
4585 5           av_push(out, SvREFCNT_inc(rv));
4586             }
4587 5           PUTBACK;
4588             }
4589 1           SvREFCNT_dec(holder);
4590 1           ST(0) = sv_2mortal(newRV_noinc((SV *)out));
4591 1           XSRETURN(1);
4592             }
4593              
4594 4           idx = file_lines_open(aTHX_ path);
4595 4 50         if (idx < 0) {
4596 0           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
4597 0           XSRETURN(1);
4598             }
4599              
4600             /* Call Perl callback */
4601             {
4602 31 100         while ((line = file_lines_next(aTHX_ idx)) != &PL_sv_undef) {
4603 27           dSP;
4604             IV count;
4605             SV *result_sv;
4606 27 50         PUSHMARK(SP);
4607 27 50         XPUSHs(sv_2mortal(line));
4608 27           PUTBACK;
4609 27           count = call_sv(callback, G_SCALAR);
4610 27           SPAGAIN;
4611 27 50         if (count > 0) {
4612 27           result_sv = POPs;
4613 27           av_push(result, SvREFCNT_inc(result_sv));
4614             }
4615 27           PUTBACK;
4616             }
4617             }
4618              
4619 4           file_lines_close(idx);
4620 4           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
4621 4           XSRETURN(1);
4622             }
4623              
4624             /* ============================================
4625             Perl bridge for the plugin API.
4626              
4627             Perl plugins are registered as a hashref of phase coderefs:
4628              
4629             File::Raw::register_plugin('csv', {
4630             read => sub { my ($path, $bytes, $opts) = @_; ... },
4631             write => sub { my ($path, $rows, $opts) = @_; ... },
4632             record => sub { my ($path, $record, $opts) = @_; ... },
4633             });
4634              
4635             The bridge allocates a PerlPluginBridge holding the coderef SVs plus
4636             a FilePlugin block whose function pointers are static C thunks. The
4637             thunks recover the bridge from FilePluginContext::plugin_state and
4638             call the appropriate coderef. The bridge is pinned in
4639             g_perl_plugins so we can free it on unregister.
4640              
4641             The 'stream' phase is intentionally not supported from Perl: a Perl
4642             stream plugin would be invoked once per chunk by file.c's read loop,
4643             and the per-call call_sv overhead defeats the point of streaming.
4644             Perl plugins that need record-by-record callbacks should implement
4645             the 'record' phase instead - File::Raw drives the iteration.
4646             ============================================ */
4647              
4648             typedef struct PerlPluginBridge {
4649             char *name; /* strdup'd; pointer is stored in plugin.name */
4650             SV *read_cv;
4651             SV *write_cv;
4652             SV *record_cv;
4653             FilePlugin plugin;
4654             } PerlPluginBridge;
4655              
4656             static HV *g_perl_plugins = NULL;
4657              
4658 13           static void perl_plugin_bridge_free(pTHX_ PerlPluginBridge *b) {
4659 13 50         if (!b) return;
4660 13 100         if (b->read_cv) SvREFCNT_dec(b->read_cv);
4661 13 100         if (b->write_cv) SvREFCNT_dec(b->write_cv);
4662 13 100         if (b->record_cv) SvREFCNT_dec(b->record_cv);
4663 13 50         if (b->name) Safefree(b->name);
4664 13           Safefree(b);
4665             }
4666              
4667 42           static SV *perl_plugin_thunk_read(pTHX_ FilePluginContext *ctx) {
4668 42           PerlPluginBridge *b = (PerlPluginBridge *)ctx->plugin_state;
4669             SV *result;
4670             int count;
4671 42           dSP;
4672              
4673 42           ENTER;
4674 42           SAVETMPS;
4675 42 50         PUSHMARK(SP);
4676 42 50         XPUSHs(sv_2mortal(newSVpv(ctx->path ? ctx->path : "", 0)));
    50          
4677 42 50         XPUSHs(sv_2mortal(newSVsv(ctx->data)));
4678 42 50         XPUSHs(sv_2mortal(newRV_inc((SV *)ctx->options)));
4679 42           PUTBACK;
4680              
4681 42           count = call_sv(b->read_cv, G_SCALAR | G_EVAL);
4682              
4683 42           SPAGAIN;
4684 42 50         if (SvTRUE(ERRSV)) {
    100          
4685 1 50         SV *err = newSVsv(ERRSV);
4686 1 50         FREETMPS;
4687 1           LEAVE;
4688 1           croak_sv(err);
4689             }
4690 41 50         if (count > 0) {
4691 41           SV *ret = POPs;
4692 41 100         if (SvOK(ret)) {
4693 39           result = newSVsv(ret);
4694             } else {
4695 2           ctx->cancel = 1;
4696 2           result = NULL;
4697             }
4698             } else {
4699 0           ctx->cancel = 1;
4700 0           result = NULL;
4701             }
4702 41           PUTBACK;
4703 41 50         FREETMPS;
4704 41           LEAVE;
4705 41           return result;
4706             }
4707              
4708 15           static SV *perl_plugin_thunk_write(pTHX_ FilePluginContext *ctx) {
4709 15           PerlPluginBridge *b = (PerlPluginBridge *)ctx->plugin_state;
4710             SV *result;
4711             int count;
4712 15           dSP;
4713              
4714 15           ENTER;
4715 15           SAVETMPS;
4716 15 50         PUSHMARK(SP);
4717 15 50         XPUSHs(sv_2mortal(newSVpv(ctx->path ? ctx->path : "", 0)));
    50          
4718 15 50         XPUSHs(sv_2mortal(newSVsv(ctx->data)));
4719 15 50         XPUSHs(sv_2mortal(newRV_inc((SV *)ctx->options)));
4720 15           PUTBACK;
4721              
4722 15           count = call_sv(b->write_cv, G_SCALAR | G_EVAL);
4723              
4724 15           SPAGAIN;
4725 15 50         if (SvTRUE(ERRSV)) {
    50          
4726 0 0         SV *err = newSVsv(ERRSV);
4727 0 0         FREETMPS;
4728 0           LEAVE;
4729 0           croak_sv(err);
4730             }
4731 15 50         if (count > 0) {
4732 15           SV *ret = POPs;
4733 15 100         if (SvOK(ret)) {
4734 13           result = newSVsv(ret);
4735             } else {
4736 2           ctx->cancel = 1;
4737 2           result = NULL;
4738             }
4739             } else {
4740 0           ctx->cancel = 1;
4741 0           result = NULL;
4742             }
4743 15           PUTBACK;
4744 15 50         FREETMPS;
4745 15           LEAVE;
4746 15           return result;
4747             }
4748              
4749 0           static SV *perl_plugin_thunk_record(pTHX_ FilePluginContext *ctx, SV *record) {
4750 0           PerlPluginBridge *b = (PerlPluginBridge *)ctx->plugin_state;
4751             SV *result;
4752             int count;
4753 0           dSP;
4754              
4755 0           ENTER;
4756 0           SAVETMPS;
4757 0 0         PUSHMARK(SP);
4758 0 0         XPUSHs(sv_2mortal(newSVpv(ctx->path ? ctx->path : "", 0)));
    0          
4759 0 0         XPUSHs(sv_2mortal(newSVsv(record)));
4760 0 0         XPUSHs(sv_2mortal(newRV_inc((SV *)ctx->options)));
4761 0           PUTBACK;
4762              
4763 0           count = call_sv(b->record_cv, G_SCALAR | G_EVAL);
4764              
4765 0           SPAGAIN;
4766 0 0         if (SvTRUE(ERRSV)) {
    0          
4767 0 0         SV *err = newSVsv(ERRSV);
4768 0 0         FREETMPS;
4769 0           LEAVE;
4770 0           croak_sv(err);
4771             }
4772 0 0         if (count > 0) {
4773 0           SV *ret = POPs;
4774 0 0         if (SvOK(ret)) {
4775 0           result = newSVsv(ret);
4776             } else {
4777 0           result = &PL_sv_undef; /* exclude */
4778             }
4779             } else {
4780 0           result = &PL_sv_undef;
4781             }
4782 0           PUTBACK;
4783 0 0         FREETMPS;
4784 0           LEAVE;
4785 0           return result;
4786             }
4787              
4788             /* ============================================
4789             Built-in 'predicate' plugin.
4790              
4791             Records flow through plugin->record_fn(), which looks up a predicate by
4792             name in g_file_callback_registry. The record fn returns the record SV
4793             when the predicate matches, or &PL_sv_undef when it does not -
4794             callers (grep/count/find) interpret this via truthiness.
4795              
4796             The 'name' option is required:
4797              
4798             File::Raw::grep_lines($p, plugin => 'predicate', name => 'is_blank');
4799              
4800             Predicates registered as Perl coderefs are invoked with the record in
4801             $_; the same convention as grep_lines's per-record callbacks.
4802             ============================================ */
4803              
4804 0           static SV *predicate_plugin_record(pTHX_ FilePluginContext *ctx, SV *record) {
4805             SV **svp;
4806             const char *pred_name;
4807             STRLEN pred_name_len;
4808             FileLineCallback *cb;
4809              
4810 0           svp = hv_fetchs(ctx->options, "name", 0);
4811 0 0         if (!svp || !*svp || !SvOK(*svp))
    0          
    0          
4812 0           croak("File::Raw plugin 'predicate': missing 'name' option");
4813 0           pred_name = SvPV(*svp, pred_name_len);
4814              
4815 0 0         if (!g_file_callback_registry)
4816 0           file_init_callback_registry(aTHX);
4817              
4818 0           svp = hv_fetch(g_file_callback_registry, pred_name, pred_name_len, 0);
4819 0 0         if (!svp || !*svp)
    0          
4820 0           croak("File::Raw plugin 'predicate': unknown predicate '%s'", pred_name);
4821 0           cb = INT2PTR(FileLineCallback *, SvIV(*svp));
4822              
4823 0 0         if (cb->predicate) {
4824 0 0         return cb->predicate(aTHX_ record) ? record : &PL_sv_undef;
4825 0 0         } else if (cb->perl_callback) {
4826 0 0         SV *old_defsv = DEFSV;
4827             SV *result_sv;
4828             int count;
4829 0           bool matched = FALSE;
4830 0           dSP;
4831              
4832 0 0         DEFSV = record;
4833 0           ENTER;
4834 0           SAVETMPS;
4835 0 0         PUSHMARK(SP);
4836 0           PUTBACK;
4837 0           count = call_sv(cb->perl_callback, G_SCALAR);
4838 0           SPAGAIN;
4839 0 0         if (count > 0) {
4840 0           result_sv = POPs;
4841 0           matched = SvTRUE(result_sv);
4842             }
4843 0           PUTBACK;
4844 0 0         FREETMPS;
4845 0           LEAVE;
4846 0 0         DEFSV = old_defsv;
4847 0 0         return matched ? record : &PL_sv_undef;
4848             }
4849 0           return &PL_sv_undef;
4850             }
4851              
4852             static FilePlugin g_predicate_plugin = {
4853             "predicate",
4854             NULL, /* read */
4855             NULL, /* write */
4856             predicate_plugin_record, /* record */
4857             NULL, /* stream */
4858             NULL /* state - the global registry is consulted directly */
4859             };
4860              
4861             /* Public XSUBs for the predicate registry that backs the 'predicate'
4862             * plugin. Adding here also makes the entry visible to the legacy 2-arg
4863             * grep_lines($p, $name) sugar, which looks names up in the same HV. */
4864              
4865 8           XS_INTERNAL(xs_register_predicate) {
4866 8           dXSARGS;
4867             const char *name;
4868             STRLEN name_len;
4869             SV *coderef;
4870             FileLineCallback *cb;
4871             FileLineCallback *existing;
4872             SV *sv;
4873              
4874 8 100         if (items != 2)
4875 1           croak("Usage: File::Raw::register_predicate($name, \\&coderef)");
4876              
4877 7           name = SvPV(ST(0), name_len);
4878 7           coderef = ST(1);
4879              
4880 7 100         if (!SvROK(coderef) || SvTYPE(SvRV(coderef)) != SVt_PVCV)
    50          
4881 2           croak("File::Raw::register_predicate: second arg must be a coderef");
4882              
4883 5           file_init_callback_registry(aTHX);
4884              
4885 5           existing = file_get_callback(aTHX_ name);
4886 5 100         if (existing) {
4887 2 50         if (existing->perl_callback) SvREFCNT_dec(existing->perl_callback);
4888 2           existing->perl_callback = newSVsv(coderef);
4889 2           existing->predicate = NULL;
4890 2           XSRETURN_YES;
4891             }
4892              
4893 3           Newxz(cb, 1, FileLineCallback);
4894 3           cb->predicate = NULL;
4895 3           cb->perl_callback = newSVsv(coderef);
4896 3           sv = newSViv(PTR2IV(cb));
4897 3           hv_store(g_file_callback_registry, name, name_len, sv, 0);
4898              
4899 3           XSRETURN_YES;
4900             }
4901              
4902 3           XS_INTERNAL(xs_list_predicates) {
4903 3           dXSARGS;
4904 3           AV *result = newAV();
4905             HE *he;
4906              
4907             PERL_UNUSED_VAR(items);
4908              
4909 3 50         if (g_file_callback_registry) {
4910 3           hv_iterinit(g_file_callback_registry);
4911 42 100         while ((he = hv_iternext(g_file_callback_registry))) {
4912             I32 klen;
4913 39           const char *kname = hv_iterkey(he, &klen);
4914 39           av_push(result, newSVpvn(kname, klen));
4915             }
4916             }
4917              
4918 3           ST(0) = sv_2mortal(newRV_noinc((SV *)result));
4919 3           XSRETURN(1);
4920             }
4921              
4922 33           XS_INTERNAL(xs_register_plugin) {
4923 33           dXSARGS;
4924             const char *name;
4925             STRLEN name_len;
4926             SV *spec;
4927             HV *spec_hv;
4928             SV **svp;
4929             PerlPluginBridge *b;
4930             int rc;
4931 33           int override = 0;
4932              
4933 33 100         if (items < 2 || items > 3)
    50          
4934 1           croak("Usage: File::Raw::register_plugin($name, \\%%phases [, $override])");
4935              
4936 32           name = SvPV(ST(0), name_len);
4937 32 50         if (name_len == 0)
4938 0           croak("File::Raw::register_plugin: name must be non-empty");
4939              
4940 32           spec = ST(1);
4941 32 100         if (!SvROK(spec) || SvTYPE(SvRV(spec)) != SVt_PVHV)
    50          
4942 1           croak("File::Raw::register_plugin: second arg must be a hashref");
4943 31           spec_hv = (HV *)SvRV(spec);
4944              
4945 31 100         if (items == 3) override = SvTRUE(ST(2));
4946              
4947 31 100         if (override) (void)file_unregister_plugin(aTHX_ name);
4948              
4949 31           Newxz(b, 1, PerlPluginBridge);
4950 31           b->name = savepv(name);
4951              
4952 31           svp = hv_fetchs(spec_hv, "read", 0);
4953 31 100         if (svp && *svp && SvROK(*svp) && SvTYPE(SvRV(*svp)) == SVt_PVCV) {
    50          
    100          
    50          
4954 27           b->read_cv = newSVsv(*svp);
4955 27           b->plugin.read_fn = perl_plugin_thunk_read;
4956             }
4957 31           svp = hv_fetchs(spec_hv, "write", 0);
4958 31 100         if (svp && *svp && SvROK(*svp) && SvTYPE(SvRV(*svp)) == SVt_PVCV) {
    50          
    50          
    50          
4959 8           b->write_cv = newSVsv(*svp);
4960 8           b->plugin.write_fn = perl_plugin_thunk_write;
4961             }
4962 31           svp = hv_fetchs(spec_hv, "record", 0);
4963 31 100         if (svp && *svp && SvROK(*svp) && SvTYPE(SvRV(*svp)) == SVt_PVCV) {
    50          
    50          
    50          
4964 1           b->record_cv = newSVsv(*svp);
4965 1           b->plugin.record_fn = perl_plugin_thunk_record;
4966             }
4967             /* hv_existss is 5.36+. Use hv_exists for portability. */
4968 31 100         if (hv_exists(spec_hv, "stream", 6))
4969 1           croak("File::Raw::register_plugin: 'stream' phase not supported "
4970             "from Perl - implement 'record' instead, or write a C plugin");
4971              
4972 30 100         if (!b->plugin.read_fn && !b->plugin.write_fn && !b->plugin.record_fn) {
    50          
    100          
4973 2           perl_plugin_bridge_free(aTHX_ b);
4974 2           croak("File::Raw::register_plugin: at least one of read/write/record "
4975             "must be a coderef");
4976             }
4977              
4978 28           b->plugin.name = b->name;
4979 28           b->plugin.state = b;
4980              
4981 28           rc = file_register_plugin(aTHX_ &b->plugin);
4982 28 100         if (rc != 1) {
4983 1           perl_plugin_bridge_free(aTHX_ b);
4984 1 50         if (rc == 0)
4985 1           croak("File::Raw::register_plugin: plugin '%s' is already registered",
4986             name);
4987 0           croak("File::Raw::register_plugin: invalid plugin spec for '%s'", name);
4988             }
4989              
4990 27 100         if (!g_perl_plugins) g_perl_plugins = newHV();
4991 27           hv_store(g_perl_plugins, name, name_len, newSViv(PTR2IV(b)), 0);
4992              
4993 27           XSRETURN_YES;
4994             }
4995              
4996 11           XS_INTERNAL(xs_unregister_plugin) {
4997 11           dXSARGS;
4998             const char *name;
4999             STRLEN name_len;
5000             SV **svp;
5001 11           int removed_perl = 0;
5002              
5003 11 50         if (items != 1)
5004 0           croak("Usage: File::Raw::unregister_plugin($name)");
5005              
5006 11           name = SvPV(ST(0), name_len);
5007              
5008 11 50         if (g_perl_plugins) {
5009 11           svp = hv_fetch(g_perl_plugins, name, name_len, 0);
5010 11 100         if (svp && *svp) {
    50          
5011 10           PerlPluginBridge *b = INT2PTR(PerlPluginBridge *, SvIV(*svp));
5012 10           (void)hv_delete(g_perl_plugins, name, name_len, G_DISCARD);
5013 10           (void)file_unregister_plugin(aTHX_ name);
5014 10           perl_plugin_bridge_free(aTHX_ b);
5015 10           removed_perl = 1;
5016             }
5017             }
5018 11 100         if (!removed_perl)
5019 1           (void)file_unregister_plugin(aTHX_ name);
5020              
5021 11           XSRETURN_YES;
5022             }
5023              
5024 3           XS_INTERNAL(xs_list_plugins) {
5025 3           dXSARGS;
5026 3           AV *result = newAV();
5027             HE *he;
5028              
5029             PERL_UNUSED_VAR(items);
5030              
5031 3 50         if (g_file_plugin_registry) {
5032 3           hv_iterinit(g_file_plugin_registry);
5033 7 100         while ((he = hv_iternext(g_file_plugin_registry))) {
5034             I32 klen;
5035 4           const char *kname = hv_iterkey(he, &klen);
5036 4           av_push(result, newSVpvn(kname, klen));
5037             }
5038             }
5039              
5040 3           ST(0) = sv_2mortal(newRV_noinc((SV *)result));
5041 3           XSRETURN(1);
5042             }
5043              
5044             /* New stat functions */
5045 2           XS_INTERNAL(xs_atime) {
5046 2           dXSARGS;
5047             const char *path;
5048 2 50         if (items != 1) croak("Usage: file::atime(path)");
5049 2           path = SvPV_nolen(ST(0));
5050 2           ST(0) = sv_2mortal(newSViv(file_atime_internal(path)));
5051 2           XSRETURN(1);
5052             }
5053              
5054 2           XS_INTERNAL(xs_ctime) {
5055 2           dXSARGS;
5056             const char *path;
5057 2 50         if (items != 1) croak("Usage: file::ctime(path)");
5058 2           path = SvPV_nolen(ST(0));
5059 2           ST(0) = sv_2mortal(newSViv(file_ctime_internal(path)));
5060 2           XSRETURN(1);
5061             }
5062              
5063 2           XS_INTERNAL(xs_mode) {
5064 2           dXSARGS;
5065             const char *path;
5066 2 50         if (items != 1) croak("Usage: file::mode(path)");
5067 2           path = SvPV_nolen(ST(0));
5068 2           ST(0) = sv_2mortal(newSViv(file_mode_internal(path)));
5069 2           XSRETURN(1);
5070             }
5071              
5072             /* Combined stat - all attributes in one syscall */
5073 4           XS_INTERNAL(xs_stat_all) {
5074 4           dXSARGS;
5075             const char *path;
5076             HV *result;
5077 4 50         if (items != 1) croak("Usage: File::Raw::stat(path)");
5078 4           path = SvPV_nolen(ST(0));
5079 4           result = file_stat_all_internal(aTHX_ path);
5080 4 100         if (result == NULL) {
5081 1           ST(0) = &PL_sv_undef;
5082             } else {
5083 3           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
5084             }
5085 4           XSRETURN(1);
5086             }
5087              
5088 4           XS_INTERNAL(xs_is_link) {
5089 4           dXSARGS;
5090             const char *path;
5091 4 50         if (items != 1) croak("Usage: file::is_link(path)");
5092 4           path = SvPV_nolen(ST(0));
5093 4 100         ST(0) = file_is_link_internal(path) ? &PL_sv_yes : &PL_sv_no;
5094 4           XSRETURN(1);
5095             }
5096              
5097 0           XS_INTERNAL(xs_is_executable) {
5098 0           dXSARGS;
5099             const char *path;
5100 0 0         if (items != 1) croak("Usage: file::is_executable(path)");
5101 0           path = SvPV_nolen(ST(0));
5102 0 0         ST(0) = file_is_executable_internal(path) ? &PL_sv_yes : &PL_sv_no;
5103 0           XSRETURN(1);
5104             }
5105              
5106             /* File manipulation functions */
5107 4           XS_INTERNAL(xs_unlink) {
5108 4           dXSARGS;
5109             const char *path;
5110 4 50         if (items != 1) croak("Usage: file::unlink(path)");
5111 4           path = SvPV_nolen(ST(0));
5112 4 100         ST(0) = file_unlink_internal(path) ? &PL_sv_yes : &PL_sv_no;
5113 4           XSRETURN(1);
5114             }
5115              
5116 3           XS_INTERNAL(xs_copy) {
5117 3           dXSARGS;
5118             const char *src;
5119             const char *dst;
5120 3 50         if (items != 2) croak("Usage: file::copy(src, dst)");
5121 3           src = SvPV_nolen(ST(0));
5122 3           dst = SvPV_nolen(ST(1));
5123 3 100         ST(0) = file_copy_internal(aTHX_ src, dst) ? &PL_sv_yes : &PL_sv_no;
5124 3           XSRETURN(1);
5125             }
5126              
5127 3           XS_INTERNAL(xs_move) {
5128 3           dXSARGS;
5129             const char *src;
5130             const char *dst;
5131 3 50         if (items != 2) croak("Usage: file::move(src, dst)");
5132 3           src = SvPV_nolen(ST(0));
5133 3           dst = SvPV_nolen(ST(1));
5134 3 100         ST(0) = file_move_internal(aTHX_ src, dst) ? &PL_sv_yes : &PL_sv_no;
5135 3           XSRETURN(1);
5136             }
5137              
5138 2           XS_INTERNAL(xs_touch) {
5139 2           dXSARGS;
5140             const char *path;
5141 2 50         if (items != 1) croak("Usage: file::touch(path)");
5142 2           path = SvPV_nolen(ST(0));
5143 2 50         ST(0) = file_touch_internal(path) ? &PL_sv_yes : &PL_sv_no;
5144 2           XSRETURN(1);
5145             }
5146              
5147 1           XS_INTERNAL(xs_clear_stat_cache) {
5148 1           dXSARGS;
5149 1 50         if (items > 1) croak("Usage: file::clear_stat_cache() or file::clear_stat_cache(path)");
5150            
5151 1 50         if (items == 1 && SvOK(ST(0))) {
    0          
5152 0           const char *path = SvPV_nolen(ST(0));
5153 0           invalidate_stat_cache_path(path);
5154             } else {
5155 1           invalidate_stat_cache();
5156             }
5157            
5158 1           ST(0) = &PL_sv_yes;
5159 1           XSRETURN(1);
5160             }
5161              
5162 1           XS_INTERNAL(xs_chmod) {
5163 1           dXSARGS;
5164             const char *path;
5165             int mode;
5166 1 50         if (items != 2) croak("Usage: file::chmod(path, mode)");
5167 1           path = SvPV_nolen(ST(0));
5168 1           mode = SvIV(ST(1));
5169 1 50         ST(0) = file_chmod_internal(path, mode) ? &PL_sv_yes : &PL_sv_no;
5170 1           XSRETURN(1);
5171             }
5172              
5173 12           XS_INTERNAL(xs_mkdir) {
5174 12           dXSARGS;
5175             const char *path;
5176 12           int mode = 0755;
5177 12 50         if (items < 1 || items > 2) croak("Usage: file::mkdir(path, [mode])");
    50          
5178 12           path = SvPV_nolen(ST(0));
5179 12 50         if (items > 1) mode = SvIV(ST(1));
5180 12 100         ST(0) = file_mkdir_internal(path, mode) ? &PL_sv_yes : &PL_sv_no;
5181 12           XSRETURN(1);
5182             }
5183              
5184 6           XS_INTERNAL(xs_rmdir) {
5185 6           dXSARGS;
5186             const char *path;
5187 6 50         if (items != 1) croak("Usage: file::rmdir(path)");
5188 6           path = SvPV_nolen(ST(0));
5189 6 100         ST(0) = file_rmdir_internal(path) ? &PL_sv_yes : &PL_sv_no;
5190 6           XSRETURN(1);
5191             }
5192              
5193 5           XS_INTERNAL(xs_readdir) {
5194 5           dXSARGS;
5195             const char *path;
5196             AV *result;
5197 5 50         if (items != 1) croak("Usage: file::readdir(path)");
5198 5           path = SvPV_nolen(ST(0));
5199 5           result = file_readdir_internal(aTHX_ path);
5200 5           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
5201 5           XSRETURN(1);
5202             }
5203              
5204             /* Path manipulation functions */
5205 12           XS_INTERNAL(xs_basename) {
5206 12           dXSARGS;
5207             const char *path;
5208 12 50         if (items != 1) croak("Usage: file::basename(path)");
5209 12           path = SvPV_nolen(ST(0));
5210 12           ST(0) = sv_2mortal(file_basename_internal(aTHX_ path));
5211 12           XSRETURN(1);
5212             }
5213              
5214 10           XS_INTERNAL(xs_dirname) {
5215 10           dXSARGS;
5216             const char *path;
5217 10 50         if (items != 1) croak("Usage: file::dirname(path)");
5218 10           path = SvPV_nolen(ST(0));
5219 10           ST(0) = sv_2mortal(file_dirname_internal(aTHX_ path));
5220 10           XSRETURN(1);
5221             }
5222              
5223 13           XS_INTERNAL(xs_extname) {
5224 13           dXSARGS;
5225             const char *path;
5226 13 50         if (items != 1) croak("Usage: file::extname(path)");
5227 13           path = SvPV_nolen(ST(0));
5228 13           ST(0) = sv_2mortal(file_extname_internal(aTHX_ path));
5229 13           XSRETURN(1);
5230             }
5231              
5232 11           XS_INTERNAL(xs_join) {
5233 11           dXSARGS;
5234             AV *parts;
5235             SSize_t i;
5236              
5237 11 50         if (items < 1) croak("Usage: file::join(part1, part2, ...)");
5238              
5239 11           parts = newAV();
5240 37 100         for (i = 0; i < items; i++) {
5241 26           av_push(parts, newSVsv(ST(i)));
5242             }
5243              
5244 11           ST(0) = sv_2mortal(file_join_internal(aTHX_ parts));
5245 11           SvREFCNT_dec((SV*)parts);
5246 11           XSRETURN(1);
5247             }
5248              
5249             /* mkpath: recursive mkdir */
5250 1           XS_INTERNAL(xs_mkpath) {
5251 1           dXSARGS;
5252             const char *path;
5253             STRLEN path_len;
5254             char buf[4096];
5255             STRLEN i;
5256 1           int created = 0;
5257              
5258 1 50         if (items != 1) croak("Usage: file_mkpath(path)");
5259 1           path = SvPV(ST(0), path_len);
5260 1 50         if (path_len >= sizeof(buf)) croak("Path too long");
5261              
5262 36 100         for (i = 0; i <= path_len; i++) {
5263 35 100         if (i == path_len || path[i] == '/' || path[i] == '\\') {
    100          
    50          
5264 5 100         if (i == 0) {
5265             /* Root / or drive-relative */
5266 1           buf[0] = path[0];
5267 1           buf[1] = '\0';
5268 1           continue;
5269             }
5270 4           memcpy(buf, path, i);
5271 4           buf[i] = '\0';
5272              
5273             /* Skip drive letter portion like C: */
5274 4 50         if (i == 2 && buf[1] == ':') continue;
    0          
5275              
5276 4 100         if (!file_is_dir_internal(buf)) {
5277 2 50         if (file_mkdir_internal(buf, 0755))
5278 2           created = 1;
5279             }
5280             }
5281             }
5282              
5283 1 50         ST(0) = created || file_is_dir_internal(path) ? &PL_sv_yes : &PL_sv_no;
    0          
5284 1           XSRETURN(1);
5285             }
5286              
5287             /* rm_rf: recursive remove directory */
5288 2           static void file_rm_rf_internal(pTHX_ const char *path) {
5289             AV *entries;
5290             SSize_t i, len;
5291              
5292 2 50         if (!file_is_dir_internal(path)) {
5293 0           file_unlink_internal(path);
5294 0           return;
5295             }
5296              
5297 2           entries = file_readdir_internal(aTHX_ path);
5298 2           len = av_len(entries) + 1;
5299 3 100         for (i = 0; i < len; i++) {
5300 1           SV **sv = av_fetch(entries, i, 0);
5301 1 50         if (sv) {
5302 1           AV *join_parts = newAV();
5303             SV *child_sv;
5304             const char *child;
5305              
5306 1           av_push(join_parts, newSVpv(path, 0));
5307 1           av_push(join_parts, newSVsv(*sv));
5308 1           child_sv = file_join_internal(aTHX_ join_parts);
5309 1           child = SvPV_nolen(child_sv);
5310              
5311 1 50         if (file_is_dir_internal(child)) {
5312 1           file_rm_rf_internal(aTHX_ child);
5313             } else {
5314 0           file_unlink_internal(child);
5315             }
5316              
5317 1           SvREFCNT_dec(child_sv);
5318 1           SvREFCNT_dec((SV*)join_parts);
5319             }
5320             }
5321 2           SvREFCNT_dec((SV*)entries);
5322 2           file_rmdir_internal(path);
5323             }
5324              
5325 1           XS_INTERNAL(xs_rm_rf) {
5326 1           dXSARGS;
5327             const char *path;
5328              
5329 1 50         if (items != 1) croak("Usage: file_rm_rf(path)");
5330 1           path = SvPV_nolen(ST(0));
5331 1           file_rm_rf_internal(aTHX_ path);
5332 1           ST(0) = &PL_sv_yes;
5333 1           XSRETURN(1);
5334             }
5335              
5336             /* Head and tail */
5337             /* head/tail share an arg-parsing convention: even items means $n is at
5338             * ST(1) and the plugin tail (if any) begins at ST(2); odd items means
5339             * $n is omitted and the plugin tail begins at ST(1). This avoids the
5340             * ambiguity of "is ST(1) a count or the first option key?". */
5341 8           XS_INTERNAL(xs_head) {
5342 8           dXSARGS;
5343             const char *path;
5344             AV *result;
5345 8           IV n = 10;
5346             int n_positional;
5347              
5348 8 50         if (items < 1)
5349 0           croak("Usage: file::head(path [, n] [, plugin => ..., key => value ...])");
5350              
5351 8           path = SvPV_nolen(ST(0));
5352 8 100         n_positional = (items % 2 == 0) ? 2 : 1;
5353 8 100         if (n_positional == 2) n = SvIV(ST(1));
5354              
5355 8 100         if (items > n_positional) {
5356             HV *opts;
5357 2           SV *holder = NULL;
5358             AV *records;
5359             AV *out;
5360             SSize_t i, total, take;
5361              
5362 2           opts = file_plugin_build_opts(aTHX_ &ST(0), n_positional, items, "head");
5363 2           records = file_records_via_plugin(aTHX_ opts, path, &holder);
5364 2           SvREFCNT_dec((SV *)opts);
5365 2 50         if (!records) {
5366 0           ST(0) = sv_2mortal(newRV_noinc((SV *)newAV()));
5367 0           XSRETURN(1);
5368             }
5369 2           out = newAV();
5370 2           total = av_len(records) + 1;
5371 2           take = (n < (IV)total) ? n : total;
5372 2 50         if (take < 0) take = 0;
5373 2           av_extend(out, take);
5374 9 100         for (i = 0; i < take; i++) {
5375 7           SV **rp = av_fetch(records, i, 0);
5376 7 50         av_push(out, SvREFCNT_inc(rp && *rp ? *rp : &PL_sv_undef));
    50          
5377             }
5378 2           SvREFCNT_dec(holder);
5379 2           ST(0) = sv_2mortal(newRV_noinc((SV *)out));
5380 2           XSRETURN(1);
5381             }
5382              
5383 6           result = file_head_internal(aTHX_ path, n);
5384 6           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
5385 6           XSRETURN(1);
5386             }
5387              
5388 7           XS_INTERNAL(xs_tail) {
5389 7           dXSARGS;
5390             const char *path;
5391             AV *result;
5392 7           IV n = 10;
5393             int n_positional;
5394              
5395 7 50         if (items < 1)
5396 0           croak("Usage: file::tail(path [, n] [, plugin => ..., key => value ...])");
5397              
5398 7           path = SvPV_nolen(ST(0));
5399 7 100         n_positional = (items % 2 == 0) ? 2 : 1;
5400 7 100         if (n_positional == 2) n = SvIV(ST(1));
5401              
5402 7 100         if (items > n_positional) {
5403             HV *opts;
5404 1           SV *holder = NULL;
5405             AV *records;
5406             AV *out;
5407             SSize_t i, total, start, take;
5408              
5409 1           opts = file_plugin_build_opts(aTHX_ &ST(0), n_positional, items, "tail");
5410 1           records = file_records_via_plugin(aTHX_ opts, path, &holder);
5411 1           SvREFCNT_dec((SV *)opts);
5412 1 50         if (!records) {
5413 0           ST(0) = sv_2mortal(newRV_noinc((SV *)newAV()));
5414 0           XSRETURN(1);
5415             }
5416 1           out = newAV();
5417 1           total = av_len(records) + 1;
5418 1           take = (n < (IV)total) ? n : total;
5419 1 50         if (take < 0) take = 0;
5420 1           start = total - take;
5421 1           av_extend(out, take);
5422 3 100         for (i = start; i < total; i++) {
5423 2           SV **rp = av_fetch(records, i, 0);
5424 2 50         av_push(out, SvREFCNT_inc(rp && *rp ? *rp : &PL_sv_undef));
    50          
5425             }
5426 1           SvREFCNT_dec(holder);
5427 1           ST(0) = sv_2mortal(newRV_noinc((SV *)out));
5428 1           XSRETURN(1);
5429             }
5430              
5431 6           result = file_tail_internal(aTHX_ path, n);
5432 6           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
5433 6           XSRETURN(1);
5434             }
5435              
5436             /* range_lines(path, from, count [, plugin => ..., key => value ...])
5437             *
5438             * 1-based, half-open in count style: range_lines($p, 5, 3) returns
5439             * lines 5, 6, 7 (or fewer if EOF arrives first). Symmetric with
5440             * head/tail in shape and plugin behaviour. */
5441 16           XS_INTERNAL(xs_range_lines) {
5442 16           dXSARGS;
5443             const char *path;
5444             IV from, count;
5445             AV *result;
5446              
5447 16 100         if (items < 3)
5448 2           croak("Usage: file::range_lines(path, from, count "
5449             "[, plugin => ..., key => value ...])");
5450              
5451 14           path = SvPV_nolen(ST(0));
5452 14           from = SvIV(ST(1));
5453 14           count = SvIV(ST(2));
5454              
5455             /* Plugin path: slice the plugin's record AoA. Same eager trade-off
5456             * as head/tail/lines under a plugin tail. */
5457 14 100         if (items > 3) {
5458             HV *opts;
5459 5           SV *holder = NULL;
5460             AV *records;
5461             AV *out;
5462             SSize_t i, total, start, end;
5463              
5464 5           opts = file_plugin_build_opts(aTHX_ &ST(0), 3, items, "range_lines");
5465 3           records = file_records_via_plugin(aTHX_ opts, path, &holder);
5466 2           SvREFCNT_dec((SV *)opts);
5467 2 50         if (!records) {
5468 0           ST(0) = sv_2mortal(newRV_noinc((SV *)newAV()));
5469 0           XSRETURN(1);
5470             }
5471 2           out = newAV();
5472 2 50         if (from < 1 || count <= 0) {
    50          
5473 0           SvREFCNT_dec(holder);
5474 0           ST(0) = sv_2mortal(newRV_noinc((SV *)out));
5475 0           XSRETURN(1);
5476             }
5477 2           total = av_len(records) + 1;
5478 2           start = from - 1; /* 1-based -> 0-based */
5479 2 50         if (start >= total) {
5480 0           SvREFCNT_dec(holder);
5481 0           ST(0) = sv_2mortal(newRV_noinc((SV *)out));
5482 0           XSRETURN(1);
5483             }
5484 2           end = start + count;
5485 2 100         if (end > total) end = total;
5486 2           av_extend(out, end - start - 1);
5487 7 100         for (i = start; i < end; i++) {
5488 5           SV **rp = av_fetch(records, i, 0);
5489 5 50         av_push(out, SvREFCNT_inc(rp && *rp ? *rp : &PL_sv_undef));
    50          
5490             }
5491 2           SvREFCNT_dec(holder);
5492 2           ST(0) = sv_2mortal(newRV_noinc((SV *)out));
5493 2           XSRETURN(1);
5494             }
5495              
5496 9           result = file_range_internal(aTHX_ path, from, count);
5497 9           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
5498 9           XSRETURN(1);
5499             }
5500              
5501             /* Atomic spew */
5502 17           XS_INTERNAL(xs_atomic_spew) {
5503 17           dXSARGS;
5504             const char *path;
5505             SV *payload;
5506 17           SV *bytes_to_write = NULL;
5507              
5508 17 50         if (items < 2)
5509 0           croak("Usage: file::atomic_spew(path, data [, plugin => ..., key => value ...])");
5510              
5511 17           path = SvPV_nolen(ST(0));
5512 17           payload = ST(1);
5513              
5514 17 100         if (items > 2) {
5515 3           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "atomic_spew");
5516 3           bytes_to_write = file_plugin_dispatch_write(aTHX_ opts, path, payload);
5517 3           SvREFCNT_dec((SV *)opts);
5518 3 50         if (!bytes_to_write) {
5519 0           ST(0) = &PL_sv_no;
5520 0           XSRETURN(1);
5521             }
5522 3           payload = sv_2mortal(bytes_to_write);
5523             }
5524              
5525 17 50         ST(0) = file_atomic_spew_internal(aTHX_ path, payload) ? &PL_sv_yes : &PL_sv_no;
5526 17           XSRETURN(1);
5527             }
5528              
5529             /* ============================================
5530             Function-style XS (for import)
5531             ============================================ */
5532              
5533 1           XS_EXTERNAL(XS_file_func_slurp) {
5534 1           dXSARGS;
5535             const char *path;
5536             SV *bytes;
5537              
5538 1 50         if (items < 1)
5539 0           croak("Usage: file_slurp($path [, plugin => ..., key => value ...])");
5540              
5541 1           path = SvPV_nolen(ST(0));
5542 1           bytes = file_slurp_internal(aTHX_ path);
5543              
5544 1 50         if (items > 1) {
5545 1           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 1, items, "slurp");
5546 1           SV *out = file_plugin_dispatch_read(aTHX_ opts, path, bytes);
5547 1           SvREFCNT_dec((SV *)opts);
5548 1 50         if (!out) {
5549 0           SvREFCNT_dec(bytes);
5550 0           ST(0) = &PL_sv_undef;
5551 0           XSRETURN(1);
5552             }
5553 1 50         if (out != bytes) {
5554 1           SvREFCNT_dec(bytes);
5555 1           bytes = out;
5556             }
5557             }
5558 1           ST(0) = sv_2mortal(bytes);
5559 1           XSRETURN(1);
5560             }
5561              
5562 1           XS_EXTERNAL(XS_file_func_spew) {
5563 1           dXSARGS;
5564             const char *path;
5565             SV *payload;
5566              
5567 1 50         if (items < 2)
5568 0           croak("Usage: file_spew($path, $data [, plugin => ..., key => value ...])");
5569              
5570 1           path = SvPV_nolen(ST(0));
5571 1           payload = ST(1);
5572              
5573 1 50         if (items > 2) {
5574 0           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "spew");
5575 0           SV *out = file_plugin_dispatch_write(aTHX_ opts, path, payload);
5576 0           SvREFCNT_dec((SV *)opts);
5577 0 0         if (!out) { ST(0) = &PL_sv_no; XSRETURN(1); }
5578 0           payload = sv_2mortal(out);
5579             }
5580 1 50         ST(0) = file_spew_internal(aTHX_ path, payload) ? &PL_sv_yes : &PL_sv_no;
5581 1           XSRETURN(1);
5582             }
5583              
5584 0           XS_EXTERNAL(XS_file_func_exists) {
5585 0           dXSARGS;
5586             const char *path;
5587 0 0         if (items != 1) croak("Usage: file_exists($path)");
5588 0           path = SvPV_nolen(ST(0));
5589 0 0         ST(0) = file_exists_internal(path) ? &PL_sv_yes : &PL_sv_no;
5590 0           XSRETURN(1);
5591             }
5592              
5593 0           XS_EXTERNAL(XS_file_func_size) {
5594 0           dXSARGS;
5595             const char *path;
5596 0 0         if (items != 1) croak("Usage: file_size($path)");
5597 0           path = SvPV_nolen(ST(0));
5598 0           ST(0) = sv_2mortal(newSViv(file_size_internal(path)));
5599 0           XSRETURN(1);
5600             }
5601              
5602 0           XS_EXTERNAL(XS_file_func_is_file) {
5603 0           dXSARGS;
5604             const char *path;
5605 0 0         if (items != 1) croak("Usage: file_is_file($path)");
5606 0           path = SvPV_nolen(ST(0));
5607 0 0         ST(0) = file_is_file_internal(path) ? &PL_sv_yes : &PL_sv_no;
5608 0           XSRETURN(1);
5609             }
5610              
5611 0           XS_EXTERNAL(XS_file_func_is_dir) {
5612 0           dXSARGS;
5613             const char *path;
5614 0 0         if (items != 1) croak("Usage: file_is_dir($path)");
5615 0           path = SvPV_nolen(ST(0));
5616 0 0         ST(0) = file_is_dir_internal(path) ? &PL_sv_yes : &PL_sv_no;
5617 0           XSRETURN(1);
5618             }
5619              
5620 0           XS_EXTERNAL(XS_file_func_lines) {
5621 0           dXSARGS;
5622             const char *path;
5623             SV *content;
5624             AV *lines;
5625              
5626 0 0         if (items < 1)
5627 0           croak("Usage: file_lines($path [, plugin => ..., key => value ...])");
5628              
5629 0           path = SvPV_nolen(ST(0));
5630              
5631 0 0         if (items > 1) {
5632 0           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 1, items, "lines");
5633 0           SV *bytes = file_slurp_internal(aTHX_ path);
5634 0           SV *out = file_plugin_dispatch_read(aTHX_ opts, path, bytes);
5635 0           SvREFCNT_dec((SV *)opts);
5636 0 0         if (!out) {
5637 0           SvREFCNT_dec(bytes);
5638 0           ST(0) = sv_2mortal(newRV_noinc((SV *)newAV()));
5639 0           XSRETURN(1);
5640             }
5641 0 0         if (out != bytes) SvREFCNT_dec(bytes);
5642 0 0         if (SvROK(out) && SvTYPE(SvRV(out)) == SVt_PVAV) {
    0          
5643 0           ST(0) = sv_2mortal(out);
5644 0           XSRETURN(1);
5645             }
5646 0           lines = file_split_lines(aTHX_ out);
5647 0           SvREFCNT_dec(out);
5648 0           ST(0) = sv_2mortal(newRV_noinc((SV *)lines));
5649 0           XSRETURN(1);
5650             }
5651              
5652 0           content = file_slurp_internal(aTHX_ path);
5653 0 0         if (content == &PL_sv_undef) {
5654 0           lines = newAV();
5655             } else {
5656 0           lines = file_split_lines(aTHX_ content);
5657 0           SvREFCNT_dec(content);
5658             }
5659 0           ST(0) = sv_2mortal(newRV_noinc((SV*)lines));
5660 0           XSRETURN(1);
5661             }
5662              
5663 0           XS_EXTERNAL(XS_file_func_unlink) {
5664 0           dXSARGS;
5665             const char *path;
5666 0 0         if (items != 1) croak("Usage: file_unlink($path)");
5667 0           path = SvPV_nolen(ST(0));
5668 0 0         ST(0) = file_unlink_internal(path) ? &PL_sv_yes : &PL_sv_no;
5669 0           XSRETURN(1);
5670             }
5671              
5672 0           XS_EXTERNAL(XS_file_func_mkdir) {
5673 0           dXSARGS;
5674             const char *path;
5675 0 0         if (items != 1) croak("Usage: file_mkdir($path)");
5676 0           path = SvPV_nolen(ST(0));
5677 0 0         ST(0) = file_mkdir_internal(path, 0755) ? &PL_sv_yes : &PL_sv_no;
5678 0           XSRETURN(1);
5679             }
5680              
5681 0           XS_EXTERNAL(XS_file_func_rmdir) {
5682 0           dXSARGS;
5683             const char *path;
5684 0 0         if (items != 1) croak("Usage: file_rmdir($path)");
5685 0           path = SvPV_nolen(ST(0));
5686 0 0         ST(0) = file_rmdir_internal(path) ? &PL_sv_yes : &PL_sv_no;
5687 0           XSRETURN(1);
5688             }
5689              
5690 0           XS_EXTERNAL(XS_file_func_touch) {
5691 0           dXSARGS;
5692             const char *path;
5693 0 0         if (items != 1) croak("Usage: file_touch($path)");
5694 0           path = SvPV_nolen(ST(0));
5695 0 0         ST(0) = file_touch_internal(path) ? &PL_sv_yes : &PL_sv_no;
5696 0           XSRETURN(1);
5697             }
5698              
5699 0           XS_EXTERNAL(XS_file_func_clear_stat_cache) {
5700 0           dXSARGS;
5701 0 0         if (items > 1) croak("Usage: file_clear_stat_cache() or file_clear_stat_cache($path)");
5702            
5703 0 0         if (items == 1 && SvOK(ST(0))) {
    0          
5704 0           const char *path = SvPV_nolen(ST(0));
5705 0           invalidate_stat_cache_path(path);
5706             } else {
5707 0           invalidate_stat_cache();
5708             }
5709            
5710 0           ST(0) = &PL_sv_yes;
5711 0           XSRETURN(1);
5712             }
5713              
5714 0           XS_EXTERNAL(XS_file_func_basename) {
5715 0           dXSARGS;
5716             const char *path;
5717 0 0         if (items != 1) croak("Usage: file_basename($path)");
5718 0           path = SvPV_nolen(ST(0));
5719 0           ST(0) = sv_2mortal(file_basename_internal(aTHX_ path));
5720 0           XSRETURN(1);
5721             }
5722              
5723 0           XS_EXTERNAL(XS_file_func_dirname) {
5724 0           dXSARGS;
5725             const char *path;
5726 0 0         if (items != 1) croak("Usage: file_dirname($path)");
5727 0           path = SvPV_nolen(ST(0));
5728 0           ST(0) = sv_2mortal(file_dirname_internal(aTHX_ path));
5729 0           XSRETURN(1);
5730             }
5731              
5732 0           XS_EXTERNAL(XS_file_func_extname) {
5733 0           dXSARGS;
5734             const char *path;
5735 0 0         if (items != 1) croak("Usage: file_extname($path)");
5736 0           path = SvPV_nolen(ST(0));
5737 0           ST(0) = sv_2mortal(file_extname_internal(aTHX_ path));
5738 0           XSRETURN(1);
5739             }
5740              
5741 0           XS_EXTERNAL(XS_file_func_mtime) {
5742 0           dXSARGS;
5743             const char *path;
5744 0 0         if (items != 1) croak("Usage: file_mtime($path)");
5745 0           path = SvPV_nolen(ST(0));
5746 0           ST(0) = sv_2mortal(newSViv(file_mtime_internal(path)));
5747 0           XSRETURN(1);
5748             }
5749              
5750 0           XS_EXTERNAL(XS_file_func_atime) {
5751 0           dXSARGS;
5752             const char *path;
5753 0 0         if (items != 1) croak("Usage: file_atime($path)");
5754 0           path = SvPV_nolen(ST(0));
5755 0           ST(0) = sv_2mortal(newSViv(file_atime_internal(path)));
5756 0           XSRETURN(1);
5757             }
5758              
5759 0           XS_EXTERNAL(XS_file_func_ctime) {
5760 0           dXSARGS;
5761             const char *path;
5762 0 0         if (items != 1) croak("Usage: file_ctime($path)");
5763 0           path = SvPV_nolen(ST(0));
5764 0           ST(0) = sv_2mortal(newSViv(file_ctime_internal(path)));
5765 0           XSRETURN(1);
5766             }
5767              
5768 0           XS_EXTERNAL(XS_file_func_mode) {
5769 0           dXSARGS;
5770             const char *path;
5771 0 0         if (items != 1) croak("Usage: file_mode($path)");
5772 0           path = SvPV_nolen(ST(0));
5773 0           ST(0) = sv_2mortal(newSViv(file_mode_internal(path)));
5774 0           XSRETURN(1);
5775             }
5776              
5777 0           XS_EXTERNAL(XS_file_func_is_link) {
5778 0           dXSARGS;
5779             const char *path;
5780 0 0         if (items != 1) croak("Usage: file_is_link($path)");
5781 0           path = SvPV_nolen(ST(0));
5782 0 0         ST(0) = file_is_link_internal(path) ? &PL_sv_yes : &PL_sv_no;
5783 0           XSRETURN(1);
5784             }
5785              
5786 0           XS_EXTERNAL(XS_file_func_is_readable) {
5787 0           dXSARGS;
5788             const char *path;
5789 0 0         if (items != 1) croak("Usage: file_is_readable($path)");
5790 0           path = SvPV_nolen(ST(0));
5791 0 0         ST(0) = file_is_readable_internal(path) ? &PL_sv_yes : &PL_sv_no;
5792 0           XSRETURN(1);
5793             }
5794              
5795 0           XS_EXTERNAL(XS_file_func_is_writable) {
5796 0           dXSARGS;
5797             const char *path;
5798 0 0         if (items != 1) croak("Usage: file_is_writable($path)");
5799 0           path = SvPV_nolen(ST(0));
5800 0 0         ST(0) = file_is_writable_internal(path) ? &PL_sv_yes : &PL_sv_no;
5801 0           XSRETURN(1);
5802             }
5803              
5804 0           XS_EXTERNAL(XS_file_func_is_executable) {
5805 0           dXSARGS;
5806             const char *path;
5807 0 0         if (items != 1) croak("Usage: file_is_executable($path)");
5808 0           path = SvPV_nolen(ST(0));
5809 0 0         ST(0) = file_is_executable_internal(path) ? &PL_sv_yes : &PL_sv_no;
5810 0           XSRETURN(1);
5811             }
5812              
5813 0           XS_EXTERNAL(XS_file_func_readdir) {
5814 0           dXSARGS;
5815             const char *path;
5816             AV *result;
5817 0 0         if (items != 1) croak("Usage: file_readdir($path)");
5818 0           path = SvPV_nolen(ST(0));
5819 0           result = file_readdir_internal(aTHX_ path);
5820 0           ST(0) = sv_2mortal(newRV_noinc((SV*)result));
5821 0           XSRETURN(1);
5822             }
5823              
5824 0           XS_EXTERNAL(XS_file_func_slurp_raw) {
5825 0           dXSARGS;
5826             const char *path;
5827 0 0         if (items != 1) croak("Usage: file_slurp_raw($path)");
5828 0           path = SvPV_nolen(ST(0));
5829 0           ST(0) = sv_2mortal(file_slurp_raw_internal(aTHX_ path));
5830 0           XSRETURN(1);
5831             }
5832              
5833 0           XS_EXTERNAL(XS_file_func_copy) {
5834 0           dXSARGS;
5835             const char *src;
5836             const char *dst;
5837 0 0         if (items != 2) croak("Usage: file_copy($src, $dst)");
5838 0           src = SvPV_nolen(ST(0));
5839 0           dst = SvPV_nolen(ST(1));
5840 0 0         ST(0) = file_copy_internal(aTHX_ src, dst) ? &PL_sv_yes : &PL_sv_no;
5841 0           XSRETURN(1);
5842             }
5843              
5844 0           XS_EXTERNAL(XS_file_func_move) {
5845 0           dXSARGS;
5846             const char *src;
5847             const char *dst;
5848 0 0         if (items != 2) croak("Usage: file_move($src, $dst)");
5849 0           src = SvPV_nolen(ST(0));
5850 0           dst = SvPV_nolen(ST(1));
5851 0 0         ST(0) = file_move_internal(aTHX_ src, dst) ? &PL_sv_yes : &PL_sv_no;
5852 0           XSRETURN(1);
5853             }
5854              
5855 0           XS_EXTERNAL(XS_file_func_chmod) {
5856 0           dXSARGS;
5857             const char *path;
5858             int mode;
5859 0 0         if (items != 2) croak("Usage: file_chmod($path, $mode)");
5860 0           path = SvPV_nolen(ST(0));
5861 0           mode = SvIV(ST(1));
5862 0 0         ST(0) = file_chmod_internal(path, mode) ? &PL_sv_yes : &PL_sv_no;
5863 0           XSRETURN(1);
5864             }
5865              
5866 0           XS_EXTERNAL(XS_file_func_append) {
5867 0           dXSARGS;
5868             const char *path;
5869             SV *payload;
5870              
5871 0 0         if (items < 2)
5872 0           croak("Usage: file_append($path, $data [, plugin => ..., key => value ...])");
5873              
5874 0           path = SvPV_nolen(ST(0));
5875 0           payload = ST(1);
5876              
5877 0 0         if (items > 2) {
5878 0           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "append");
5879 0           SV *out = file_plugin_dispatch_write(aTHX_ opts, path, payload);
5880 0           SvREFCNT_dec((SV *)opts);
5881 0 0         if (!out) { ST(0) = &PL_sv_no; XSRETURN(1); }
5882 0           payload = sv_2mortal(out);
5883             }
5884 0 0         ST(0) = file_append_internal(aTHX_ path, payload) ? &PL_sv_yes : &PL_sv_no;
5885 0           XSRETURN(1);
5886             }
5887              
5888 0           XS_EXTERNAL(XS_file_func_atomic_spew) {
5889 0           dXSARGS;
5890             const char *path;
5891             SV *payload;
5892              
5893 0 0         if (items < 2)
5894 0           croak("Usage: file_atomic_spew($path, $data [, plugin => ..., key => value ...])");
5895              
5896 0           path = SvPV_nolen(ST(0));
5897 0           payload = ST(1);
5898              
5899 0 0         if (items > 2) {
5900 0           HV *opts = file_plugin_build_opts(aTHX_ &ST(0), 2, items, "atomic_spew");
5901 0           SV *out = file_plugin_dispatch_write(aTHX_ opts, path, payload);
5902 0           SvREFCNT_dec((SV *)opts);
5903 0 0         if (!out) { ST(0) = &PL_sv_no; XSRETURN(1); }
5904 0           payload = sv_2mortal(out);
5905             }
5906 0 0         ST(0) = file_atomic_spew_internal(aTHX_ path, payload) ? &PL_sv_yes : &PL_sv_no;
5907 0           XSRETURN(1);
5908             }
5909              
5910             /* Function entry for selective import */
5911             typedef struct {
5912             const char *name; /* Name without file_ prefix (e.g., "slurp") */
5913             int args; /* 1 or 2 arguments */
5914             void (*xs_func)(pTHX_ CV*);
5915             Perl_ppaddr_t pp_func;
5916             } ImportEntry;
5917              
5918             static const ImportEntry import_funcs[] = {
5919             /* 1-arg functions */
5920             {"slurp", 1, XS_file_func_slurp, pp_file_slurp},
5921             {"slurp_raw", 1, XS_file_func_slurp_raw, pp_file_slurp_raw},
5922             {"exists", 1, XS_file_func_exists, pp_file_exists},
5923             {"size", 1, XS_file_func_size, pp_file_size},
5924             {"is_file", 1, XS_file_func_is_file, pp_file_is_file},
5925             {"is_dir", 1, XS_file_func_is_dir, pp_file_is_dir},
5926             {"lines", 1, XS_file_func_lines, pp_file_lines},
5927             {"unlink", 1, XS_file_func_unlink, pp_file_unlink},
5928             {"mkdir", 1, XS_file_func_mkdir, pp_file_mkdir},
5929             {"rmdir", 1, XS_file_func_rmdir, pp_file_rmdir},
5930             {"touch", 1, XS_file_func_touch, pp_file_touch},
5931             {"clear_stat_cache", 1, XS_file_func_clear_stat_cache, pp_file_clear_stat_cache},
5932             {"basename", 1, XS_file_func_basename, pp_file_basename},
5933             {"dirname", 1, XS_file_func_dirname, pp_file_dirname},
5934             {"extname", 1, XS_file_func_extname, pp_file_extname},
5935             {"mtime", 1, XS_file_func_mtime, pp_file_mtime},
5936             {"atime", 1, XS_file_func_atime, pp_file_atime},
5937             {"ctime", 1, XS_file_func_ctime, pp_file_ctime},
5938             {"mode", 1, XS_file_func_mode, pp_file_mode},
5939             {"is_link", 1, XS_file_func_is_link, pp_file_is_link},
5940             {"is_readable", 1, XS_file_func_is_readable, pp_file_is_readable},
5941             {"is_writable", 1, XS_file_func_is_writable, pp_file_is_writable},
5942             {"is_executable", 1, XS_file_func_is_executable, pp_file_is_executable},
5943             {"readdir", 1, XS_file_func_readdir, pp_file_readdir},
5944             /* 2-arg functions */
5945             {"spew", 2, XS_file_func_spew, pp_file_spew},
5946             {"copy", 2, XS_file_func_copy, pp_file_copy},
5947             {"move", 2, XS_file_func_move, pp_file_move},
5948             {"chmod", 2, XS_file_func_chmod, pp_file_chmod},
5949             {"append", 2, XS_file_func_append, pp_file_append},
5950             {"atomic_spew", 2, XS_file_func_atomic_spew, pp_file_atomic_spew},
5951             /* variadic functions (args=0 means plain newXS, no custom op) */
5952             {"join", 0, xs_join, NULL},
5953             {"mkpath", 0, xs_mkpath, NULL},
5954             {"rm_rf", 0, xs_rm_rf, NULL},
5955             {"range_lines", 0, xs_range_lines, NULL},
5956             {NULL, 0, NULL, NULL}
5957             };
5958              
5959             #define IMPORT_FUNCS_COUNT (sizeof(import_funcs) / sizeof(import_funcs[0]) - 1)
5960              
5961 155           static void install_import_entry(pTHX_ const char *pkg, const ImportEntry *e) {
5962             char short_name[256];
5963 155           snprintf(short_name, sizeof(short_name), "file_%s", e->name);
5964 155 100         if (e->args == 0) {
5965             /* Variadic: plain newXS, no custom op */
5966             char full_name[256];
5967 20           snprintf(full_name, sizeof(full_name), "%s::%s", pkg, short_name);
5968 20           newXS(full_name, e->xs_func, __FILE__);
5969 135 100         } else if (e->args == 1) {
5970 105           install_file_func_1arg(aTHX_ pkg, short_name, e->xs_func, e->pp_func);
5971             } else {
5972 30           install_file_func_2arg(aTHX_ pkg, short_name, e->xs_func, e->pp_func);
5973             }
5974 155           }
5975              
5976 4           static void install_all_imports(pTHX_ const char *pkg) {
5977             int i;
5978 140 100         for (i = 0; import_funcs[i].name != NULL; i++) {
5979 136           install_import_entry(aTHX_ pkg, &import_funcs[i]);
5980             }
5981 4           }
5982              
5983             /* file::import - import function-style accessors with custom ops */
5984 36           XS_EXTERNAL(XS_file_import) {
5985 36           dXSARGS;
5986             const char *pkg;
5987             int i, j;
5988              
5989             /* Get caller's package */
5990 36 50         pkg = CopSTASHPV(PL_curcop);
    50          
    50          
    50          
    0          
    50          
    50          
5991              
5992             /* No args after package name = no imports */
5993 36 100         if (items <= 1) {
5994 26           XSRETURN_EMPTY;
5995             }
5996              
5997             /* Process each requested import */
5998 29 100         for (i = 1; i < items; i++) {
5999             STRLEN len;
6000 23           const char *arg = SvPV(ST(i), len);
6001              
6002             /* Check for :all or import (both mean import everything) */
6003 23 100         if ((len == 4 && strEQ(arg, ":all")) ||
    100          
6004 21 100         (len == 6 && strEQ(arg, "import"))) {
    100          
6005 4           install_all_imports(aTHX_ pkg);
6006 4           XSRETURN_EMPTY; /* :all means we're done */
6007             }
6008              
6009             /* Look up the requested function */
6010 357 50         for (j = 0; import_funcs[j].name != NULL; j++) {
6011 357 100         if (strEQ(arg, import_funcs[j].name)) {
6012 19           install_import_entry(aTHX_ pkg, &import_funcs[j]);
6013 19           break;
6014             }
6015             }
6016              
6017             /* If not found, warn but don't die */
6018 19 50         if (import_funcs[j].name == NULL) {
6019 0           warn("File::Raw: '%s' is not exported", arg);
6020             }
6021             }
6022              
6023 6           XSRETURN_EMPTY;
6024             }
6025              
6026             /* ============================================
6027             Boot
6028             ============================================ */
6029              
6030 28           XS_EXTERNAL(boot_File__Raw) {
6031 28           dXSBOOTARGSXSAPIVERCHK;
6032             PERL_UNUSED_VAR(items);
6033              
6034 28           file_init(aTHX);
6035              
6036             /* Register custom ops */
6037 28           XopENTRY_set(&file_slurp_xop, xop_name, "file_slurp");
6038 28           XopENTRY_set(&file_slurp_xop, xop_desc, "file slurp");
6039 28           XopENTRY_set(&file_slurp_xop, xop_class, OA_UNOP);
6040 28           Perl_custom_op_register(aTHX_ pp_file_slurp, &file_slurp_xop);
6041              
6042 28           XopENTRY_set(&file_spew_xop, xop_name, "file_spew");
6043 28           XopENTRY_set(&file_spew_xop, xop_desc, "file spew");
6044 28           XopENTRY_set(&file_spew_xop, xop_class, OA_BINOP);
6045 28           Perl_custom_op_register(aTHX_ pp_file_spew, &file_spew_xop);
6046              
6047 28           XopENTRY_set(&file_exists_xop, xop_name, "file_exists");
6048 28           XopENTRY_set(&file_exists_xop, xop_desc, "file exists");
6049 28           XopENTRY_set(&file_exists_xop, xop_class, OA_UNOP);
6050 28           Perl_custom_op_register(aTHX_ pp_file_exists, &file_exists_xop);
6051              
6052 28           XopENTRY_set(&file_size_xop, xop_name, "file_size");
6053 28           XopENTRY_set(&file_size_xop, xop_desc, "file size");
6054 28           XopENTRY_set(&file_size_xop, xop_class, OA_UNOP);
6055 28           Perl_custom_op_register(aTHX_ pp_file_size, &file_size_xop);
6056              
6057 28           XopENTRY_set(&file_is_file_xop, xop_name, "file_is_file");
6058 28           XopENTRY_set(&file_is_file_xop, xop_desc, "file is_file");
6059 28           XopENTRY_set(&file_is_file_xop, xop_class, OA_UNOP);
6060 28           Perl_custom_op_register(aTHX_ pp_file_is_file, &file_is_file_xop);
6061              
6062 28           XopENTRY_set(&file_is_dir_xop, xop_name, "file_is_dir");
6063 28           XopENTRY_set(&file_is_dir_xop, xop_desc, "file is_dir");
6064 28           XopENTRY_set(&file_is_dir_xop, xop_class, OA_UNOP);
6065 28           Perl_custom_op_register(aTHX_ pp_file_is_dir, &file_is_dir_xop);
6066              
6067 28           XopENTRY_set(&file_lines_xop, xop_name, "file_lines");
6068 28           XopENTRY_set(&file_lines_xop, xop_desc, "file lines");
6069 28           XopENTRY_set(&file_lines_xop, xop_class, OA_UNOP);
6070 28           Perl_custom_op_register(aTHX_ pp_file_lines, &file_lines_xop);
6071              
6072 28           XopENTRY_set(&file_unlink_xop, xop_name, "file_unlink");
6073 28           XopENTRY_set(&file_unlink_xop, xop_desc, "file unlink");
6074 28           XopENTRY_set(&file_unlink_xop, xop_class, OA_UNOP);
6075 28           Perl_custom_op_register(aTHX_ pp_file_unlink, &file_unlink_xop);
6076              
6077 28           XopENTRY_set(&file_mkdir_xop, xop_name, "file_mkdir");
6078 28           XopENTRY_set(&file_mkdir_xop, xop_desc, "file mkdir");
6079 28           XopENTRY_set(&file_mkdir_xop, xop_class, OA_UNOP);
6080 28           Perl_custom_op_register(aTHX_ pp_file_mkdir, &file_mkdir_xop);
6081              
6082 28           XopENTRY_set(&file_rmdir_xop, xop_name, "file_rmdir");
6083 28           XopENTRY_set(&file_rmdir_xop, xop_desc, "file rmdir");
6084 28           XopENTRY_set(&file_rmdir_xop, xop_class, OA_UNOP);
6085 28           Perl_custom_op_register(aTHX_ pp_file_rmdir, &file_rmdir_xop);
6086              
6087 28           XopENTRY_set(&file_touch_xop, xop_name, "file_touch");
6088 28           XopENTRY_set(&file_touch_xop, xop_desc, "file touch");
6089 28           XopENTRY_set(&file_touch_xop, xop_class, OA_UNOP);
6090 28           Perl_custom_op_register(aTHX_ pp_file_touch, &file_touch_xop);
6091              
6092 28           XopENTRY_set(&file_clear_stat_cache_xop, xop_name, "file_clear_stat_cache");
6093 28           XopENTRY_set(&file_clear_stat_cache_xop, xop_desc, "clear stat cache");
6094 28           XopENTRY_set(&file_clear_stat_cache_xop, xop_class, OA_BASEOP);
6095 28           Perl_custom_op_register(aTHX_ pp_file_clear_stat_cache, &file_clear_stat_cache_xop);
6096              
6097 28           XopENTRY_set(&file_basename_xop, xop_name, "file_basename");
6098 28           XopENTRY_set(&file_basename_xop, xop_desc, "file basename");
6099 28           XopENTRY_set(&file_basename_xop, xop_class, OA_UNOP);
6100 28           Perl_custom_op_register(aTHX_ pp_file_basename, &file_basename_xop);
6101              
6102 28           XopENTRY_set(&file_dirname_xop, xop_name, "file_dirname");
6103 28           XopENTRY_set(&file_dirname_xop, xop_desc, "file dirname");
6104 28           XopENTRY_set(&file_dirname_xop, xop_class, OA_UNOP);
6105 28           Perl_custom_op_register(aTHX_ pp_file_dirname, &file_dirname_xop);
6106              
6107 28           XopENTRY_set(&file_extname_xop, xop_name, "file_extname");
6108 28           XopENTRY_set(&file_extname_xop, xop_desc, "file extname");
6109 28           XopENTRY_set(&file_extname_xop, xop_class, OA_UNOP);
6110 28           Perl_custom_op_register(aTHX_ pp_file_extname, &file_extname_xop);
6111              
6112 28           XopENTRY_set(&file_mtime_xop, xop_name, "file_mtime");
6113 28           XopENTRY_set(&file_mtime_xop, xop_desc, "file mtime");
6114 28           XopENTRY_set(&file_mtime_xop, xop_class, OA_UNOP);
6115 28           Perl_custom_op_register(aTHX_ pp_file_mtime, &file_mtime_xop);
6116              
6117 28           XopENTRY_set(&file_atime_xop, xop_name, "file_atime");
6118 28           XopENTRY_set(&file_atime_xop, xop_desc, "file atime");
6119 28           XopENTRY_set(&file_atime_xop, xop_class, OA_UNOP);
6120 28           Perl_custom_op_register(aTHX_ pp_file_atime, &file_atime_xop);
6121              
6122 28           XopENTRY_set(&file_ctime_xop, xop_name, "file_ctime");
6123 28           XopENTRY_set(&file_ctime_xop, xop_desc, "file ctime");
6124 28           XopENTRY_set(&file_ctime_xop, xop_class, OA_UNOP);
6125 28           Perl_custom_op_register(aTHX_ pp_file_ctime, &file_ctime_xop);
6126              
6127 28           XopENTRY_set(&file_mode_xop, xop_name, "file_mode");
6128 28           XopENTRY_set(&file_mode_xop, xop_desc, "file mode");
6129 28           XopENTRY_set(&file_mode_xop, xop_class, OA_UNOP);
6130 28           Perl_custom_op_register(aTHX_ pp_file_mode, &file_mode_xop);
6131              
6132 28           XopENTRY_set(&file_is_link_xop, xop_name, "file_is_link");
6133 28           XopENTRY_set(&file_is_link_xop, xop_desc, "file is_link");
6134 28           XopENTRY_set(&file_is_link_xop, xop_class, OA_UNOP);
6135 28           Perl_custom_op_register(aTHX_ pp_file_is_link, &file_is_link_xop);
6136              
6137 28           XopENTRY_set(&file_is_readable_xop, xop_name, "file_is_readable");
6138 28           XopENTRY_set(&file_is_readable_xop, xop_desc, "file is_readable");
6139 28           XopENTRY_set(&file_is_readable_xop, xop_class, OA_UNOP);
6140 28           Perl_custom_op_register(aTHX_ pp_file_is_readable, &file_is_readable_xop);
6141              
6142 28           XopENTRY_set(&file_is_writable_xop, xop_name, "file_is_writable");
6143 28           XopENTRY_set(&file_is_writable_xop, xop_desc, "file is_writable");
6144 28           XopENTRY_set(&file_is_writable_xop, xop_class, OA_UNOP);
6145 28           Perl_custom_op_register(aTHX_ pp_file_is_writable, &file_is_writable_xop);
6146              
6147 28           XopENTRY_set(&file_is_executable_xop, xop_name, "file_is_executable");
6148 28           XopENTRY_set(&file_is_executable_xop, xop_desc, "file is_executable");
6149 28           XopENTRY_set(&file_is_executable_xop, xop_class, OA_UNOP);
6150 28           Perl_custom_op_register(aTHX_ pp_file_is_executable, &file_is_executable_xop);
6151              
6152 28           XopENTRY_set(&file_readdir_xop, xop_name, "file_readdir");
6153 28           XopENTRY_set(&file_readdir_xop, xop_desc, "file readdir");
6154 28           XopENTRY_set(&file_readdir_xop, xop_class, OA_UNOP);
6155 28           Perl_custom_op_register(aTHX_ pp_file_readdir, &file_readdir_xop);
6156              
6157 28           XopENTRY_set(&file_slurp_raw_xop, xop_name, "file_slurp_raw");
6158 28           XopENTRY_set(&file_slurp_raw_xop, xop_desc, "file slurp_raw");
6159 28           XopENTRY_set(&file_slurp_raw_xop, xop_class, OA_UNOP);
6160 28           Perl_custom_op_register(aTHX_ pp_file_slurp_raw, &file_slurp_raw_xop);
6161              
6162 28           XopENTRY_set(&file_copy_xop, xop_name, "file_copy");
6163 28           XopENTRY_set(&file_copy_xop, xop_desc, "file copy");
6164 28           XopENTRY_set(&file_copy_xop, xop_class, OA_BINOP);
6165 28           Perl_custom_op_register(aTHX_ pp_file_copy, &file_copy_xop);
6166              
6167 28           XopENTRY_set(&file_move_xop, xop_name, "file_move");
6168 28           XopENTRY_set(&file_move_xop, xop_desc, "file move");
6169 28           XopENTRY_set(&file_move_xop, xop_class, OA_BINOP);
6170 28           Perl_custom_op_register(aTHX_ pp_file_move, &file_move_xop);
6171              
6172 28           XopENTRY_set(&file_chmod_xop, xop_name, "file_chmod");
6173 28           XopENTRY_set(&file_chmod_xop, xop_desc, "file chmod");
6174 28           XopENTRY_set(&file_chmod_xop, xop_class, OA_BINOP);
6175 28           Perl_custom_op_register(aTHX_ pp_file_chmod, &file_chmod_xop);
6176              
6177 28           XopENTRY_set(&file_append_xop, xop_name, "file_append");
6178 28           XopENTRY_set(&file_append_xop, xop_desc, "file append");
6179 28           XopENTRY_set(&file_append_xop, xop_class, OA_BINOP);
6180 28           Perl_custom_op_register(aTHX_ pp_file_append, &file_append_xop);
6181              
6182 28           XopENTRY_set(&file_atomic_spew_xop, xop_name, "file_atomic_spew");
6183 28           XopENTRY_set(&file_atomic_spew_xop, xop_desc, "file atomic_spew");
6184 28           XopENTRY_set(&file_atomic_spew_xop, xop_class, OA_BINOP);
6185 28           Perl_custom_op_register(aTHX_ pp_file_atomic_spew, &file_atomic_spew_xop);
6186              
6187             /* Install functions with call checker for custom op optimization */
6188             {
6189             CV *cv;
6190             SV *ckobj;
6191              
6192             /* 1-arg functions with call checker */
6193 28           cv = newXS("File::Raw::size", xs_size, __FILE__);
6194 28           ckobj = newSViv(PTR2IV(pp_file_size));
6195 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6196              
6197 28           cv = newXS("File::Raw::mtime", xs_mtime, __FILE__);
6198 28           ckobj = newSViv(PTR2IV(pp_file_mtime));
6199 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6200              
6201 28           cv = newXS("File::Raw::atime", xs_atime, __FILE__);
6202 28           ckobj = newSViv(PTR2IV(pp_file_atime));
6203 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6204              
6205 28           cv = newXS("File::Raw::ctime", xs_ctime, __FILE__);
6206 28           ckobj = newSViv(PTR2IV(pp_file_ctime));
6207 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6208              
6209 28           cv = newXS("File::Raw::mode", xs_mode, __FILE__);
6210 28           ckobj = newSViv(PTR2IV(pp_file_mode));
6211 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6212              
6213 28           cv = newXS("File::Raw::exists", xs_exists, __FILE__);
6214 28           ckobj = newSViv(PTR2IV(pp_file_exists));
6215 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6216              
6217 28           cv = newXS("File::Raw::is_file", xs_is_file, __FILE__);
6218 28           ckobj = newSViv(PTR2IV(pp_file_is_file));
6219 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6220              
6221 28           cv = newXS("File::Raw::is_dir", xs_is_dir, __FILE__);
6222 28           ckobj = newSViv(PTR2IV(pp_file_is_dir));
6223 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6224              
6225 28           cv = newXS("File::Raw::is_link", xs_is_link, __FILE__);
6226 28           ckobj = newSViv(PTR2IV(pp_file_is_link));
6227 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6228              
6229 28           cv = newXS("File::Raw::is_readable", xs_is_readable, __FILE__);
6230 28           ckobj = newSViv(PTR2IV(pp_file_is_readable));
6231 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6232              
6233 28           cv = newXS("File::Raw::is_writable", xs_is_writable, __FILE__);
6234 28           ckobj = newSViv(PTR2IV(pp_file_is_writable));
6235 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6236              
6237 28           cv = newXS("File::Raw::is_executable", xs_is_executable, __FILE__);
6238 28           ckobj = newSViv(PTR2IV(pp_file_is_executable));
6239 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6240              
6241             /* File manipulation - 1-arg */
6242 28           cv = newXS("File::Raw::unlink", xs_unlink, __FILE__);
6243 28           ckobj = newSViv(PTR2IV(pp_file_unlink));
6244 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6245              
6246 28           cv = newXS("File::Raw::mkdir", xs_mkdir, __FILE__);
6247 28           ckobj = newSViv(PTR2IV(pp_file_mkdir));
6248 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6249              
6250 28           cv = newXS("File::Raw::rmdir", xs_rmdir, __FILE__);
6251 28           ckobj = newSViv(PTR2IV(pp_file_rmdir));
6252 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6253              
6254 28           cv = newXS("File::Raw::touch", xs_touch, __FILE__);
6255 28           ckobj = newSViv(PTR2IV(pp_file_touch));
6256 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6257              
6258 28           cv = newXS("File::Raw::clear_stat_cache", xs_clear_stat_cache, __FILE__);
6259 28           ckobj = newSViv(PTR2IV(pp_file_clear_stat_cache));
6260 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6261              
6262 28           cv = newXS("File::Raw::basename", xs_basename, __FILE__);
6263 28           ckobj = newSViv(PTR2IV(pp_file_basename));
6264 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6265              
6266 28           cv = newXS("File::Raw::dirname", xs_dirname, __FILE__);
6267 28           ckobj = newSViv(PTR2IV(pp_file_dirname));
6268 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6269              
6270 28           cv = newXS("File::Raw::extname", xs_extname, __FILE__);
6271 28           ckobj = newSViv(PTR2IV(pp_file_extname));
6272 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6273              
6274 28           cv = newXS("File::Raw::slurp", xs_slurp, __FILE__);
6275 28           ckobj = newSViv(PTR2IV(pp_file_slurp));
6276 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6277              
6278 28           cv = newXS("File::Raw::slurp_raw", xs_slurp_raw, __FILE__);
6279 28           ckobj = newSViv(PTR2IV(pp_file_slurp_raw));
6280 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6281              
6282 28           cv = newXS("File::Raw::lines", xs_lines, __FILE__);
6283 28           ckobj = newSViv(PTR2IV(pp_file_lines));
6284 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6285              
6286 28           cv = newXS("File::Raw::readdir", xs_readdir, __FILE__);
6287 28           ckobj = newSViv(PTR2IV(pp_file_readdir));
6288 28           cv_set_call_checker(cv, file_call_checker_1arg, ckobj);
6289              
6290             /* 2-arg functions with call checker */
6291 28           cv = newXS("File::Raw::spew", xs_spew, __FILE__);
6292 28           ckobj = newSViv(PTR2IV(pp_file_spew));
6293 28           cv_set_call_checker(cv, file_call_checker_2arg, ckobj);
6294              
6295 28           cv = newXS("File::Raw::append", xs_append, __FILE__);
6296 28           ckobj = newSViv(PTR2IV(pp_file_append));
6297 28           cv_set_call_checker(cv, file_call_checker_2arg, ckobj);
6298              
6299 28           cv = newXS("File::Raw::copy", xs_copy, __FILE__);
6300 28           ckobj = newSViv(PTR2IV(pp_file_copy));
6301 28           cv_set_call_checker(cv, file_call_checker_2arg, ckobj);
6302              
6303 28           cv = newXS("File::Raw::move", xs_move, __FILE__);
6304 28           ckobj = newSViv(PTR2IV(pp_file_move));
6305 28           cv_set_call_checker(cv, file_call_checker_2arg, ckobj);
6306              
6307 28           cv = newXS("File::Raw::chmod", xs_chmod, __FILE__);
6308 28           ckobj = newSViv(PTR2IV(pp_file_chmod));
6309 28           cv_set_call_checker(cv, file_call_checker_2arg, ckobj);
6310              
6311 28           cv = newXS("File::Raw::atomic_spew", xs_atomic_spew, __FILE__);
6312 28           ckobj = newSViv(PTR2IV(pp_file_atomic_spew));
6313 28           cv_set_call_checker(cv, file_call_checker_2arg, ckobj);
6314             }
6315              
6316             /* Functions without custom op optimization */
6317 28           newXS("File::Raw::join", xs_join, __FILE__);
6318 28           newXS("File::Raw::mkpath", xs_mkpath, __FILE__);
6319 28           newXS("File::Raw::rm_rf", xs_rm_rf, __FILE__);
6320 28           newXS("File::Raw::each_line", xs_each_line, __FILE__);
6321 28           newXS("File::Raw::grep_lines", xs_grep_lines, __FILE__);
6322 28           newXS("File::Raw::count_lines", xs_count_lines, __FILE__);
6323 28           newXS("File::Raw::find_line", xs_find_line, __FILE__);
6324 28           newXS("File::Raw::map_lines", xs_map_lines, __FILE__);
6325              
6326             /* Plugin API */
6327 28           newXS("File::Raw::register_plugin", xs_register_plugin, __FILE__);
6328 28           newXS("File::Raw::unregister_plugin", xs_unregister_plugin, __FILE__);
6329 28           newXS("File::Raw::list_plugins", xs_list_plugins, __FILE__);
6330 28           newXS("File::Raw::register_predicate", xs_register_predicate, __FILE__);
6331 28           newXS("File::Raw::list_predicates", xs_list_predicates, __FILE__);
6332              
6333             /* Built-in 'predicate' plugin: routes the eight built-in line
6334             * predicates and any user-registered ones through plugin dispatch.
6335             * Initialise the predicate storage first so the plugin's record fn
6336             * always sees a populated registry. */
6337 28           file_init_callback_registry(aTHX);
6338 28 50         if (file_register_plugin(aTHX_ &g_predicate_plugin) != 1) {
6339 0           croak("File::Raw boot: failed to register built-in 'predicate' plugin");
6340             }
6341              
6342             /* Combined stat - all attributes in one syscall */
6343 28           newXS("File::Raw::stat", xs_stat_all, __FILE__);
6344              
6345             /* Head and tail */
6346 28           newXS("File::Raw::head", xs_head, __FILE__);
6347 28           newXS("File::Raw::tail", xs_tail, __FILE__);
6348 28           newXS("File::Raw::range_lines", xs_range_lines, __FILE__);
6349              
6350             /* Import function */
6351 28           newXS("File::Raw::import", XS_file_import, __FILE__);
6352              
6353             /* Memory-mapped files */
6354 28           newXS("File::Raw::mmap_open", xs_mmap_open, __FILE__);
6355 28           newXS("File::Raw::mmap::data", xs_mmap_data, __FILE__);
6356 28           newXS("File::Raw::mmap::sync", xs_mmap_sync, __FILE__);
6357 28           newXS("File::Raw::mmap::close", xs_mmap_close, __FILE__);
6358 28           newXS("File::Raw::mmap::DESTROY", xs_mmap_DESTROY, __FILE__);
6359              
6360             /* Line iterators */
6361 28           newXS("File::Raw::lines_iter", xs_lines_iter, __FILE__);
6362 28           newXS("File::Raw::lines::next", xs_lines_iter_next, __FILE__);
6363 28           newXS("File::Raw::lines::eof", xs_lines_iter_eof, __FILE__);
6364 28           newXS("File::Raw::lines::close", xs_lines_iter_close, __FILE__);
6365 28           newXS("File::Raw::lines::DESTROY", xs_lines_iter_DESTROY, __FILE__);
6366              
6367             /* Register cleanup for global destruction */
6368 28           Perl_call_atexit(aTHX_ file_cleanup_callback_registry, NULL);
6369              
6370             #if PERL_REVISION > 5 || (PERL_REVISION == 5 && PERL_VERSION >= 22)
6371 28           Perl_xs_boot_epilog(aTHX_ ax);
6372             #else
6373             XSRETURN_YES;
6374             #endif
6375 28           }