zpmod  b19981f
High-performance Zsh module for script optimization and filesystem helpers
fs.c
Go to the documentation of this file.
1 /* SPDX-License-Identifier: MIT */
6 /* Canonical include order: module mdh/pro provide vendor + config context */
7 #include "zpmod.mdh"
8 #include "zpmod.pro"
9 #include "zpmod_vendor_shims.h"
10 #include <dirent.h>
11 #include <errno.h>
12 #include <fcntl.h>
13 #include <limits.h>
14 #include <stdio.h>
15 #include <string.h>
16 #include <sys/stat.h>
17 #include <unistd.h>
18 #if defined(__has_include)
19 #if __has_include(<sys/mman.h>)
20 #include <sys/mman.h>
21 #define ZPMOD_HAVE_MMAP 1
22 #endif
23 #endif
24 #include "zpmod_fs.h"
25 
26 
27 #if defined(ZSH_VERSION) && defined(HAVE_GETAPARAM)
28 #define get_zpmod_config(K) getaparam("ZPMOD", (K))
29 #else
30 #define GET_ZPMOD_CONFIG(K) getsparam("ZPMOD_" #K)
31 #endif
32 
34 /* (nam, outname, inname, follow, fields) — see header for parameter intent */
35 // NOLINTBEGIN(bugprone-easily-swappable-parameters)
36 int zp_pathstat_core(char *nam /* builtin name */,
37  char *outname /* output array name */,
38  char *inname /* input array name */,
39  int follow /* follow symlinks (stat vs lstat) */,
40  char *fields /* field filter tokens */) {
41  char **inarr = getaparam(inname);
42  if (!inarr) {
43  zwarnnam(nam, "%s: input must be an indexed array", inname);
44  return 1;
45  }
46  const int want_type = (!fields || strstr(fields, "type"));
47  const int want_size = (!fields || strstr(fields, "size"));
48  const int want_mode = (!fields || strstr(fields, "mode"));
49  const int want_mtime = (!fields || strstr(fields, "mtime"));
50  const int want_uid = (!fields || strstr(fields, "uid"));
51  const int want_gid = (!fields || strstr(fields, "gid"));
52  const int want_ino = (!fields || strstr(fields, "ino"));
53  const int want_nlink = (!fields || strstr(fields, "nlink"));
54 
55  unsetparam(outname);
56  size_t in_count = 0;
57  for (int i = 0; inarr[i]; ++i) {
58  ++in_count;
59  }
60  char **out = (char **)zalloc((in_count + 1) * sizeof(char *));
61  out[0] = NULL;
62  setaparam(outname, out);
63 
64  struct stat st;
65  int idx = 1;
66  for (int i = 0; inarr[i]; ++i) {
67  int p_len = 0;
68  char *p_in = zp_unmetafy_zalloc(inarr[i], &p_len);
69  if (!p_in) {
70  zwarnnam(nam, "oom");
71  return 1;
72  }
73 
74  int rc = follow ? stat(p_in, &st) : lstat(p_in, &st);
75  char buf[512];
76  int off = 0;
77  if (rc == 0) {
78  off += snprintf(buf + off, (int)sizeof(buf) - off, "path=%s", p_in);
79  if (want_type) {
80  char t = '?';
81  if (S_ISREG(st.st_mode)) {
82  t = 'f';
83  } else if (S_ISDIR(st.st_mode)) {
84  t = 'd';
85  } else if (S_ISLNK(st.st_mode)) {
86  t = 'l';
87  }
88  off += snprintf(buf + off, (int)sizeof(buf) - off, ",type=%c", t);
89  }
90  if (want_size) {
91  off += snprintf(buf + off, (int)sizeof(buf) - off, ",size=%ld",
92  (long)st.st_size);
93  }
94  if (want_mode) {
95  off += snprintf(buf + off, (int)sizeof(buf) - off, ",mode=%o",
96  (unsigned)(st.st_mode & 07777));
97  }
98  if (want_mtime) {
99  off += snprintf(buf + off, (int)sizeof(buf) - off, ",mtime=%ld",
100  (long)st.st_mtime);
101  }
102  if (want_uid) {
103  off += snprintf(buf + off, (int)sizeof(buf) - off, ",uid=%ld",
104  (long)st.st_uid);
105  }
106  if (want_gid) {
107  off += snprintf(buf + off, (int)sizeof(buf) - off, ",gid=%ld",
108  (long)st.st_gid);
109  }
110  if (want_ino) {
111  off += snprintf(buf + off, (int)sizeof(buf) - off, ",ino=%ld",
112  (long)st.st_ino);
113  }
114  if (want_nlink) {
115  off += snprintf(buf + off, (int)sizeof(buf) - off, ",nlink=%ld",
116  (long)st.st_nlink);
117  }
118  } else {
119  off += snprintf(buf + off, (int)sizeof(buf) - off, "path=%s", p_in);
120  if (want_type) {
121  off += snprintf(buf + off, (int)sizeof(buf) - off, ",type=%c", '?');
122  }
123  off += snprintf(buf + off, (int)sizeof(buf) - off, ",errno=%d", errno);
124  }
125  buf[sizeof(buf) - 1] = '\0';
126  /* Make 'off' observable to silence static analyzer dead store warnings. */
127  volatile int zpmod_sink_off = off;
128  (void)zpmod_sink_off;
129  int used = (int)strlen(buf);
130  char *outstr = metafy(buf, used, META_DUP);
131  char indexed[256];
132  snprintf(indexed, sizeof(indexed), "%s[%d]", outname, idx++);
133  setsparam(indexed, outstr);
134  zfree(p_in, p_len + 1);
135  }
136  return 0;
137 }
138 // NOLINTEND(bugprone-easily-swappable-parameters)
139 
140 /* ---------------------- optional lightweight caches ---------------------- */
141 /* Very small, conservative caches guarded by env toggles. Intent is to avoid
142  * repeated stat/dir-list work during startup, not a general purpose cache. */
143 
144 static int zp_fs_cache_enabled(void) {
145  const char *e = GET_ZPMOD_CONFIG(FS_CACHE);
146  return e && *e && (*e != '0');
147 }
148 
149 typedef struct {
150  dev_t dev;
151  ino_t ino;
152  time_t mtime;
153  off_t size; /* identity */
154  int is_dir; /* for dir-list */
155  char *dir_entries; /* serialized names, metafied; NULL if not cached */
157 
158 #define ZP_FS_CACHE_MAX 64
160 static int zp_fs_cache_count = 0;
161 
162 static int zp_fs_cache_lookup(const char *dir, struct stat *st, int *out_idx) {
163  (void)dir;
164  for (int i = 0; i < zp_fs_cache_count; ++i) {
166  if (!e->is_dir) {
167  continue;
168  }
169  if (e->dev == st->st_dev && e->ino == st->st_ino &&
170  e->mtime == st->st_mtime && e->size == st->st_size) {
171  *out_idx = i;
172  return 1;
173  }
174  }
175  return 0;
176 }
177 
178 static int zp_fs_cache_insert_dir(const char *dir, const struct stat *st,
179  char *serialized) {
180  (void)dir;
183  e->dev = st->st_dev;
184  e->ino = st->st_ino;
185  e->mtime = st->st_mtime;
186  e->size = st->st_size;
187  e->is_dir = 1;
188  e->dir_entries = serialized;
189  return zp_fs_cache_count - 1;
190  }
191  /* Simple FIFO eviction */
192  zp_fs_cache_entry *ev = &zp_fs_cache[0];
193  if (ev->dir_entries) {
194  zsfree(ev->dir_entries);
195  }
196  for (int i = 1; i < ZP_FS_CACHE_MAX; ++i) {
197  zp_fs_cache[i - 1] = zp_fs_cache[i];
198  }
200  e->dev = st->st_dev;
201  e->ino = st->st_ino;
202  e->mtime = st->st_mtime;
203  e->size = st->st_size;
204  e->is_dir = 1;
205  e->dir_entries = serialized;
206  return ZP_FS_CACHE_MAX - 1;
207 }
208 
209 /* Hook dirlist_core to consult/emit cache. */
210 /* We inject minimal logic by wrapping the parameter setting path. */
211 
212 /* Redefine function with caching logic by providing a weak alias if toolchain
213  * allows; otherwise inline the logic near the end. Simpler: append a helper
214  * to be called by callers; however, we can’t change public API now. */
215 
216 /* ----------------------------- path warmup ------------------------------ */
217 
253 // NOLINTBEGIN(bugprone-easily-swappable-parameters)
254 int zp_path_warmup_core(const char *nam, int quiet, int prune_missing,
255  int dry_run) {
256  (void)nam; /* currently unused */
257  char **p = getaparam("path");
258  if (!p || !*p) {
259  return 0;
260  }
261 
262  long total_exec = 0;
263  long plen = (long)arrlen(p);
264 
265  if (!quiet) {
266  fprintf(stderr, "zpmod: path-warmup scanning %ld directories...\n", plen);
267  }
268 
269  /* Intentionally not touching the command hash table here; warming path only.
270  */
271  for (int i = 0; p[i]; ++i) {
272  char *dir = p[i];
273  if (!dir || !*dir) {
274  continue;
275  }
276  DIR *dp = opendir(dir);
277  if (!dp) {
278  continue;
279  }
280  struct dirent *de;
281  struct stat st;
282  char full[PATH_MAX];
283  while ((de = readdir(dp)) != NULL) {
284  const char *name = de->d_name;
285  if (!name || name[0] == '.') {
286  continue;
287  }
288  int n = snprintf(full, sizeof(full), "%s/%s", dir, name);
289  if (n <= 0 || (size_t)n >= sizeof(full)) {
290  continue;
291  }
292  if (stat(full, &st) == 0 && S_ISREG(st.st_mode) && (st.st_mode & 0111)) {
293  ++total_exec; /* touched */
294  }
295  }
296  closedir(dp);
297  }
298 
299  if (prune_missing) {
300  /*
301  * SAFE PATH PRUNING IMPLEMENTATION
302  *
303  * This implementation avoids the memory corruption that occurred in earlier
304  * versions when attempting to modify the path array in-place or reuse
305  * getaparam() results.
306  *
307  * Key safety principles:
308  * 1. Never modify arrays returned by getaparam() - they're owned by zsh's
309  * parameter system
310  * 2. Use separate allocation with ztrdup() for string ownership
311  * 3. Build completely new array rather than in-place modification
312  * 4. Avoid getaparam() dependency chains that can create circular
313  * references
314  */
315 
316  /* Phase 1: Count valid directories and report pruning actions */
317  int valid_count = 0;
318  for (int i = 0; p[i]; ++i) {
319  char *dir = p[i];
320  if (!dir || !*dir) {
321  continue; /* Skip empty entries */
322  }
323  struct stat check_st;
324  if (stat(dir, &check_st) == 0 && S_ISDIR(check_st.st_mode)) {
325  valid_count++;
326  } else if (!quiet) {
327  /* Report pruning action (works for both dry-run and actual pruning) */
328  fprintf(stderr, "zpmod: path-warmup %s missing directory: %s\n",
329  dry_run ? "would prune" : "pruning", dir);
330  }
331  }
332 
333  /* Phase 2: Rebuild path array if pruning is needed and not in dry-run mode
334  */
335  if (!dry_run && valid_count < (int)plen) {
336  /*
337  * Allocate new array with exact size needed.
338  * Using zalloc() ensures compatibility with zsh's memory management.
339  */
340  char **new_path = (char **)zalloc((valid_count + 1) * sizeof(char *));
341  int new_idx = 0;
342 
343  /* Copy only valid directories, creating new string instances */
344  for (int i = 0; p[i]; ++i) {
345  char *dir = p[i];
346  if (!dir || !*dir) {
347  continue;
348  }
349  struct stat check_st;
350  if (stat(dir, &check_st) == 0 && S_ISDIR(check_st.st_mode)) {
351  /*
352  * Critical: Use ztrdup() to create independent string copy.
353  * This ensures the new array has proper ownership and doesn't
354  * depend on the lifetime of the original getaparam() result.
355  */
356  new_path[new_idx++] = ztrdup(dir);
357  }
358  }
359  new_path[new_idx] = NULL; /* Null-terminate array */
360 
361  /*
362  * Safe parameter update: setaparam() takes ownership of the new array.
363  * This doesn't create conflicts with the original getaparam() result
364  * because we're providing a completely independent array.
365  */
366  setaparam("path", new_path);
367  }
368  }
369 
370  if (!quiet) {
371  fprintf(stderr, "zpmod: path-warmup touched %ld executables.\n",
372  total_exec);
373  }
374  return (int)total_exec;
375 }
376 // NOLINTEND(bugprone-easily-swappable-parameters)
377 
379 /* (nam, outname, dir, inc_all, only_dirs, only_files) — see header for
380  * parameter intent */
381 // NOLINTBEGIN(bugprone-easily-swappable-parameters)
382 int zp_dirlist_core(char *nam /* builtin name */,
383  char *outname /* output array name */,
384  char *dir /* directory to list */,
385  int inc_all /* include dotfiles */,
386  int only_dirs /* restrict to directories */,
387  int only_files /* restrict to regular files */) {
388  int dlen = 0;
389  char *udir = zp_unmetafy_zalloc(dir, &dlen);
390  if (!udir) {
391  zwarnnam(nam, "oom");
392  return 1;
393  }
394  DIR *dp = opendir(udir);
395  if (!dp) {
396  int e = errno;
397  zfree(udir, dlen + 1);
398  zwarnnam(nam, "%s: %e", dir, e);
399  return 1;
400  }
401  unsetparam(outname);
402  char **out = (char **)zalloc(sizeof(char *));
403  out[0] = NULL;
404  setaparam(outname, out);
405 
406  struct dirent *de;
407  struct stat st;
408  int idx = 1;
409  while ((de = readdir(dp)) != NULL) {
410  const char *name = de->d_name;
411  if (!inc_all && name[0] == '.') {
412  continue;
413  }
414 
415  char full[PATH_MAX];
416  int n = snprintf(full, sizeof(full), "%s/%s", udir, name);
417  if (n <= 0 || (size_t)n >= sizeof(full)) {
418  continue;
419  }
420  if (lstat(full, &st) != 0) {
421  continue;
422  }
423  if (only_dirs && !S_ISDIR(st.st_mode)) {
424  continue;
425  }
426  if (only_files && !S_ISREG(st.st_mode)) {
427  continue;
428  }
429 
430  char indexed[256];
431  snprintf(indexed, sizeof(indexed), "%s[%d]", outname, idx++);
432  setsparam(indexed, metafy((char *)name, (int)strlen(name), META_DUP));
433  }
434  closedir(dp);
435  zfree(udir, dlen + 1);
436  return 0;
437 }
438 // NOLINTEND(bugprone-easily-swappable-parameters)
439 
441 /* (nam, outname, path, use_mmap, split, delim) — see header for parameter
442  * intent */
443 // NOLINTBEGIN(bugprone-easily-swappable-parameters)
444 int zp_readfile_core(char *nam /* builtin name */,
445  char *outname /* scalar/array target name */,
446  char *path /* file path */,
447  int use_mmap /* prefer mmap when available */,
448  int split /* split output into array */,
449  int delim /* delimiter used when split=1 */) {
450  int plen = 0;
451  char *upath = zp_unmetafy_zalloc(path, &plen);
452  if (!upath) {
453  zwarnnam(nam, "oom");
454  return 1;
455  }
456  int fd = open(upath, O_RDONLY);
457  if (fd < 0) {
458  zwarnnam(nam, "%s: %e", path, errno);
459  return 1;
460  }
461  struct stat st;
462  if (fstat(fd, &st) != 0) {
463  int e = errno;
464  close(fd);
465  zwarnnam(nam, "%s: %e", path, e);
466  return 1;
467  }
468  size_t sz = (size_t)st.st_size;
469  char *buf = NULL;
470  size_t cap = 0;
471 #ifdef ZPMOD_HAVE_MMAP
472  if (use_mmap && sz > 0) {
473  void *m = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd, 0);
474  if (m != MAP_FAILED) {
475  buf = (char *)m;
476  cap = sz;
477  }
478  }
479 #endif
480  if (!buf) {
481  cap = sz ? sz + 1 : 4096;
482  buf = (char *)zalloc(cap);
483  if (!buf) {
484  int e = errno;
485  close(fd);
486  zwarnnam(nam, "oom: %e", e);
487  return 1;
488  }
489  size_t off = 0;
490  ssize_t rd;
491  while ((rd = read(fd, buf + off, cap - off)) > 0) {
492  off += (size_t)rd;
493  if (off == cap) {
494  size_t ncap = cap * 2;
495  char *nb = (char *)zrealloc(buf, ncap);
496  if (!nb) {
497  int e = errno;
498  zfree(buf, cap);
499  close(fd);
500  zwarnnam(nam, "oom: %e", e);
501  return 1;
502  }
503  buf = nb;
504  cap = ncap;
505  }
506  }
507  if (rd < 0) {
508  int e = errno;
509  zfree(buf, cap);
510  close(fd);
511  zwarnnam(nam, "%s: %e", path, e);
512  return 1;
513  }
514  sz = off;
515  }
516  close(fd);
517  zfree(upath, plen + 1);
518 
519  if (!split) {
520  unsetparam(outname);
521  setsparam(outname, metafy(buf, (int)sz, META_DUP));
522 #ifdef ZPMOD_HAVE_MMAP
523  if (use_mmap && cap == sz) {
524  munmap(buf, sz);
525  } else {
526  zfree(buf, cap);
527  }
528 #else
529  zfree(buf, cap);
530 #endif
531  return 0;
532  }
533 
534  unsetparam(outname);
535  char **out = (char **)zalloc(sizeof(char *));
536  out[0] = NULL;
537  setaparam(outname, out);
538  int idx = 1;
539  size_t start = 0;
540  for (size_t i = 0; i < sz; ++i) {
541  if ((unsigned char)buf[i] == (unsigned char)delim) {
542  int len = (int)(i - start);
543  char *rec = metafy(buf + start, len, META_DUP);
544  char indexed[256];
545  snprintf(indexed, sizeof(indexed), "%s[%d]", outname, idx++);
546  setsparam(indexed, rec);
547  if ((unsigned char)delim == (unsigned char)'\r' && (i + 1) < sz &&
548  (unsigned char)buf[i + 1] == (unsigned char)'\n') {
549  start = i + 2;
550  ++i;
551  } else {
552  start = i + 1;
553  }
554  }
555  }
556  if (start < sz) {
557  int len = (int)(sz - start);
558  char *rec = metafy(buf + start, len, META_DUP);
559  char indexed[256];
560  snprintf(indexed, sizeof(indexed), "%s[%d]", outname, idx++);
561  setsparam(indexed, rec);
562  }
563 #ifdef ZPMOD_HAVE_MMAP
564  if (use_mmap && cap == sz) {
565  munmap(buf, sz);
566  } else {
567  zfree(buf, cap);
568  }
569 #else
570  zfree(buf, cap);
571 #endif
572  return 0;
573 }
574 // NOLINTEND(bugprone-easily-swappable-parameters)
#define PATH_MAX
Definition: bundle_build.c:26
static int zp_fs_cache_count
Definition: fs.c:160
static int zp_fs_cache_enabled(void)
Definition: fs.c:144
#define ZP_FS_CACHE_MAX
Definition: fs.c:158
static int zp_fs_cache_insert_dir(const char *dir, const struct stat *st, char *serialized)
Definition: fs.c:178
int zp_path_warmup_core(const char *nam, int quiet, int prune_missing, int dry_run)
Implements path-warmup functionality for executable discovery and path pruning.
Definition: fs.c:254
int zp_readfile_core(char *nam, char *outname, char *path, int use_mmap, int split, int delim)
See zpmod_fs.h for contract.
Definition: fs.c:444
#define GET_ZPMOD_CONFIG(K)
Definition: fs.c:30
int zp_dirlist_core(char *nam, char *outname, char *dir, int inc_all, int only_dirs, int only_files)
See zpmod_fs.h for contract.
Definition: fs.c:382
int zp_pathstat_core(char *nam, char *outname, char *inname, int follow, char *fields)
See zpmod_fs.h for contract.
Definition: fs.c:36
static zp_fs_cache_entry zp_fs_cache[ZP_FS_CACHE_MAX]
Definition: fs.c:159
static int zp_fs_cache_lookup(const char *dir, struct stat *st, int *out_idx)
Definition: fs.c:162
Definition: fs.c:149
char * dir_entries
Definition: fs.c:155
int is_dir
Definition: fs.c:154
dev_t dev
Definition: fs.c:150
ino_t ino
Definition: fs.c:151
time_t mtime
Definition: fs.c:152
off_t size
Definition: fs.c:153
char * zp_unmetafy_zalloc(const char *to_copy, int *new_len)
Duplicate and unmetafy a zsh string with zalloc; see header for details.
Definition: utils.c:76
Module declaration header (mdh) for zpmod.
Prototype stub for zpmod when building out-of-tree.
Filesystem helpers used by builtins and zpmod subcommands.
void * zalloc(size_t size)
void zsfree(char *ptr)
void unsetparam(const char *name)
char * metafy(char *s, int len, int how)
void setaparam(const char *name, char **value)
void zfree(void *ptr, size_t size)
char ** getaparam(const char *name)
void setsparam(const char *name, char *value)
void * zrealloc(void *ptr, size_t size)
void zwarnnam(const char *, const char *,...)
char ** path
int arrlen(char **)
char * ztrdup(const char *)
Local, non-invasive shims to suppress benign vendor header warnings.