zpmod  b19981f
High-performance Zsh module for script optimization and filesystem helpers
fpath.c
Go to the documentation of this file.
1 /* SPDX-License-Identifier: MIT */
6 /* Canonical module header ordering */
7 #include "zpmod.mdh"
8 #include "zpmod.pro"
9 #include "zpmod_fpath.h"
10 #include "zpmod_vendor_shims.h"
11 #include <dirent.h>
12 #include <stdio.h>
13 #include <string.h>
14 #include <sys/stat.h>
15 
56 // NOLINTBEGIN(readability-function-cognitive-complexity)
57 int cmd_fpath_index(char *nam, char **argv) {
58  const char *outfile = NULL;
59  int rebuild = 0;
60  int preload = 0;
61 
62  while (*argv && argv[0][0] == '-') {
63  if (strcmp(argv[0], "--rebuild") == 0) {
64  rebuild = 1;
65  argv++;
66  continue;
67  }
68  if (strcmp(argv[0], "--preload") == 0) {
69  preload = 1;
70  argv++;
71  continue;
72  }
73  if (strcmp(argv[0], "--out") == 0) {
74  if (!argv[1]) {
75  zwarnnam(nam, "--out requires an argument");
76  return 1;
77  }
78  outfile = argv[1];
79  argv += 2;
80  continue;
81  }
82  if (strcmp(argv[0], "--") == 0) {
83  argv++;
84  break;
85  }
86  break;
87  }
88 
89  char **fpath = getaparam("fpath");
90  if (!fpath) {
91  return 0;
92  }
93 
94  /*
95  * INTELLIGENT SKIP DETECTION
96  *
97  * Attempts to detect when the existing index is still fresh and can be
98  * reused, avoiding expensive directory scanning when FPATH directories
99  * haven't changed.
100  *
101  * This optimization is crucial for startup performance in environments with
102  * large FPATH configurations or slow filesystem access.
103  */
104  if (outfile && !rebuild) {
105  FILE *rf = fopen(outfile, "r");
106  if (rf) {
107  char line[2048];
108  int header_ok = 0;
109 
110  /* Verify index format version */
111  if (fgets(line, sizeof(line), rf) &&
112  strncmp(line, "# zpmod fpath-index v1", 22) == 0) {
113  /*
114  * HEADER VALIDATION STRATEGY
115  *
116  * We require the header to completely cover the current fpath
117  * configuration. This ensures that any changes to fpath ordering,
118  * additions, or removals will trigger a rebuild as expected by test
119  * scenarios.
120  *
121  * Additionally, we validate directory mtimes to detect content changes
122  * within existing directories (new functions added, etc.).
123  */
124  int idx = 0;
125  header_ok = 1;
126 
127  while (fgets(line, sizeof(line), rf)) {
128  if (strncmp(line, "# dir ", 6) != 0) {
129  break; /* End of header section */
130  }
131 
132  /* Parse header line: # dir <index> <path> <mtime> */
133  char tag[8];
134  int i_hdr = -1;
135  char pathbuf[2048];
136  long mt_recorded = 0;
137 
138  if (sscanf(line, "# %7s %d %2047s %ld", tag, &i_hdr, pathbuf,
139  &mt_recorded) != 4) {
140  header_ok = 0;
141  break;
142  }
143 
144  /* Validate header entry format and sequencing */
145  if (strcmp(tag, "dir") != 0 || i_hdr != idx) {
146  header_ok = 0;
147  break;
148  }
149 
150  /* Ensure current fpath matches recorded path at same index */
151  if (!fpath[idx] || strcmp(fpath[idx], pathbuf) != 0) {
152  header_ok = 0;
153  break;
154  }
155 
156  /*
157  * CONSISTENT MTIME HANDLING
158  *
159  * This is the critical fix for skip detection reliability.
160  * We ensure that missing directories are handled identically
161  * during both generation and verification phases.
162  */
163  struct stat st_now;
164  long mt_current =
165  -1; /* Default for missing/inaccessible directories */
166 
167  if (stat(fpath[idx], &st_now) == 0) {
168  mt_current = (long)st_now.st_mtime;
169  }
170  /* If stat() fails, mt_current remains -1, matching generation
171  * behavior */
172 
173  if (mt_current != mt_recorded) {
174  header_ok = 0;
175  break; /* Directory content changed or accessibility changed */
176  }
177  idx++;
178  }
179 
180  /*
181  * SKIP CONDITION VALIDATION
182  *
183  * We can safely skip rebuilding only if:
184  * 1. All header entries were valid (header_ok = 1)
185  * 2. We consumed exactly all current fpath entries (fpath[idx] == NULL)
186  *
187  * This ensures complete coverage and detects fpath changes like:
188  * - New directories added to fpath
189  * - Directories removed from fpath
190  * - Directory reordering in fpath
191  */
192  if (header_ok && fpath[idx] == NULL) {
193  fclose(rf);
194 
195  if (preload) {
196  /*
197  * PRELOAD MODE: Populate function table without file I/O
198  *
199  * When skip is detected but preload is requested, we scan
200  * the directories to populate shfunctab with function stubs.
201  * This provides the performance benefit of preloading without
202  * the overhead of regenerating the index file.
203  */
204  for (int i = 0; i < idx; i++) {
205  DIR *d = opendir(fpath[i]);
206  if (!d) {
207  continue;
208  }
209 
210  struct dirent *de;
211  while ((de = readdir(d))) {
212  if (de->d_name[0] == '.' || de->d_name[0] == '_') {
213  continue; /* Skip hidden and private functions */
214  }
215 
216  /* Only add if function not already loaded */
217  if (shfunctab && !gethashnode2(shfunctab, de->d_name)) {
218  Shfunc sf = (Shfunc)zalloc(sizeof(*sf));
219  if (!sf) {
220  continue;
221  }
222 
223  memset(sf, 0, sizeof(*sf));
224  sf->node.nam = dupstring(de->d_name);
225  sf->filename = ztrdup(fpath[i]);
226  addhashnode(shfunctab, sf->node.nam, &sf->node);
227  }
228  }
229  closedir(d);
230  }
231  }
232  return 0; /* Successfully skipped rebuild */
233  }
234  }
235  fclose(rf);
236  }
237  }
238 
239  /* Build / preload */
240  FILE *out_fp = stdout;
241  if (outfile) {
242  out_fp = fopen(outfile, "w");
243  if (!out_fp) {
244  zwarnnam(nam, "cannot open for writing: %s", outfile);
245  return 1;
246  }
247  fprintf(out_fp, "# zpmod fpath-index v1\n");
248  }
249 
250  /* Emit header with dir mtimes so we can skip next time */
251  if (outfile) {
252  struct stat st;
253  for (int i = 0; fpath[i]; ++i) {
254  long mt = -1;
255  if (stat(fpath[i], &st) == 0) {
256  mt = (long)st.st_mtime;
257  }
258  fprintf(out_fp, "# dir %d %s %ld\n", i, fpath[i], mt);
259  }
260  }
261 
262  for (int i = 0; fpath[i]; i++) {
263  DIR *d = opendir(fpath[i]);
264  if (!d) {
265  continue;
266  }
267  struct dirent *de;
268  while ((de = readdir(d))) {
269  if (de->d_name[0] == '.' || de->d_name[0] == '_') {
270  continue;
271  }
272  if (preload) {
273 #ifdef ZSH_OOT_MODULE
274  /* When building out-of-tree we rely on zsh headers imported via
275  * zpmod.mdh above; ensure symbols exist before using. */
276 #endif
277  if (shfunctab && !gethashnode2(shfunctab, de->d_name)) {
278  Shfunc sf = (Shfunc)zalloc(sizeof(*sf));
279  if (!sf) {
280  continue;
281  }
282  memset(sf, 0, sizeof(*sf));
283  sf->node.nam = dupstring(de->d_name);
284  sf->filename = ztrdup(fpath[i]);
285  addhashnode(shfunctab, sf->node.nam, &sf->node);
286  }
287  }
288  if (!preload) {
289  fprintf(out_fp, "%s %s/%s\n", de->d_name, fpath[i], de->d_name);
290  }
291  }
292  closedir(d);
293  }
294 
295  if (outfile && out_fp != stdout) {
296  fclose(out_fp);
297  }
298  return 0;
299 }
300 // NOLINTEND(readability-function-cognitive-complexity)
int cmd_fpath_index(char *nam, char **argv)
Implements zpmod fpath-index with intelligent skip detection.
Definition: fpath.c:57
Module declaration header (mdh) for zpmod.
Prototype stub for zpmod when building out-of-tree.
void * zalloc(size_t size)
char * dupstring(const char *s)
char ** getaparam(const char *name)
void zwarnnam(const char *, const char *,...)
HashTable shfunctab
HashNode gethashnode2(HashTable, const char *)
void addhashnode(HashTable, char *, void *)
char * ztrdup(const char *)
Local, non-invasive shims to suppress benign vendor header warnings.