File Coverage

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