blob: ccb3ef8853d8c800c893fc5e22cb1894ff40a845 [file] [log] [blame]
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001/* vi:set ts=8 sts=4 sw=4 noet:
2 *
3 * VIM - Vi IMproved by Bram Moolenaar
4 *
5 * Do ":help uganda" in Vim to read copying and usage conditions.
6 * Do ":help credits" in Vim to see a list of people who contributed.
7 * See README.txt for an overview of the Vim source code.
8 */
9
10/*
11 * findfile.c: Search for files in directories listed in 'path'
12 */
13
14#include "vim.h"
15
16/*
17 * File searching functions for 'path', 'tags' and 'cdpath' options.
18 * External visible functions:
19 * vim_findfile_init() creates/initialises the search context
20 * vim_findfile_free_visited() free list of visited files/dirs of search
21 * context
22 * vim_findfile() find a file in the search context
23 * vim_findfile_cleanup() cleanup/free search context created by
24 * vim_findfile_init()
25 *
26 * All static functions and variables start with 'ff_'
27 *
28 * In general it works like this:
29 * First you create yourself a search context by calling vim_findfile_init().
30 * It is possible to give a search context from a previous call to
31 * vim_findfile_init(), so it can be reused. After this you call vim_findfile()
32 * until you are satisfied with the result or it returns NULL. On every call it
33 * returns the next file which matches the conditions given to
34 * vim_findfile_init(). If it doesn't find a next file it returns NULL.
35 *
36 * It is possible to call vim_findfile_init() again to reinitialise your search
37 * with some new parameters. Don't forget to pass your old search context to
38 * it, so it can reuse it and especially reuse the list of already visited
39 * directories. If you want to delete the list of already visited directories
40 * simply call vim_findfile_free_visited().
41 *
42 * When you are done call vim_findfile_cleanup() to free the search context.
43 *
44 * The function vim_findfile_init() has a long comment, which describes the
45 * needed parameters.
46 *
47 *
48 *
49 * ATTENTION:
50 * ==========
Christian Brabandtee17b6f2023-09-09 11:23:50 +020051 * Also we use an allocated search context here, these functions are NOT
52 * thread-safe!
Bram Moolenaar5fd0f502019-02-13 23:13:28 +010053 *
54 * To minimize parameter passing (or because I'm to lazy), only the
55 * external visible functions get a search context as a parameter. This is
56 * then assigned to a static global, which is used throughout the local
57 * functions.
58 */
59
60/*
61 * type for the directory search stack
62 */
63typedef struct ff_stack
64{
65 struct ff_stack *ffs_prev;
66
67 // the fix part (no wildcards) and the part containing the wildcards
68 // of the search path
John Marriottd6e3c902025-02-18 20:45:48 +010069 string_T ffs_fix_path;
70 string_T ffs_wc_path;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +010071
72 // files/dirs found in the above directory, matched by the first wildcard
73 // of wc_part
74 char_u **ffs_filearray;
75 int ffs_filearray_size;
John Drouhard95fca122022-08-01 11:38:17 +010076 int ffs_filearray_cur; // needed for partly handled dirs
Bram Moolenaar5fd0f502019-02-13 23:13:28 +010077
78 // to store status of partly handled directories
79 // 0: we work on this directory for the first time
80 // 1: this directory was partly searched in an earlier step
81 int ffs_stage;
82
83 // How deep are we in the directory tree?
84 // Counts backward from value of level parameter to vim_findfile_init
85 int ffs_level;
86
87 // Did we already expand '**' to an empty string?
88 int ffs_star_star_empty;
89} ff_stack_T;
90
91/*
92 * type for already visited directories or files.
93 */
94typedef struct ff_visited
95{
96 struct ff_visited *ffv_next;
97
Bram Moolenaar5fd0f502019-02-13 23:13:28 +010098 // Visited directories are different if the wildcard string are
99 // different. So we have to save it.
100 char_u *ffv_wc_path;
Bram Moolenaar2bd9dbc2022-08-25 18:12:06 +0100101
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100102 // for unix use inode etc for comparison (needed because of links), else
103 // use filename.
104#ifdef UNIX
105 int ffv_dev_valid; // ffv_dev and ffv_ino were set
106 dev_t ffv_dev; // device number
107 ino_t ffv_ino; // inode number
108#endif
109 // The memory for this struct is allocated according to the length of
110 // ffv_fname.
111 char_u ffv_fname[1]; // actually longer
112} ff_visited_T;
113
114/*
115 * We might have to manage several visited lists during a search.
116 * This is especially needed for the tags option. If tags is set to:
117 * "./++/tags,./++/TAGS,++/tags" (replace + with *)
118 * So we have to do 3 searches:
119 * 1) search from the current files directory downward for the file "tags"
120 * 2) search from the current files directory downward for the file "TAGS"
121 * 3) search from Vims current directory downwards for the file "tags"
122 * As you can see, the first and the third search are for the same file, so for
123 * the third search we can use the visited list of the first search. For the
124 * second search we must start from a empty visited list.
125 * The struct ff_visited_list_hdr is used to manage a linked list of already
126 * visited lists.
127 */
128typedef struct ff_visited_list_hdr
129{
130 struct ff_visited_list_hdr *ffvl_next;
131
132 // the filename the attached visited list is for
133 char_u *ffvl_filename;
134
135 ff_visited_T *ffvl_visited_list;
136
137} ff_visited_list_hdr_T;
138
139
140/*
141 * '**' can be expanded to several directory levels.
142 * Set the default maximum depth.
143 */
144#define FF_MAX_STAR_STAR_EXPAND ((char_u)30)
145
146/*
147 * The search context:
148 * ffsc_stack_ptr: the stack for the dirs to search
149 * ffsc_visited_list: the currently active visited list
150 * ffsc_dir_visited_list: the currently active visited list for search dirs
151 * ffsc_visited_lists_list: the list of all visited lists
152 * ffsc_dir_visited_lists_list: the list of all visited lists for search dirs
153 * ffsc_file_to_search: the file to search for
154 * ffsc_start_dir: the starting directory, if search path was relative
155 * ffsc_fix_path: the fix part of the given path (without wildcards)
156 * Needed for upward search.
157 * ffsc_wc_path: the part of the given path containing wildcards
158 * ffsc_level: how many levels of dirs to search downwards
159 * ffsc_stopdirs_v: array of stop directories for upward search
160 * ffsc_find_what: FINDFILE_BOTH, FINDFILE_DIR or FINDFILE_FILE
161 * ffsc_tagfile: searching for tags file, don't use 'suffixesadd'
162 */
163typedef struct ff_search_ctx_T
164{
165 ff_stack_T *ffsc_stack_ptr;
166 ff_visited_list_hdr_T *ffsc_visited_list;
167 ff_visited_list_hdr_T *ffsc_dir_visited_list;
168 ff_visited_list_hdr_T *ffsc_visited_lists_list;
169 ff_visited_list_hdr_T *ffsc_dir_visited_lists_list;
John Marriottd6e3c902025-02-18 20:45:48 +0100170 string_T ffsc_file_to_search;
171 string_T ffsc_start_dir;
172 string_T ffsc_fix_path;
173 string_T ffsc_wc_path;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100174 int ffsc_level;
John Marriottd6e3c902025-02-18 20:45:48 +0100175 string_T *ffsc_stopdirs_v;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100176 int ffsc_find_what;
177 int ffsc_tagfile;
178} ff_search_ctx_T;
179
180// locally needed functions
John Marriottd6e3c902025-02-18 20:45:48 +0100181static int ff_check_visited(ff_visited_T **, char_u *, size_t, char_u *, size_t);
Bram Moolenaar5843f5f2019-08-20 20:13:45 +0200182static void vim_findfile_free_visited(void *search_ctx_arg);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100183static void vim_findfile_free_visited_list(ff_visited_list_hdr_T **list_headp);
184static void ff_free_visited_list(ff_visited_T *vl);
John Marriottd6e3c902025-02-18 20:45:48 +0100185static ff_visited_list_hdr_T* ff_get_visited_list(char_u *, size_t, ff_visited_list_hdr_T **list_headp);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100186
187static void ff_push(ff_search_ctx_T *search_ctx, ff_stack_T *stack_ptr);
188static ff_stack_T *ff_pop(ff_search_ctx_T *search_ctx);
189static void ff_clear(ff_search_ctx_T *search_ctx);
190static void ff_free_stack_element(ff_stack_T *stack_ptr);
John Marriottd6e3c902025-02-18 20:45:48 +0100191static ff_stack_T *ff_create_stack_element(char_u *, size_t, char_u *, size_t, int, int);
192static int ff_path_in_stoplist(char_u *, int, string_T *);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100193
John Marriottd6e3c902025-02-18 20:45:48 +0100194static string_T ff_expand_buffer = {NULL, 0}; // used for expanding filenames
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100195
196#if 0
197/*
198 * if someone likes findfirst/findnext, here are the functions
199 * NOT TESTED!!
200 */
201
202static void *ff_fn_search_context = NULL;
203
204 char_u *
205vim_findfirst(char_u *path, char_u *filename, int level)
206{
207 ff_fn_search_context =
208 vim_findfile_init(path, filename, NULL, level, TRUE, FALSE,
209 ff_fn_search_context, rel_fname);
210 if (NULL == ff_fn_search_context)
211 return NULL;
212 else
213 return vim_findnext()
214}
215
216 char_u *
217vim_findnext(void)
218{
219 char_u *ret = vim_findfile(ff_fn_search_context);
220
221 if (NULL == ret)
222 {
223 vim_findfile_cleanup(ff_fn_search_context);
224 ff_fn_search_context = NULL;
225 }
226 return ret;
227}
228#endif
229
230/*
231 * Initialization routine for vim_findfile().
232 *
233 * Returns the newly allocated search context or NULL if an error occurred.
234 *
235 * Don't forget to clean up by calling vim_findfile_cleanup() if you are done
236 * with the search context.
237 *
238 * Find the file 'filename' in the directory 'path'.
239 * The parameter 'path' may contain wildcards. If so only search 'level'
240 * directories deep. The parameter 'level' is the absolute maximum and is
241 * not related to restricts given to the '**' wildcard. If 'level' is 100
242 * and you use '**200' vim_findfile() will stop after 100 levels.
243 *
244 * 'filename' cannot contain wildcards! It is used as-is, no backslashes to
245 * escape special characters.
246 *
247 * If 'stopdirs' is not NULL and nothing is found downward, the search is
248 * restarted on the next higher directory level. This is repeated until the
249 * start-directory of a search is contained in 'stopdirs'. 'stopdirs' has the
250 * format ";*<dirname>*\(;<dirname>\)*;\=$".
251 *
252 * If the 'path' is relative, the starting dir for the search is either VIM's
253 * current dir or if the path starts with "./" the current files dir.
254 * If the 'path' is absolute, the starting dir is that part of the path before
255 * the first wildcard.
256 *
257 * Upward search is only done on the starting dir.
258 *
259 * If 'free_visited' is TRUE the list of already visited files/directories is
260 * cleared. Set this to FALSE if you just want to search from another
261 * directory, but want to be sure that no directory from a previous search is
262 * searched again. This is useful if you search for a file at different places.
263 * The list of visited files/dirs can also be cleared with the function
264 * vim_findfile_free_visited().
265 *
266 * Set the parameter 'find_what' to FINDFILE_DIR if you want to search for
267 * directories only, FINDFILE_FILE for files only, FINDFILE_BOTH for both.
268 *
269 * A search context returned by a previous call to vim_findfile_init() can be
270 * passed in the parameter "search_ctx_arg". This context is reused and
271 * reinitialized with the new parameters. The list of already visited
272 * directories from this context is only deleted if the parameter
273 * "free_visited" is true. Be aware that the passed "search_ctx_arg" is freed
274 * if the reinitialization fails.
275 *
276 * If you don't have a search context from a previous call "search_ctx_arg"
277 * must be NULL.
278 *
279 * This function silently ignores a few errors, vim_findfile() will have
280 * limited functionality then.
281 */
282 void *
283vim_findfile_init(
284 char_u *path,
285 char_u *filename,
John Marriottd6e3c902025-02-18 20:45:48 +0100286 size_t filenamelen,
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100287 char_u *stopdirs UNUSED,
288 int level,
289 int free_visited,
290 int find_what,
291 void *search_ctx_arg,
292 int tagfile, // expanding names of tags files
293 char_u *rel_fname) // file name to use for "."
294{
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100295 char_u *wc_part;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100296 ff_stack_T *sptr;
297 ff_search_ctx_T *search_ctx;
John Marriottd6e3c902025-02-18 20:45:48 +0100298 int add_sep;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100299
300 // If a search context is given by the caller, reuse it, else allocate a
301 // new one.
302 if (search_ctx_arg != NULL)
303 search_ctx = search_ctx_arg;
304 else
305 {
Bram Moolenaara80faa82020-04-12 19:37:17 +0200306 search_ctx = ALLOC_CLEAR_ONE(ff_search_ctx_T);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100307 if (search_ctx == NULL)
308 goto error_return;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100309 }
310 search_ctx->ffsc_find_what = find_what;
311 search_ctx->ffsc_tagfile = tagfile;
312
313 // clear the search context, but NOT the visited lists
314 ff_clear(search_ctx);
315
316 // clear visited list if wanted
317 if (free_visited == TRUE)
318 vim_findfile_free_visited(search_ctx);
319 else
320 {
321 // Reuse old visited lists. Get the visited list for the given
322 // filename. If no list for the current filename exists, creates a new
323 // one.
324 search_ctx->ffsc_visited_list = ff_get_visited_list(filename,
John Marriottd6e3c902025-02-18 20:45:48 +0100325 filenamelen, &search_ctx->ffsc_visited_lists_list);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100326 if (search_ctx->ffsc_visited_list == NULL)
327 goto error_return;
328 search_ctx->ffsc_dir_visited_list = ff_get_visited_list(filename,
John Marriottd6e3c902025-02-18 20:45:48 +0100329 filenamelen, &search_ctx->ffsc_dir_visited_lists_list);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100330 if (search_ctx->ffsc_dir_visited_list == NULL)
331 goto error_return;
332 }
333
John Marriottd6e3c902025-02-18 20:45:48 +0100334 if (ff_expand_buffer.string == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100335 {
John Marriottd6e3c902025-02-18 20:45:48 +0100336 ff_expand_buffer.length = 0;
337 ff_expand_buffer.string = alloc(MAXPATHL);
338 if (ff_expand_buffer.string == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100339 goto error_return;
340 }
341
342 // Store information on starting dir now if path is relative.
343 // If path is absolute, we do that later.
344 if (path[0] == '.'
345 && (vim_ispathsep(path[1]) || path[1] == NUL)
346 && (!tagfile || vim_strchr(p_cpo, CPO_DOTTAG) == NULL)
347 && rel_fname != NULL)
348 {
349 int len = (int)(gettail(rel_fname) - rel_fname);
350
351 if (!vim_isAbsName(rel_fname) && len + 1 < MAXPATHL)
352 {
353 // Make the start dir an absolute path name.
John Marriottd6e3c902025-02-18 20:45:48 +0100354 vim_strncpy(ff_expand_buffer.string, rel_fname, len);
355 ff_expand_buffer.length = len;
356
357 search_ctx->ffsc_start_dir.string = FullName_save(ff_expand_buffer.string, FALSE);
358 if (search_ctx->ffsc_start_dir.string == NULL)
359 goto error_return;
360 search_ctx->ffsc_start_dir.length = STRLEN(search_ctx->ffsc_start_dir.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100361 }
362 else
John Marriottd6e3c902025-02-18 20:45:48 +0100363 {
364 search_ctx->ffsc_start_dir.length = len;
365 search_ctx->ffsc_start_dir.string = vim_strnsave(rel_fname,
366 search_ctx->ffsc_start_dir.length);
367 if (search_ctx->ffsc_start_dir.string == NULL)
368 goto error_return;
369 }
370
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100371 if (*++path != NUL)
372 ++path;
373 }
374 else if (*path == NUL || !vim_isAbsName(path))
375 {
376#ifdef BACKSLASH_IN_FILENAME
377 // "c:dir" needs "c:" to be expanded, otherwise use current dir
378 if (*path != NUL && path[1] == ':')
379 {
380 char_u drive[3];
381
382 drive[0] = path[0];
383 drive[1] = ':';
384 drive[2] = NUL;
John Marriottd6e3c902025-02-18 20:45:48 +0100385 if (vim_FullName(drive, ff_expand_buffer.string, MAXPATHL, TRUE) == FAIL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100386 goto error_return;
387 path += 2;
388 }
389 else
390#endif
John Marriottd6e3c902025-02-18 20:45:48 +0100391 if (mch_dirname(ff_expand_buffer.string, MAXPATHL) == FAIL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100392 goto error_return;
393
John Marriottd6e3c902025-02-18 20:45:48 +0100394 ff_expand_buffer.length = STRLEN(ff_expand_buffer.string);
395
396 search_ctx->ffsc_start_dir.length = ff_expand_buffer.length;
397 search_ctx->ffsc_start_dir.string = vim_strnsave(ff_expand_buffer.string,
398 search_ctx->ffsc_start_dir.length);
399 if (search_ctx->ffsc_start_dir.string == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100400 goto error_return;
401
402#ifdef BACKSLASH_IN_FILENAME
403 // A path that starts with "/dir" is relative to the drive, not to the
404 // directory (but not for "//machine/dir"). Only use the drive name.
405 if ((*path == '/' || *path == '\\')
406 && path[1] != path[0]
John Marriottd6e3c902025-02-18 20:45:48 +0100407 && search_ctx->ffsc_start_dir.string[1] == ':')
408 {
409 search_ctx->ffsc_start_dir.string[2] = NUL;
410 search_ctx->ffsc_start_dir.length = 2;
411 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100412#endif
413 }
414
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100415 /*
416 * If stopdirs are given, split them into an array of pointers.
417 * If this fails (mem allocation), there is no upward search at all or a
418 * stop directory is not recognized -> continue silently.
419 * If stopdirs just contains a ";" or is empty,
420 * search_ctx->ffsc_stopdirs_v will only contain a NULL pointer. This
421 * is handled as unlimited upward search. See function
422 * ff_path_in_stoplist() for details.
423 */
424 if (stopdirs != NULL)
425 {
426 char_u *walker = stopdirs;
427 int dircount;
428
429 while (*walker == ';')
430 walker++;
431
432 dircount = 1;
John Marriottd6e3c902025-02-18 20:45:48 +0100433 search_ctx->ffsc_stopdirs_v = ALLOC_ONE(string_T);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100434
435 if (search_ctx->ffsc_stopdirs_v != NULL)
436 {
John Marriottd6e3c902025-02-18 20:45:48 +0100437 string_T *tmp; // for convenience
438
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100439 do
440 {
441 char_u *helper;
442 void *ptr;
zeertzjq764526e2024-07-11 22:24:15 +0200443 size_t len;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100444
445 helper = walker;
446 ptr = vim_realloc(search_ctx->ffsc_stopdirs_v,
John Marriottd6e3c902025-02-18 20:45:48 +0100447 (dircount + 1) * sizeof(string_T));
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100448 if (ptr)
449 search_ctx->ffsc_stopdirs_v = ptr;
450 else
451 // ignore, keep what we have and continue
452 break;
453 walker = vim_strchr(walker, ';');
zeertzjq764526e2024-07-11 22:24:15 +0200454 len = walker ? (size_t)(walker - helper) : STRLEN(helper);
455 // "" means ascent till top of directory tree.
John Marriottd6e3c902025-02-18 20:45:48 +0100456
zeertzjq764526e2024-07-11 22:24:15 +0200457 if (*helper != NUL && !vim_isAbsName(helper)
458 && len + 1 < MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100459 {
zeertzjq764526e2024-07-11 22:24:15 +0200460 // Make the stop dir an absolute path name.
John Marriottd6e3c902025-02-18 20:45:48 +0100461 vim_strncpy(ff_expand_buffer.string, helper, len);
462 ff_expand_buffer.length = len;
463
464 tmp = &search_ctx->ffsc_stopdirs_v[dircount - 1];
465 tmp->string = FullName_save(ff_expand_buffer.string, FALSE);
466 if (tmp->string != NULL)
467 tmp->length = STRLEN(tmp->string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100468 }
469 else
John Marriottd6e3c902025-02-18 20:45:48 +0100470 {
471 tmp = &search_ctx->ffsc_stopdirs_v[dircount - 1];
472 tmp->length = len;
473 tmp->string = vim_strnsave(helper, tmp->length);
474 if (tmp->string == NULL)
475 tmp->length = 0;
476 }
zeertzjq764526e2024-07-11 22:24:15 +0200477 if (walker)
478 walker++;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100479 dircount++;
480
481 } while (walker != NULL);
John Marriottd6e3c902025-02-18 20:45:48 +0100482
483 tmp = &search_ctx->ffsc_stopdirs_v[dircount - 1];
484 tmp->string = NULL;
485 tmp->length = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100486 }
487 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100488
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100489 search_ctx->ffsc_level = level;
490
491 /*
492 * split into:
493 * -fix path
494 * -wildcard_stuff (might be NULL)
495 */
496 wc_part = vim_strchr(path, '*');
497 if (wc_part != NULL)
498 {
499 int llevel;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100500 char *errpt;
501
502 // save the fix part of the path
John Marriottd6e3c902025-02-18 20:45:48 +0100503 search_ctx->ffsc_fix_path.length = (size_t)(wc_part - path);
504 search_ctx->ffsc_fix_path.string = vim_strnsave(path,
505 search_ctx->ffsc_fix_path.length);
506 if (search_ctx->ffsc_fix_path.string == NULL)
507 goto error_return;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100508
509 /*
510 * copy wc_path and add restricts to the '**' wildcard.
511 * The octet after a '**' is used as a (binary) counter.
512 * So '**3' is transposed to '**^C' ('^C' is ASCII value 3)
513 * or '**76' is transposed to '**N'( 'N' is ASCII value 76).
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100514 * If no restrict is given after '**' the default is used.
515 * Due to this technique the path looks awful if you print it as a
516 * string.
517 */
John Marriottd6e3c902025-02-18 20:45:48 +0100518 ff_expand_buffer.length = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100519 while (*wc_part != NUL)
520 {
John Marriottd6e3c902025-02-18 20:45:48 +0100521 if (ff_expand_buffer.length + 5 >= MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100522 {
Bram Moolenaar9d00e4a2022-01-05 17:49:15 +0000523 emsg(_(e_path_too_long_for_completion));
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100524 break;
525 }
526 if (STRNCMP(wc_part, "**", 2) == 0)
527 {
John Marriottd6e3c902025-02-18 20:45:48 +0100528 ff_expand_buffer.string[ff_expand_buffer.length++] = *wc_part++;
529 ff_expand_buffer.string[ff_expand_buffer.length++] = *wc_part++;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100530
531 llevel = strtol((char *)wc_part, &errpt, 10);
532 if ((char_u *)errpt != wc_part && llevel > 0 && llevel < 255)
John Marriottd6e3c902025-02-18 20:45:48 +0100533 ff_expand_buffer.string[ff_expand_buffer.length++] = llevel;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100534 else if ((char_u *)errpt != wc_part && llevel == 0)
535 // restrict is 0 -> remove already added '**'
John Marriottd6e3c902025-02-18 20:45:48 +0100536 ff_expand_buffer.length -= 2;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100537 else
John Marriottd6e3c902025-02-18 20:45:48 +0100538 ff_expand_buffer.string[ff_expand_buffer.length++] = FF_MAX_STAR_STAR_EXPAND;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100539 wc_part = (char_u *)errpt;
540 if (*wc_part != NUL && !vim_ispathsep(*wc_part))
541 {
Bram Moolenaareaaac012022-01-02 17:00:40 +0000542 semsg(_(e_invalid_path_number_must_be_at_end_of_path_or_be_followed_by_str), PATHSEPSTR);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100543 goto error_return;
544 }
545 }
546 else
John Marriottd6e3c902025-02-18 20:45:48 +0100547 ff_expand_buffer.string[ff_expand_buffer.length++] = *wc_part++;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100548 }
John Marriottd6e3c902025-02-18 20:45:48 +0100549 ff_expand_buffer.string[ff_expand_buffer.length] = NUL;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100550
John Marriottd6e3c902025-02-18 20:45:48 +0100551 search_ctx->ffsc_wc_path.length = ff_expand_buffer.length;
552 search_ctx->ffsc_wc_path.string = vim_strnsave(ff_expand_buffer.string,
553 search_ctx->ffsc_wc_path.length);
554 if (search_ctx->ffsc_wc_path.string == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100555 goto error_return;
556 }
557 else
John Marriottd6e3c902025-02-18 20:45:48 +0100558 {
559 search_ctx->ffsc_fix_path.length = STRLEN(path);
560 search_ctx->ffsc_fix_path.string = vim_strnsave(path,
561 search_ctx->ffsc_fix_path.length);
562 if (search_ctx->ffsc_fix_path.string == NULL)
563 goto error_return;
564 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100565
John Marriottd6e3c902025-02-18 20:45:48 +0100566 if (search_ctx->ffsc_start_dir.string == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100567 {
568 // store the fix part as startdir.
569 // This is needed if the parameter path is fully qualified.
John Marriottd6e3c902025-02-18 20:45:48 +0100570 search_ctx->ffsc_start_dir.length = search_ctx->ffsc_fix_path.length;
571 search_ctx->ffsc_start_dir.string = vim_strnsave(search_ctx->ffsc_fix_path.string,
572 search_ctx->ffsc_start_dir.length);
573 if (search_ctx->ffsc_start_dir.string == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100574 goto error_return;
John Marriottd6e3c902025-02-18 20:45:48 +0100575 search_ctx->ffsc_fix_path.string[0] = NUL;
576 search_ctx->ffsc_fix_path.length = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100577 }
578
579 // create an absolute path
John Marriottd6e3c902025-02-18 20:45:48 +0100580 if (search_ctx->ffsc_start_dir.length
581 + search_ctx->ffsc_fix_path.length + 3 >= MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100582 {
Bram Moolenaar9d00e4a2022-01-05 17:49:15 +0000583 emsg(_(e_path_too_long_for_completion));
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100584 goto error_return;
585 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100586
John Marriottd6e3c902025-02-18 20:45:48 +0100587 add_sep = !after_pathsep(search_ctx->ffsc_start_dir.string,
588 search_ctx->ffsc_start_dir.string + search_ctx->ffsc_start_dir.length);
589 ff_expand_buffer.length = vim_snprintf(
590 (char *)ff_expand_buffer.string,
591 MAXPATHL,
592 "%s%s",
593 search_ctx->ffsc_start_dir.string,
594 add_sep ? PATHSEPSTR : "");
595
596 {
597 size_t bufsize = ff_expand_buffer.length + search_ctx->ffsc_fix_path.length + 1;
598 char_u *buf = alloc(bufsize);
599
600 if (buf == NULL)
601 goto error_return;
602
603 vim_snprintf(
604 (char *)buf,
605 bufsize,
606 "%s%s",
607 ff_expand_buffer.string,
608 search_ctx->ffsc_fix_path.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100609 if (mch_isdir(buf))
610 {
John Marriottd6e3c902025-02-18 20:45:48 +0100611 if (search_ctx->ffsc_fix_path.length > 0)
612 {
613 add_sep = !after_pathsep(search_ctx->ffsc_fix_path.string,
614 search_ctx->ffsc_fix_path.string + search_ctx->ffsc_fix_path.length);
615 ff_expand_buffer.length += vim_snprintf(
616 (char *)ff_expand_buffer.string + ff_expand_buffer.length,
617 MAXPATHL - ff_expand_buffer.length,
618 "%s%s",
619 search_ctx->ffsc_fix_path.string,
620 add_sep ? PATHSEPSTR : "");
621 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100622 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100623 else
624 {
John Marriottd6e3c902025-02-18 20:45:48 +0100625 char_u *p = gettail(search_ctx->ffsc_fix_path.string);
626 int len = (int)search_ctx->ffsc_fix_path.length;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100627
John Marriottd6e3c902025-02-18 20:45:48 +0100628 if (p > search_ctx->ffsc_fix_path.string)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100629 {
Christian Brabandt7a4ca322021-07-25 15:08:05 +0200630 // do not add '..' to the path and start upwards searching
John Marriottd6e3c902025-02-18 20:45:48 +0100631 len = (int)(p - search_ctx->ffsc_fix_path.string) - 1;
Christian Brabandt7a4ca322021-07-25 15:08:05 +0200632 if ((len >= 2
John Marriottd6e3c902025-02-18 20:45:48 +0100633 && STRNCMP(search_ctx->ffsc_fix_path.string, "..", 2) == 0)
634 && (len == 2 || search_ctx->ffsc_fix_path.string[2] == PATHSEP))
Christian Brabandt7a4ca322021-07-25 15:08:05 +0200635 {
636 vim_free(buf);
637 goto error_return;
638 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100639
John Marriottd6e3c902025-02-18 20:45:48 +0100640 add_sep = !after_pathsep(search_ctx->ffsc_fix_path.string,
641 search_ctx->ffsc_fix_path.string + search_ctx->ffsc_fix_path.length);
642 ff_expand_buffer.length += vim_snprintf(
643 (char *)ff_expand_buffer.string + ff_expand_buffer.length,
644 MAXPATHL - ff_expand_buffer.length,
645 "%.*s%s",
646 len,
647 search_ctx->ffsc_fix_path.string,
648 add_sep ? PATHSEPSTR : "");
649 }
650
651 if (search_ctx->ffsc_wc_path.string != NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100652 {
John Marriottd6e3c902025-02-18 20:45:48 +0100653 size_t tempsize = (search_ctx->ffsc_fix_path.length - len)
654 + search_ctx->ffsc_wc_path.length
655 + 1;
656 char_u *temp = alloc(tempsize);
657
658 if (temp == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100659 {
660 vim_free(buf);
661 vim_free(temp);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100662 goto error_return;
663 }
664
John Marriottd6e3c902025-02-18 20:45:48 +0100665 search_ctx->ffsc_wc_path.length = vim_snprintf(
666 (char *)temp,
667 tempsize,
668 "%s%s",
669 search_ctx->ffsc_fix_path.string + len,
670 search_ctx->ffsc_wc_path.string);
671 vim_free(search_ctx->ffsc_wc_path.string);
672 search_ctx->ffsc_wc_path.string = temp;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100673 }
674 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100675 vim_free(buf);
676 }
677
John Marriottd6e3c902025-02-18 20:45:48 +0100678 sptr = ff_create_stack_element(ff_expand_buffer.string,
679 ff_expand_buffer.length,
680 search_ctx->ffsc_wc_path.string,
681 search_ctx->ffsc_wc_path.length,
682 level,
683 0);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100684
685 if (sptr == NULL)
686 goto error_return;
687
688 ff_push(search_ctx, sptr);
689
John Marriottd6e3c902025-02-18 20:45:48 +0100690 search_ctx->ffsc_file_to_search.length = filenamelen;
691 search_ctx->ffsc_file_to_search.string = vim_strnsave(filename,
692 search_ctx->ffsc_file_to_search.length);
693 if (search_ctx->ffsc_file_to_search.string == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100694 goto error_return;
695
696 return search_ctx;
697
698error_return:
699 /*
700 * We clear the search context now!
701 * Even when the caller gave us a (perhaps valid) context we free it here,
702 * as we might have already destroyed it.
703 */
704 vim_findfile_cleanup(search_ctx);
705 return NULL;
706}
707
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100708/*
709 * Get the stopdir string. Check that ';' is not escaped.
710 */
711 char_u *
712vim_findfile_stopdir(char_u *buf)
713{
714 char_u *r_ptr = buf;
John Marriottd6e3c902025-02-18 20:45:48 +0100715 char_u *r_ptr_end = NULL; // points to NUL at end of string "r_ptr"
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100716
717 while (*r_ptr != NUL && *r_ptr != ';')
718 {
719 if (r_ptr[0] == '\\' && r_ptr[1] == ';')
720 {
721 // Overwrite the escape char,
722 // use STRLEN(r_ptr) to move the trailing '\0'.
John Marriottd6e3c902025-02-18 20:45:48 +0100723 if (r_ptr_end == NULL)
724 r_ptr_end = r_ptr + STRLEN(r_ptr);
725 mch_memmove(r_ptr, r_ptr + 1,
726 (size_t)(r_ptr_end - (r_ptr + 1)) + 1); // +1 for NUL
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100727 r_ptr++;
John Marriottd6e3c902025-02-18 20:45:48 +0100728 --r_ptr_end;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100729 }
730 r_ptr++;
731 }
732 if (*r_ptr == ';')
733 {
John Marriottd6e3c902025-02-18 20:45:48 +0100734 *r_ptr = NUL;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100735 r_ptr++;
736 }
737 else if (*r_ptr == NUL)
738 r_ptr = NULL;
739 return r_ptr;
740}
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100741
742/*
743 * Clean up the given search context. Can handle a NULL pointer.
744 */
745 void
746vim_findfile_cleanup(void *ctx)
747{
748 if (ctx == NULL)
749 return;
750
751 vim_findfile_free_visited(ctx);
752 ff_clear(ctx);
753 vim_free(ctx);
754}
755
756/*
757 * Find a file in a search context.
758 * The search context was created with vim_findfile_init() above.
759 * Return a pointer to an allocated file name or NULL if nothing found.
760 * To get all matching files call this function until you get NULL.
761 *
762 * If the passed search_context is NULL, NULL is returned.
763 *
764 * The search algorithm is depth first. To change this replace the
765 * stack with a list (don't forget to leave partly searched directories on the
766 * top of the list).
767 */
768 char_u *
769vim_findfile(void *search_ctx_arg)
770{
John Marriottd6e3c902025-02-18 20:45:48 +0100771 string_T file_path;
772 string_T rest_of_wildcards;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100773 char_u *path_end = NULL;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100774 ff_stack_T *stackp;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100775 ff_search_ctx_T *search_ctx;
776
777 if (search_ctx_arg == NULL)
778 return NULL;
779
780 search_ctx = (ff_search_ctx_T *)search_ctx_arg;
781
782 /*
783 * filepath is used as buffer for various actions and as the storage to
784 * return a found filename.
785 */
John Marriottd6e3c902025-02-18 20:45:48 +0100786 if ((file_path.string = alloc(MAXPATHL)) == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100787 return NULL;
788
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100789 // store the end of the start dir -- needed for upward search
John Marriottd6e3c902025-02-18 20:45:48 +0100790 if (search_ctx->ffsc_start_dir.string != NULL)
791 path_end = &search_ctx->ffsc_start_dir.string[
792 search_ctx->ffsc_start_dir.length];
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100793
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100794 // upward search loop
795 for (;;)
796 {
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100797 // downward search loop
798 for (;;)
799 {
Dominique Pelleaf4a61a2021-12-27 17:21:41 +0000800 // check if user wants to stop the search
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100801 ui_breakcheck();
802 if (got_int)
803 break;
804
805 // get directory to work on from stack
806 stackp = ff_pop(search_ctx);
807 if (stackp == NULL)
808 break;
809
810 /*
811 * TODO: decide if we leave this test in
812 *
813 * GOOD: don't search a directory(-tree) twice.
814 * BAD: - check linked list for every new directory entered.
815 * - check for double files also done below
816 *
817 * Here we check if we already searched this directory.
818 * We already searched a directory if:
819 * 1) The directory is the same.
820 * 2) We would use the same wildcard string.
821 *
822 * Good if you have links on same directory via several ways
823 * or you have selfreferences in directories (e.g. SuSE Linux 6.3:
824 * /etc/rc.d/init.d is linked to /etc/rc.d -> endless loop)
825 *
826 * This check is only needed for directories we work on for the
827 * first time (hence stackp->ff_filearray == NULL)
828 */
829 if (stackp->ffs_filearray == NULL
830 && ff_check_visited(&search_ctx->ffsc_dir_visited_list
831 ->ffvl_visited_list,
John Marriottd6e3c902025-02-18 20:45:48 +0100832 stackp->ffs_fix_path.string,
833 stackp->ffs_fix_path.length,
834 stackp->ffs_wc_path.string,
835 stackp->ffs_wc_path.length) == FAIL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100836 {
837#ifdef FF_VERBOSE
838 if (p_verbose >= 5)
839 {
840 verbose_enter_scroll();
841 smsg("Already Searched: %s (%s)",
John Marriottd6e3c902025-02-18 20:45:48 +0100842 stackp->ffs_fix_path.string, stackp->ffs_wc_path.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100843 // don't overwrite this either
844 msg_puts("\n");
845 verbose_leave_scroll();
846 }
847#endif
848 ff_free_stack_element(stackp);
849 continue;
850 }
851#ifdef FF_VERBOSE
852 else if (p_verbose >= 5)
853 {
854 verbose_enter_scroll();
855 smsg("Searching: %s (%s)",
John Marriottd6e3c902025-02-18 20:45:48 +0100856 stackp->ffs_fix_path.string, stackp->ffs_wc_path.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100857 // don't overwrite this either
858 msg_puts("\n");
859 verbose_leave_scroll();
860 }
861#endif
862
863 // check depth
864 if (stackp->ffs_level <= 0)
865 {
866 ff_free_stack_element(stackp);
867 continue;
868 }
869
John Marriottd6e3c902025-02-18 20:45:48 +0100870 file_path.string[0] = NUL;
871 file_path.length = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100872
873 /*
874 * If no filearray till now expand wildcards
875 * The function expand_wildcards() can handle an array of paths
876 * and all possible expands are returned in one array. We use this
877 * to handle the expansion of '**' into an empty string.
878 */
879 if (stackp->ffs_filearray == NULL)
880 {
881 char_u *dirptrs[2];
882
883 // we use filepath to build the path expand_wildcards() should
884 // expand.
John Marriottd6e3c902025-02-18 20:45:48 +0100885 dirptrs[0] = file_path.string;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100886 dirptrs[1] = NULL;
887
888 // if we have a start dir copy it in
John Marriottd6e3c902025-02-18 20:45:48 +0100889 if (!vim_isAbsName(stackp->ffs_fix_path.string)
890 && search_ctx->ffsc_start_dir.string)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100891 {
John Marriottd6e3c902025-02-18 20:45:48 +0100892 if (search_ctx->ffsc_start_dir.length + 1 < MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100893 {
John Marriottd6e3c902025-02-18 20:45:48 +0100894 int add_sep = !after_pathsep(search_ctx->ffsc_start_dir.string,
895 search_ctx->ffsc_start_dir.string + search_ctx->ffsc_start_dir.length);
896 file_path.length = vim_snprintf(
897 (char *)file_path.string,
898 MAXPATHL,
899 "%s%s",
900 search_ctx->ffsc_start_dir.string,
901 add_sep ? PATHSEPSTR : "");
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100902 }
903 else
904 {
905 ff_free_stack_element(stackp);
906 goto fail;
907 }
908 }
909
910 // append the fix part of the search path
John Marriottd6e3c902025-02-18 20:45:48 +0100911 if (file_path.length + stackp->ffs_fix_path.length + 1 < MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100912 {
John Marriottd6e3c902025-02-18 20:45:48 +0100913 int add_sep = !after_pathsep(stackp->ffs_fix_path.string,
914 stackp->ffs_fix_path.string + stackp->ffs_fix_path.length);
915 file_path.length += vim_snprintf(
916 (char *)file_path.string + file_path.length,
917 MAXPATHL - file_path.length,
918 "%s%s",
919 stackp->ffs_fix_path.string,
920 add_sep ? PATHSEPSTR : "");
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100921 }
922 else
923 {
924 ff_free_stack_element(stackp);
925 goto fail;
926 }
927
John Marriottd6e3c902025-02-18 20:45:48 +0100928 rest_of_wildcards.string = stackp->ffs_wc_path.string;
929 rest_of_wildcards.length = stackp->ffs_wc_path.length;
930 if (*rest_of_wildcards.string != NUL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100931 {
John Marriottd6e3c902025-02-18 20:45:48 +0100932 if (STRNCMP(rest_of_wildcards.string, "**", 2) == 0)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100933 {
John Marriottd6e3c902025-02-18 20:45:48 +0100934 char_u *p;
935
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100936 // pointer to the restrict byte
937 // The restrict byte is not a character!
John Marriottd6e3c902025-02-18 20:45:48 +0100938 p = rest_of_wildcards.string + 2;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100939
940 if (*p > 0)
941 {
942 (*p)--;
John Marriottd6e3c902025-02-18 20:45:48 +0100943 if (file_path.length + 1 < MAXPATHL)
944 file_path.string[file_path.length++] = '*';
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100945 else
946 {
947 ff_free_stack_element(stackp);
948 goto fail;
949 }
950 }
951
952 if (*p == 0)
953 {
954 // remove '**<numb> from wildcards
John Marriottd6e3c902025-02-18 20:45:48 +0100955 mch_memmove(rest_of_wildcards.string,
956 rest_of_wildcards.string + 3,
957 (size_t)(rest_of_wildcards.length - 3) + 1); // +1 for NUL
958 rest_of_wildcards.length -= 3;
959 stackp->ffs_wc_path.length = rest_of_wildcards.length;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100960 }
961 else
John Marriottd6e3c902025-02-18 20:45:48 +0100962 {
963 rest_of_wildcards.string += 3;
964 rest_of_wildcards.length -= 3;
965 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100966
967 if (stackp->ffs_star_star_empty == 0)
968 {
969 // if not done before, expand '**' to empty
970 stackp->ffs_star_star_empty = 1;
John Marriottd6e3c902025-02-18 20:45:48 +0100971 dirptrs[1] = stackp->ffs_fix_path.string;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100972 }
973 }
974
975 /*
976 * Here we copy until the next path separator or the end of
977 * the path. If we stop at a path separator, there is
978 * still something else left. This is handled below by
979 * pushing every directory returned from expand_wildcards()
980 * on the stack again for further search.
981 */
John Marriottd6e3c902025-02-18 20:45:48 +0100982 while (*rest_of_wildcards.string
983 && !vim_ispathsep(*rest_of_wildcards.string))
984 {
985 if (file_path.length + 1 < MAXPATHL)
986 {
987 file_path.string[file_path.length++] = *rest_of_wildcards.string++;
988 --rest_of_wildcards.length;
989 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100990 else
991 {
992 ff_free_stack_element(stackp);
993 goto fail;
994 }
John Marriottd6e3c902025-02-18 20:45:48 +0100995 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +0100996
John Marriottd6e3c902025-02-18 20:45:48 +0100997 file_path.string[file_path.length] = NUL;
998 if (vim_ispathsep(*rest_of_wildcards.string))
999 {
1000 rest_of_wildcards.string++;
1001 rest_of_wildcards.length--;
1002 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001003 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001004
1005 /*
1006 * Expand wildcards like "*" and "$VAR".
1007 * If the path is a URL don't try this.
1008 */
1009 if (path_with_url(dirptrs[0]))
1010 {
Bram Moolenaarc799fe22019-05-28 23:08:19 +02001011 stackp->ffs_filearray = ALLOC_ONE(char_u *);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001012 if (stackp->ffs_filearray != NULL
1013 && (stackp->ffs_filearray[0]
John Marriottd6e3c902025-02-18 20:45:48 +01001014 = vim_strnsave(dirptrs[0], file_path.length)) != NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001015 stackp->ffs_filearray_size = 1;
1016 else
1017 stackp->ffs_filearray_size = 0;
1018 }
1019 else
1020 // Add EW_NOTWILD because the expanded path may contain
1021 // wildcard characters that are to be taken literally.
1022 // This is a bit of a hack.
1023 expand_wildcards((dirptrs[1] == NULL) ? 1 : 2, dirptrs,
1024 &stackp->ffs_filearray_size,
1025 &stackp->ffs_filearray,
1026 EW_DIR|EW_ADDSLASH|EW_SILENT|EW_NOTWILD);
1027
1028 stackp->ffs_filearray_cur = 0;
1029 stackp->ffs_stage = 0;
1030 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001031 else
John Marriottd6e3c902025-02-18 20:45:48 +01001032 {
1033 rest_of_wildcards.string = &stackp->ffs_wc_path.string[
1034 stackp->ffs_wc_path.length];
1035 rest_of_wildcards.length = 0;
1036 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001037
1038 if (stackp->ffs_stage == 0)
1039 {
John Marriottd6e3c902025-02-18 20:45:48 +01001040 int i;
1041
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001042 // this is the first time we work on this directory
John Marriottd6e3c902025-02-18 20:45:48 +01001043 if (*rest_of_wildcards.string == NUL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001044 {
John Marriottd6e3c902025-02-18 20:45:48 +01001045 size_t len;
1046 char_u *suf;
1047
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001048 /*
1049 * We don't have further wildcards to expand, so we have to
1050 * check for the final file now.
1051 */
1052 for (i = stackp->ffs_filearray_cur;
1053 i < stackp->ffs_filearray_size; ++i)
1054 {
1055 if (!path_with_url(stackp->ffs_filearray[i])
1056 && !mch_isdir(stackp->ffs_filearray[i]))
Bram Moolenaar217e1b82019-12-01 21:41:28 +01001057 continue; // not a directory
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001058
1059 // prepare the filename to be checked for existence
1060 // below
John Marriottd6e3c902025-02-18 20:45:48 +01001061 len = STRLEN(stackp->ffs_filearray[i]);
1062 if (len + 1 + search_ctx->ffsc_file_to_search.length
1063 < MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001064 {
John Marriottd6e3c902025-02-18 20:45:48 +01001065 int add_sep = !after_pathsep(stackp->ffs_filearray[i],
1066 stackp->ffs_filearray[i] + len);
1067 file_path.length = vim_snprintf(
1068 (char *)file_path.string,
1069 MAXPATHL,
1070 "%s%s%s",
1071 stackp->ffs_filearray[i],
1072 add_sep ? PATHSEPSTR : "",
1073 search_ctx->ffsc_file_to_search.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001074 }
1075 else
1076 {
1077 ff_free_stack_element(stackp);
1078 goto fail;
1079 }
1080
1081 /*
1082 * Try without extra suffix and then with suffixes
1083 * from 'suffixesadd'.
1084 */
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001085 if (search_ctx->ffsc_tagfile)
1086 suf = (char_u *)"";
1087 else
1088 suf = curbuf->b_p_sua;
1089 for (;;)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001090 {
1091 // if file exists and we didn't already find it
John Marriottd6e3c902025-02-18 20:45:48 +01001092 if ((path_with_url(file_path.string)
1093 || (mch_getperm(file_path.string) >= 0
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001094 && (search_ctx->ffsc_find_what
1095 == FINDFILE_BOTH
1096 || ((search_ctx->ffsc_find_what
1097 == FINDFILE_DIR)
John Marriottd6e3c902025-02-18 20:45:48 +01001098 == mch_isdir(file_path.string)))))
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001099#ifndef FF_VERBOSE
1100 && (ff_check_visited(
Bram Moolenaar2bd9dbc2022-08-25 18:12:06 +01001101 &search_ctx->ffsc_visited_list
1102 ->ffvl_visited_list,
John Marriottd6e3c902025-02-18 20:45:48 +01001103 file_path.string,
1104 file_path.length,
1105 (char_u *)"", 0) == OK)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001106#endif
1107 )
1108 {
1109#ifdef FF_VERBOSE
1110 if (ff_check_visited(
Bram Moolenaar2bd9dbc2022-08-25 18:12:06 +01001111 &search_ctx->ffsc_visited_list
1112 ->ffvl_visited_list,
John Marriottd6e3c902025-02-18 20:45:48 +01001113 file_path.string,
1114 file_path.length,
1115 (char_u *)"", 0) == FAIL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001116 {
1117 if (p_verbose >= 5)
1118 {
1119 verbose_enter_scroll();
John Marriottd6e3c902025-02-18 20:45:48 +01001120 smsg("Already: %s", file_path.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001121 // don't overwrite this either
1122 msg_puts("\n");
1123 verbose_leave_scroll();
1124 }
1125 continue;
1126 }
1127#endif
1128
1129 // push dir to examine rest of subdirs later
1130 stackp->ffs_filearray_cur = i + 1;
1131 ff_push(search_ctx, stackp);
1132
John Marriottd6e3c902025-02-18 20:45:48 +01001133 if (!path_with_url(file_path.string))
1134 file_path.length = simplify_filename(file_path.string);
1135
1136 if (mch_dirname(ff_expand_buffer.string, MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001137 == OK)
1138 {
John Marriottd6e3c902025-02-18 20:45:48 +01001139 char_u *p;
1140
1141 ff_expand_buffer.length = STRLEN(ff_expand_buffer.string);
1142 p = shorten_fname(file_path.string,
1143 ff_expand_buffer.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001144 if (p != NULL)
John Marriottd6e3c902025-02-18 20:45:48 +01001145 {
1146 mch_memmove(file_path.string, p,
1147 (size_t)((file_path.string + file_path.length) - p) + 1); // +1 for NUL
1148 file_path.length -= (p - file_path.string);
1149 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001150 }
1151#ifdef FF_VERBOSE
1152 if (p_verbose >= 5)
1153 {
1154 verbose_enter_scroll();
John Marriottd6e3c902025-02-18 20:45:48 +01001155 smsg("HIT: %s", file_path.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001156 // don't overwrite this either
1157 msg_puts("\n");
1158 verbose_leave_scroll();
1159 }
1160#endif
John Marriottd6e3c902025-02-18 20:45:48 +01001161 return file_path.string;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001162 }
1163
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001164 // Not found or found already, try next suffix.
1165 if (*suf == NUL)
1166 break;
John Marriottd6e3c902025-02-18 20:45:48 +01001167 file_path.length += copy_option_part(&suf, file_path.string + file_path.length,
1168 MAXPATHL - file_path.length, ",");
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001169 }
1170 }
1171 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001172 else
1173 {
1174 /*
1175 * still wildcards left, push the directories for further
1176 * search
1177 */
1178 for (i = stackp->ffs_filearray_cur;
1179 i < stackp->ffs_filearray_size; ++i)
1180 {
1181 if (!mch_isdir(stackp->ffs_filearray[i]))
1182 continue; // not a directory
1183
1184 ff_push(search_ctx,
1185 ff_create_stack_element(
1186 stackp->ffs_filearray[i],
John Marriottd6e3c902025-02-18 20:45:48 +01001187 STRLEN(stackp->ffs_filearray[i]),
1188 rest_of_wildcards.string,
1189 rest_of_wildcards.length,
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001190 stackp->ffs_level - 1, 0));
1191 }
1192 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001193 stackp->ffs_filearray_cur = 0;
1194 stackp->ffs_stage = 1;
1195 }
1196
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001197 /*
1198 * if wildcards contains '**' we have to descent till we reach the
1199 * leaves of the directory tree.
1200 */
John Marriottd6e3c902025-02-18 20:45:48 +01001201 if (STRNCMP(stackp->ffs_wc_path.string, "**", 2) == 0)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001202 {
John Marriottd6e3c902025-02-18 20:45:48 +01001203 int i;
1204
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001205 for (i = stackp->ffs_filearray_cur;
1206 i < stackp->ffs_filearray_size; ++i)
1207 {
1208 if (fnamecmp(stackp->ffs_filearray[i],
John Marriottd6e3c902025-02-18 20:45:48 +01001209 stackp->ffs_fix_path.string) == 0)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001210 continue; // don't repush same directory
1211 if (!mch_isdir(stackp->ffs_filearray[i]))
1212 continue; // not a directory
1213 ff_push(search_ctx,
John Marriottd6e3c902025-02-18 20:45:48 +01001214 ff_create_stack_element(
1215 stackp->ffs_filearray[i],
1216 STRLEN(stackp->ffs_filearray[i]),
1217 stackp->ffs_wc_path.string,
1218 stackp->ffs_wc_path.length,
1219 stackp->ffs_level - 1, 1));
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001220 }
1221 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001222
1223 // we are done with the current directory
1224 ff_free_stack_element(stackp);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001225 }
1226
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001227 // If we reached this, we didn't find anything downwards.
1228 // Let's check if we should do an upward search.
John Marriottd6e3c902025-02-18 20:45:48 +01001229 if (search_ctx->ffsc_start_dir.string
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001230 && search_ctx->ffsc_stopdirs_v != NULL && !got_int)
1231 {
1232 ff_stack_T *sptr;
zeertzjqe6ab23b2024-07-11 22:22:26 +02001233 // path_end may point to the NUL or the previous path separator
John Marriottd6e3c902025-02-18 20:45:48 +01001234 int plen = (path_end - search_ctx->ffsc_start_dir.string)
zeertzjqe6ab23b2024-07-11 22:22:26 +02001235 + (*path_end != NUL);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001236
1237 // is the last starting directory in the stop list?
John Marriottd6e3c902025-02-18 20:45:48 +01001238 if (ff_path_in_stoplist(search_ctx->ffsc_start_dir.string,
zeertzjqe6ab23b2024-07-11 22:22:26 +02001239 plen, search_ctx->ffsc_stopdirs_v) == TRUE)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001240 break;
1241
1242 // cut of last dir
John Marriottd6e3c902025-02-18 20:45:48 +01001243 while (path_end > search_ctx->ffsc_start_dir.string
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001244 && vim_ispathsep(*path_end))
1245 path_end--;
John Marriottd6e3c902025-02-18 20:45:48 +01001246 while (path_end > search_ctx->ffsc_start_dir.string
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001247 && !vim_ispathsep(path_end[-1]))
1248 path_end--;
John Marriottd6e3c902025-02-18 20:45:48 +01001249 *path_end = NUL;
1250
1251 // we may have shortened search_ctx->ffsc_start_dir, so update it's length
1252 search_ctx->ffsc_start_dir.length = (size_t)(path_end - search_ctx->ffsc_start_dir.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001253 path_end--;
1254
John Marriottd6e3c902025-02-18 20:45:48 +01001255 if (*search_ctx->ffsc_start_dir.string == NUL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001256 break;
1257
John Marriottd6e3c902025-02-18 20:45:48 +01001258 if (search_ctx->ffsc_start_dir.length + 1
1259 + search_ctx->ffsc_fix_path.length < MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001260 {
John Marriottd6e3c902025-02-18 20:45:48 +01001261 int add_sep = !after_pathsep(search_ctx->ffsc_start_dir.string,
1262 search_ctx->ffsc_start_dir.string + search_ctx->ffsc_start_dir.length);
1263 file_path.length = vim_snprintf(
1264 (char *)file_path.string,
1265 MAXPATHL,
1266 "%s%s%s",
1267 search_ctx->ffsc_start_dir.string,
1268 add_sep ? PATHSEPSTR : "",
1269 search_ctx->ffsc_fix_path.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001270 }
1271 else
1272 goto fail;
1273
1274 // create a new stack entry
John Marriottd6e3c902025-02-18 20:45:48 +01001275 sptr = ff_create_stack_element(file_path.string, file_path.length,
1276 search_ctx->ffsc_wc_path.string, search_ctx->ffsc_wc_path.length,
1277 search_ctx->ffsc_level, 0);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001278 if (sptr == NULL)
1279 break;
1280 ff_push(search_ctx, sptr);
1281 }
1282 else
1283 break;
1284 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001285
1286fail:
John Marriottd6e3c902025-02-18 20:45:48 +01001287 vim_free(file_path.string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001288 return NULL;
1289}
1290
1291/*
1292 * Free the list of lists of visited files and directories
1293 * Can handle it if the passed search_context is NULL;
1294 */
Bram Moolenaar5843f5f2019-08-20 20:13:45 +02001295 static void
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001296vim_findfile_free_visited(void *search_ctx_arg)
1297{
1298 ff_search_ctx_T *search_ctx;
1299
1300 if (search_ctx_arg == NULL)
1301 return;
1302
1303 search_ctx = (ff_search_ctx_T *)search_ctx_arg;
1304 vim_findfile_free_visited_list(&search_ctx->ffsc_visited_lists_list);
1305 vim_findfile_free_visited_list(&search_ctx->ffsc_dir_visited_lists_list);
1306}
1307
1308 static void
1309vim_findfile_free_visited_list(ff_visited_list_hdr_T **list_headp)
1310{
1311 ff_visited_list_hdr_T *vp;
1312
1313 while (*list_headp != NULL)
1314 {
1315 vp = (*list_headp)->ffvl_next;
1316 ff_free_visited_list((*list_headp)->ffvl_visited_list);
1317
1318 vim_free((*list_headp)->ffvl_filename);
1319 vim_free(*list_headp);
1320 *list_headp = vp;
1321 }
1322 *list_headp = NULL;
1323}
1324
1325 static void
1326ff_free_visited_list(ff_visited_T *vl)
1327{
1328 ff_visited_T *vp;
1329
1330 while (vl != NULL)
1331 {
1332 vp = vl->ffv_next;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001333 vim_free(vl->ffv_wc_path);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001334 vim_free(vl);
1335 vl = vp;
1336 }
1337 vl = NULL;
1338}
1339
1340/*
1341 * Returns the already visited list for the given filename. If none is found it
1342 * allocates a new one.
1343 */
1344 static ff_visited_list_hdr_T*
1345ff_get_visited_list(
1346 char_u *filename,
John Marriottd6e3c902025-02-18 20:45:48 +01001347 size_t filenamelen,
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001348 ff_visited_list_hdr_T **list_headp)
1349{
1350 ff_visited_list_hdr_T *retptr = NULL;
1351
1352 // check if a visited list for the given filename exists
1353 if (*list_headp != NULL)
1354 {
1355 retptr = *list_headp;
1356 while (retptr != NULL)
1357 {
1358 if (fnamecmp(filename, retptr->ffvl_filename) == 0)
1359 {
1360#ifdef FF_VERBOSE
1361 if (p_verbose >= 5)
1362 {
1363 verbose_enter_scroll();
1364 smsg("ff_get_visited_list: FOUND list for %s",
1365 filename);
1366 // don't overwrite this either
1367 msg_puts("\n");
1368 verbose_leave_scroll();
1369 }
1370#endif
1371 return retptr;
1372 }
1373 retptr = retptr->ffvl_next;
1374 }
1375 }
1376
1377#ifdef FF_VERBOSE
1378 if (p_verbose >= 5)
1379 {
1380 verbose_enter_scroll();
1381 smsg("ff_get_visited_list: new list for %s", filename);
1382 // don't overwrite this either
1383 msg_puts("\n");
1384 verbose_leave_scroll();
1385 }
1386#endif
1387
1388 /*
1389 * if we reach this we didn't find a list and we have to allocate new list
1390 */
Bram Moolenaarc799fe22019-05-28 23:08:19 +02001391 retptr = ALLOC_ONE(ff_visited_list_hdr_T);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001392 if (retptr == NULL)
1393 return NULL;
1394
1395 retptr->ffvl_visited_list = NULL;
John Marriottd6e3c902025-02-18 20:45:48 +01001396 retptr->ffvl_filename = vim_strnsave(filename, filenamelen);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001397 if (retptr->ffvl_filename == NULL)
1398 {
1399 vim_free(retptr);
1400 return NULL;
1401 }
1402 retptr->ffvl_next = *list_headp;
1403 *list_headp = retptr;
1404
1405 return retptr;
1406}
1407
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001408/*
1409 * check if two wildcard paths are equal. Returns TRUE or FALSE.
1410 * They are equal if:
1411 * - both paths are NULL
1412 * - they have the same length
1413 * - char by char comparison is OK
1414 * - the only differences are in the counters behind a '**', so
1415 * '**\20' is equal to '**\24'
1416 */
1417 static int
1418ff_wc_equal(char_u *s1, char_u *s2)
1419{
1420 int i, j;
1421 int c1 = NUL;
1422 int c2 = NUL;
1423 int prev1 = NUL;
1424 int prev2 = NUL;
1425
1426 if (s1 == s2)
1427 return TRUE;
1428
1429 if (s1 == NULL || s2 == NULL)
1430 return FALSE;
1431
1432 for (i = 0, j = 0; s1[i] != NUL && s2[j] != NUL;)
1433 {
1434 c1 = PTR2CHAR(s1 + i);
1435 c2 = PTR2CHAR(s2 + j);
1436
1437 if ((p_fic ? MB_TOLOWER(c1) != MB_TOLOWER(c2) : c1 != c2)
1438 && (prev1 != '*' || prev2 != '*'))
1439 return FALSE;
1440 prev2 = prev1;
1441 prev1 = c1;
1442
Bram Moolenaar1614a142019-10-06 22:00:13 +02001443 i += mb_ptr2len(s1 + i);
1444 j += mb_ptr2len(s2 + j);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001445 }
1446 return s1[i] == s2[j];
1447}
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001448
1449/*
1450 * maintains the list of already visited files and dirs
1451 * returns FAIL if the given file/dir is already in the list
1452 * returns OK if it is newly added
1453 *
1454 * TODO: What to do on memory allocation problems?
1455 * -> return TRUE - Better the file is found several times instead of
1456 * never.
1457 */
1458 static int
1459ff_check_visited(
1460 ff_visited_T **visited_list,
Bram Moolenaar2bd9dbc2022-08-25 18:12:06 +01001461 char_u *fname,
John Marriottd6e3c902025-02-18 20:45:48 +01001462 size_t fnamelen,
1463 char_u *wc_path,
1464 size_t wc_pathlen)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001465{
1466 ff_visited_T *vp;
1467#ifdef UNIX
1468 stat_T st;
1469 int url = FALSE;
1470#endif
1471
Dominique Pelleaf4a61a2021-12-27 17:21:41 +00001472 // For a URL we only compare the name, otherwise we compare the
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001473 // device/inode (unix) or the full path name (not Unix).
1474 if (path_with_url(fname))
1475 {
John Marriottd6e3c902025-02-18 20:45:48 +01001476 vim_strncpy(ff_expand_buffer.string, fname, fnamelen);
1477 ff_expand_buffer.length = fnamelen;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001478#ifdef UNIX
1479 url = TRUE;
1480#endif
1481 }
1482 else
1483 {
John Marriottd6e3c902025-02-18 20:45:48 +01001484 ff_expand_buffer.string[0] = NUL;
1485 ff_expand_buffer.length = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001486#ifdef UNIX
1487 if (mch_stat((char *)fname, &st) < 0)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001488 return FAIL;
John Marriottd6e3c902025-02-18 20:45:48 +01001489#else
1490 if (vim_FullName(fname, ff_expand_buffer.string, MAXPATHL, TRUE) == FAIL)
1491 return FAIL;
1492 ff_expand_buffer.length = STRLEN(ff_expand_buffer.string);
1493#endif
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001494 }
1495
1496 // check against list of already visited files
1497 for (vp = *visited_list; vp != NULL; vp = vp->ffv_next)
1498 {
1499 if (
1500#ifdef UNIX
1501 !url ? (vp->ffv_dev_valid && vp->ffv_dev == st.st_dev
1502 && vp->ffv_ino == st.st_ino)
1503 :
1504#endif
John Marriottd6e3c902025-02-18 20:45:48 +01001505 fnamecmp(vp->ffv_fname, ff_expand_buffer.string) == 0
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001506 )
1507 {
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001508 // are the wildcard parts equal
1509 if (ff_wc_equal(vp->ffv_wc_path, wc_path) == TRUE)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001510 // already visited
1511 return FAIL;
1512 }
1513 }
1514
1515 /*
1516 * New file/dir. Add it to the list of visited files/dirs.
1517 */
zeertzjq1b438a82023-02-01 13:11:15 +00001518 vp = alloc(
John Marriottd6e3c902025-02-18 20:45:48 +01001519 offsetof(ff_visited_T, ffv_fname) + ff_expand_buffer.length + 1);
Yegappan Lakshmanan1cfb14a2023-01-09 19:04:23 +00001520 if (vp == NULL)
1521 return OK;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001522
Yegappan Lakshmanan1cfb14a2023-01-09 19:04:23 +00001523#ifdef UNIX
1524 if (!url)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001525 {
Yegappan Lakshmanan1cfb14a2023-01-09 19:04:23 +00001526 vp->ffv_dev_valid = TRUE;
Yegappan Lakshmanan1cfb14a2023-01-09 19:04:23 +00001527 vp->ffv_dev = st.st_dev;
John Marriottd6e3c902025-02-18 20:45:48 +01001528 vp->ffv_ino = st.st_ino;
Yegappan Lakshmanan1cfb14a2023-01-09 19:04:23 +00001529 vp->ffv_fname[0] = NUL;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001530 }
Yegappan Lakshmanan1cfb14a2023-01-09 19:04:23 +00001531 else
1532 {
1533 vp->ffv_dev_valid = FALSE;
1534#endif
John Marriottd6e3c902025-02-18 20:45:48 +01001535 STRCPY(vp->ffv_fname, ff_expand_buffer.string);
Yegappan Lakshmanan1cfb14a2023-01-09 19:04:23 +00001536#ifdef UNIX
1537 }
1538#endif
1539 if (wc_path != NULL)
John Marriottd6e3c902025-02-18 20:45:48 +01001540 vp->ffv_wc_path = vim_strnsave(wc_path, wc_pathlen);
Yegappan Lakshmanan1cfb14a2023-01-09 19:04:23 +00001541 else
1542 vp->ffv_wc_path = NULL;
1543
1544 vp->ffv_next = *visited_list;
1545 *visited_list = vp;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001546
1547 return OK;
1548}
1549
1550/*
1551 * create stack element from given path pieces
1552 */
1553 static ff_stack_T *
1554ff_create_stack_element(
1555 char_u *fix_part,
John Marriottd6e3c902025-02-18 20:45:48 +01001556 size_t fix_partlen,
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001557 char_u *wc_part,
John Marriottd6e3c902025-02-18 20:45:48 +01001558 size_t wc_partlen,
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001559 int level,
1560 int star_star_empty)
1561{
1562 ff_stack_T *new;
1563
Bram Moolenaarc799fe22019-05-28 23:08:19 +02001564 new = ALLOC_ONE(ff_stack_T);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001565 if (new == NULL)
1566 return NULL;
1567
1568 new->ffs_prev = NULL;
1569 new->ffs_filearray = NULL;
1570 new->ffs_filearray_size = 0;
1571 new->ffs_filearray_cur = 0;
1572 new->ffs_stage = 0;
1573 new->ffs_level = level;
1574 new->ffs_star_star_empty = star_star_empty;
1575
1576 // the following saves NULL pointer checks in vim_findfile
1577 if (fix_part == NULL)
John Marriottd6e3c902025-02-18 20:45:48 +01001578 {
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001579 fix_part = (char_u *)"";
John Marriottd6e3c902025-02-18 20:45:48 +01001580 fix_partlen = 0;
1581 }
1582 new->ffs_fix_path.string = vim_strnsave(fix_part, fix_partlen);
1583 new->ffs_fix_path.length = fix_partlen;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001584
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001585 if (wc_part == NULL)
John Marriottd6e3c902025-02-18 20:45:48 +01001586 {
1587 wc_part = (char_u *)"";
1588 wc_partlen = 0;
1589 }
1590 new->ffs_wc_path.string = vim_strnsave(wc_part, wc_partlen);
1591 new->ffs_wc_path.length = wc_partlen;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001592
John Marriottd6e3c902025-02-18 20:45:48 +01001593 if (new->ffs_fix_path.string == NULL || new->ffs_wc_path.string == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001594 {
1595 ff_free_stack_element(new);
1596 new = NULL;
1597 }
1598
1599 return new;
1600}
1601
1602/*
1603 * Push a dir on the directory stack.
1604 */
1605 static void
1606ff_push(ff_search_ctx_T *search_ctx, ff_stack_T *stack_ptr)
1607{
1608 // check for NULL pointer, not to return an error to the user, but
1609 // to prevent a crash
Yegappan Lakshmanan7f8b2552023-01-08 13:44:24 +00001610 if (stack_ptr == NULL)
1611 return;
1612
1613 stack_ptr->ffs_prev = search_ctx->ffsc_stack_ptr;
1614 search_ctx->ffsc_stack_ptr = stack_ptr;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001615}
1616
1617/*
1618 * Pop a dir from the directory stack.
1619 * Returns NULL if stack is empty.
1620 */
1621 static ff_stack_T *
1622ff_pop(ff_search_ctx_T *search_ctx)
1623{
1624 ff_stack_T *sptr;
1625
1626 sptr = search_ctx->ffsc_stack_ptr;
1627 if (search_ctx->ffsc_stack_ptr != NULL)
1628 search_ctx->ffsc_stack_ptr = search_ctx->ffsc_stack_ptr->ffs_prev;
1629
1630 return sptr;
1631}
1632
1633/*
1634 * free the given stack element
1635 */
1636 static void
1637ff_free_stack_element(ff_stack_T *stack_ptr)
1638{
John Marriottd6e3c902025-02-18 20:45:48 +01001639 // VIM_CLEAR_STRING handles possible NULL pointers
1640 VIM_CLEAR_STRING(stack_ptr->ffs_fix_path);
1641 VIM_CLEAR_STRING(stack_ptr->ffs_wc_path);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001642
1643 if (stack_ptr->ffs_filearray != NULL)
1644 FreeWild(stack_ptr->ffs_filearray_size, stack_ptr->ffs_filearray);
1645
1646 vim_free(stack_ptr);
1647}
1648
1649/*
1650 * Clear the search context, but NOT the visited list.
1651 */
1652 static void
1653ff_clear(ff_search_ctx_T *search_ctx)
1654{
1655 ff_stack_T *sptr;
1656
1657 // clear up stack
1658 while ((sptr = ff_pop(search_ctx)) != NULL)
1659 ff_free_stack_element(sptr);
1660
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001661 if (search_ctx->ffsc_stopdirs_v != NULL)
1662 {
1663 int i = 0;
1664
John Marriottd6e3c902025-02-18 20:45:48 +01001665 while (search_ctx->ffsc_stopdirs_v[i].string != NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001666 {
John Marriottd6e3c902025-02-18 20:45:48 +01001667 vim_free(search_ctx->ffsc_stopdirs_v[i].string);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001668 i++;
1669 }
John Marriottd6e3c902025-02-18 20:45:48 +01001670 VIM_CLEAR(search_ctx->ffsc_stopdirs_v);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001671 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001672
1673 // reset everything
John Marriottd6e3c902025-02-18 20:45:48 +01001674 VIM_CLEAR_STRING(search_ctx->ffsc_file_to_search);
1675 VIM_CLEAR_STRING(search_ctx->ffsc_start_dir);
1676 VIM_CLEAR_STRING(search_ctx->ffsc_fix_path);
1677 VIM_CLEAR_STRING(search_ctx->ffsc_wc_path);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001678 search_ctx->ffsc_level = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001679}
1680
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001681/*
1682 * check if the given path is in the stopdirs
1683 * returns TRUE if yes else FALSE
1684 */
1685 static int
John Marriottd6e3c902025-02-18 20:45:48 +01001686ff_path_in_stoplist(char_u *path, int path_len, string_T *stopdirs_v)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001687{
1688 int i = 0;
1689
1690 // eat up trailing path separators, except the first
1691 while (path_len > 1 && vim_ispathsep(path[path_len - 1]))
1692 path_len--;
1693
1694 // if no path consider it as match
1695 if (path_len == 0)
1696 return TRUE;
1697
John Marriottd6e3c902025-02-18 20:45:48 +01001698 for (i = 0; stopdirs_v[i].string != NULL; i++)
zeertzjqe6ab23b2024-07-11 22:22:26 +02001699 // match for parent directory. So '/home' also matches
1700 // '/home/rks'. Check for PATHSEP in stopdirs_v[i], else
1701 // '/home/r' would also match '/home/rks'
John Marriottd6e3c902025-02-18 20:45:48 +01001702 if (fnamencmp(stopdirs_v[i].string, path, path_len) == 0
1703 && ((int)stopdirs_v[i].length <= path_len
1704 || vim_ispathsep(stopdirs_v[i].string[path_len])))
zeertzjqe6ab23b2024-07-11 22:22:26 +02001705 return TRUE;
1706
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001707 return FALSE;
1708}
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001709
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001710/*
1711 * Find the file name "ptr[len]" in the path. Also finds directory names.
1712 *
1713 * On the first call set the parameter 'first' to TRUE to initialize
1714 * the search. For repeating calls to FALSE.
1715 *
1716 * Repeating calls will return other files called 'ptr[len]' from the path.
1717 *
1718 * Only on the first call 'ptr' and 'len' are used. For repeating calls they
1719 * don't need valid values.
1720 *
1721 * If nothing found on the first call the option FNAME_MESS will issue the
1722 * message:
1723 * 'Can't find file "<file>" in path'
1724 * On repeating calls:
1725 * 'No more file "<file>" found in path'
1726 *
1727 * options:
1728 * FNAME_MESS give error message when not found
1729 *
1730 * Uses NameBuff[]!
1731 *
1732 * Returns an allocated string for the file name. NULL for error.
1733 *
1734 */
1735 char_u *
1736find_file_in_path(
1737 char_u *ptr, // file name
1738 int len, // length of file name
1739 int options,
1740 int first, // use count'th matching file name
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001741 char_u *rel_fname, // file name searching relative to
1742 char_u **file_to_find, // in/out: modified copy of file name
1743 char **search_ctx) // in/out: state of the search
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001744{
1745 return find_file_in_path_option(ptr, len, options, first,
1746 *curbuf->b_p_path == NUL ? p_path : curbuf->b_p_path,
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001747 FINDFILE_BOTH, rel_fname, curbuf->b_p_sua,
1748 file_to_find, search_ctx);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001749}
1750
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001751# if defined(EXITFREE) || defined(PROTO)
1752 void
1753free_findfile(void)
1754{
John Marriottd6e3c902025-02-18 20:45:48 +01001755 VIM_CLEAR_STRING(ff_expand_buffer);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001756}
1757# endif
1758
1759/*
1760 * Find the directory name "ptr[len]" in the path.
1761 *
1762 * options:
1763 * FNAME_MESS give error message when not found
1764 * FNAME_UNESC unescape backslashes.
1765 *
1766 * Uses NameBuff[]!
1767 *
1768 * Returns an allocated string for the file name. NULL for error.
1769 */
1770 char_u *
1771find_directory_in_path(
1772 char_u *ptr, // file name
1773 int len, // length of file name
1774 int options,
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001775 char_u *rel_fname, // file name searching relative to
1776 char_u **file_to_find, // in/out: modified copy of file name
1777 char **search_ctx) // in/out: state of the search
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001778{
1779 return find_file_in_path_option(ptr, len, options, TRUE, p_cdpath,
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001780 FINDFILE_DIR, rel_fname, (char_u *)"",
1781 file_to_find, search_ctx);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001782}
1783
1784 char_u *
1785find_file_in_path_option(
1786 char_u *ptr, // file name
1787 int len, // length of file name
1788 int options,
1789 int first, // use count'th matching file name
1790 char_u *path_option, // p_path or p_cdpath
1791 int find_what, // FINDFILE_FILE, _DIR or _BOTH
1792 char_u *rel_fname, // file name we are looking relative to.
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001793 char_u *suffixes, // list of suffixes, 'suffixesadd' option
1794 char_u **file_to_find, // in/out: modified copy of file name
1795 char **search_ctx_arg) // in/out: state of the search
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001796{
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001797 ff_search_ctx_T **search_ctx = (ff_search_ctx_T **)search_ctx_arg;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001798 static char_u *dir;
1799 static int did_findfile_init = FALSE;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001800 char_u *file_name = NULL;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001801 int rel_to_curdir;
1802# ifdef AMIGA
1803 struct Process *proc = (struct Process *)FindTask(0L);
1804 APTR save_winptr = proc->pr_WindowPtr;
1805
1806 // Avoid a requester here for a volume that doesn't exist.
1807 proc->pr_WindowPtr = (APTR)-1L;
1808# endif
John Marriottd6e3c902025-02-18 20:45:48 +01001809 static size_t file_to_findlen = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001810
1811 if (first == TRUE)
1812 {
John Marriottd6e3c902025-02-18 20:45:48 +01001813 char_u save_char;
1814
Bram Moolenaare015d992021-11-17 19:01:53 +00001815 if (len == 0)
1816 return NULL;
1817
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001818 // copy file name into NameBuff, expanding environment variables
1819 save_char = ptr[len];
1820 ptr[len] = NUL;
1821 expand_env_esc(ptr, NameBuff, MAXPATHL, FALSE, TRUE, NULL);
1822 ptr[len] = save_char;
1823
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001824 vim_free(*file_to_find);
John Marriottd6e3c902025-02-18 20:45:48 +01001825 file_to_findlen = STRLEN(NameBuff);
1826 *file_to_find = vim_strnsave(NameBuff, file_to_findlen);
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001827 if (*file_to_find == NULL) // out of memory
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001828 {
1829 file_name = NULL;
John Marriottd6e3c902025-02-18 20:45:48 +01001830 file_to_findlen = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001831 goto theend;
1832 }
1833 if (options & FNAME_UNESC)
1834 {
1835 // Change all "\ " to " ".
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001836 for (ptr = *file_to_find; *ptr != NUL; ++ptr)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001837 if (ptr[0] == '\\' && ptr[1] == ' ')
John Marriottd6e3c902025-02-18 20:45:48 +01001838 {
1839 mch_memmove(ptr, ptr + 1,
1840 (size_t)((*file_to_find + file_to_findlen) - (ptr + 1)) + 1);
1841 --file_to_findlen;
1842 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001843 }
1844 }
1845
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001846 rel_to_curdir = ((*file_to_find)[0] == '.'
1847 && ((*file_to_find)[1] == NUL
1848 || vim_ispathsep((*file_to_find)[1])
1849 || ((*file_to_find)[1] == '.'
1850 && ((*file_to_find)[2] == NUL
1851 || vim_ispathsep((*file_to_find)[2])))));
1852 if (vim_isAbsName(*file_to_find)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001853 // "..", "../path", "." and "./path": don't use the path_option
1854 || rel_to_curdir
1855# if defined(MSWIN)
1856 // handle "\tmp" as absolute path
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001857 || vim_ispathsep((*file_to_find)[0])
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001858 // handle "c:name" as absolute path
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001859 || ((*file_to_find)[0] != NUL && (*file_to_find)[1] == ':')
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001860# endif
1861# ifdef AMIGA
1862 // handle ":tmp" as absolute path
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001863 || (*file_to_find)[0] == ':'
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001864# endif
1865 )
1866 {
1867 /*
1868 * Absolute path, no need to use "path_option".
1869 * If this is not a first call, return NULL. We already returned a
1870 * filename on the first call.
1871 */
1872 if (first == TRUE)
1873 {
1874 int l;
1875 int run;
John Marriottd6e3c902025-02-18 20:45:48 +01001876 size_t rel_fnamelen = 0;
1877 char_u *suffix;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001878
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001879 if (path_with_url(*file_to_find))
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001880 {
John Marriottd6e3c902025-02-18 20:45:48 +01001881 file_name = vim_strnsave(*file_to_find, file_to_findlen);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001882 goto theend;
1883 }
1884
John Marriottd6e3c902025-02-18 20:45:48 +01001885 if (rel_fname != NULL)
1886 rel_fnamelen = STRLEN(rel_fname);
1887
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001888 // When FNAME_REL flag given first use the directory of the file.
1889 // Otherwise or when this fails use the current directory.
1890 for (run = 1; run <= 2; ++run)
1891 {
John Marriottd6e3c902025-02-18 20:45:48 +01001892 l = (int)file_to_findlen;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001893 if (run == 1
1894 && rel_to_curdir
1895 && (options & FNAME_REL)
1896 && rel_fname != NULL
John Marriottd6e3c902025-02-18 20:45:48 +01001897 && rel_fnamelen + l < MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001898 {
John Marriottd6e3c902025-02-18 20:45:48 +01001899 l = vim_snprintf(
1900 (char *)NameBuff,
1901 MAXPATHL,
1902 "%.*s%s",
1903 (int)(gettail(rel_fname) - rel_fname),
1904 rel_fname,
1905 *file_to_find);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001906 }
1907 else
1908 {
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001909 STRCPY(NameBuff, *file_to_find);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001910 run = 2;
1911 }
1912
1913 // When the file doesn't exist, try adding parts of
1914 // 'suffixesadd'.
John Marriottd6e3c902025-02-18 20:45:48 +01001915 suffix = suffixes;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001916 for (;;)
1917 {
1918 if (mch_getperm(NameBuff) >= 0
1919 && (find_what == FINDFILE_BOTH
1920 || ((find_what == FINDFILE_DIR)
1921 == mch_isdir(NameBuff))))
1922 {
John Marriottd6e3c902025-02-18 20:45:48 +01001923 file_name = vim_strnsave(NameBuff, l);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001924 goto theend;
1925 }
John Marriottd6e3c902025-02-18 20:45:48 +01001926 if (*suffix == NUL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001927 break;
John Marriottd6e3c902025-02-18 20:45:48 +01001928 l += copy_option_part(&suffix, NameBuff + l, MAXPATHL - l, ",");
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001929 }
1930 }
1931 }
1932 }
1933 else
1934 {
1935 /*
1936 * Loop over all paths in the 'path' or 'cdpath' option.
1937 * When "first" is set, first setup to the start of the option.
1938 * Otherwise continue to find the next match.
1939 */
1940 if (first == TRUE)
1941 {
1942 // vim_findfile_free_visited can handle a possible NULL pointer
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001943 vim_findfile_free_visited(*search_ctx);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001944 dir = path_option;
1945 did_findfile_init = FALSE;
1946 }
1947
1948 for (;;)
1949 {
1950 if (did_findfile_init)
1951 {
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001952 file_name = vim_findfile(*search_ctx);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001953 if (file_name != NULL)
1954 break;
1955
1956 did_findfile_init = FALSE;
1957 }
1958 else
1959 {
John Marriottd6e3c902025-02-18 20:45:48 +01001960 char_u *buf;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001961 char_u *r_ptr;
1962
1963 if (dir == NULL || *dir == NUL)
1964 {
1965 // We searched all paths of the option, now we can
1966 // free the search context.
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001967 vim_findfile_cleanup(*search_ctx);
1968 *search_ctx = NULL;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001969 break;
1970 }
1971
Bram Moolenaar51e14382019-05-25 20:21:28 +02001972 if ((buf = alloc(MAXPATHL)) == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001973 break;
1974
1975 // copy next path
John Marriottd6e3c902025-02-18 20:45:48 +01001976 buf[0] = NUL;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001977 copy_option_part(&dir, buf, MAXPATHL, " ,");
1978
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001979 // get the stopdir string
1980 r_ptr = vim_findfile_stopdir(buf);
John Marriottd6e3c902025-02-18 20:45:48 +01001981 *search_ctx = vim_findfile_init(buf, *file_to_find, file_to_findlen,
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001982 r_ptr, 100, FALSE, find_what,
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001983 *search_ctx, FALSE, rel_fname);
1984 if (*search_ctx != NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001985 did_findfile_init = TRUE;
1986 vim_free(buf);
1987 }
1988 }
1989 }
1990 if (file_name == NULL && (options & FNAME_MESS))
1991 {
1992 if (first == TRUE)
1993 {
1994 if (find_what == FINDFILE_DIR)
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001995 semsg(_(e_cant_find_directory_str_in_cdpath), *file_to_find);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001996 else
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00001997 semsg(_(e_cant_find_file_str_in_path), *file_to_find);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01001998 }
1999 else
2000 {
2001 if (find_what == FINDFILE_DIR)
Bram Moolenaareaaac012022-01-02 17:00:40 +00002002 semsg(_(e_no_more_directory_str_found_in_cdpath),
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00002003 *file_to_find);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002004 else
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00002005 semsg(_(e_no_more_file_str_found_in_path), *file_to_find);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002006 }
2007 }
2008
2009theend:
2010# ifdef AMIGA
2011 proc->pr_WindowPtr = save_winptr;
2012# endif
2013 return file_name;
2014}
2015
2016/*
2017 * Get the file name at the cursor.
2018 * If Visual mode is active, use the selected text if it's in one line.
2019 * Returns the name in allocated memory, NULL for failure.
2020 */
2021 char_u *
2022grab_file_name(long count, linenr_T *file_lnum)
2023{
2024 int options = FNAME_MESS|FNAME_EXP|FNAME_REL|FNAME_UNESC;
2025
2026 if (VIsual_active)
2027 {
2028 int len;
2029 char_u *ptr;
2030
2031 if (get_visual_text(NULL, &ptr, &len) == FAIL)
2032 return NULL;
Bram Moolenaarefd5d8a2020-09-14 19:11:45 +02002033 // Only recognize ":123" here
Keith Thompson184f71c2024-01-04 21:19:04 +01002034 if (file_lnum != NULL && ptr[len] == ':' && SAFE_isdigit(ptr[len + 1]))
Bram Moolenaarefd5d8a2020-09-14 19:11:45 +02002035 {
2036 char_u *p = ptr + len + 1;
2037
2038 *file_lnum = getdigits(&p);
2039 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002040 return find_file_name_in_path(ptr, len, options,
2041 count, curbuf->b_ffname);
2042 }
2043 return file_name_at_cursor(options | FNAME_HYP, count, file_lnum);
2044}
2045
2046/*
2047 * Return the file name under or after the cursor.
2048 *
2049 * The 'path' option is searched if the file name is not absolute.
2050 * The string returned has been alloc'ed and should be freed by the caller.
2051 * NULL is returned if the file name or file is not found.
2052 *
2053 * options:
2054 * FNAME_MESS give error messages
2055 * FNAME_EXP expand to path
2056 * FNAME_HYP check for hypertext link
2057 * FNAME_INCL apply "includeexpr"
2058 */
2059 char_u *
2060file_name_at_cursor(int options, long count, linenr_T *file_lnum)
2061{
2062 return file_name_in_line(ml_get_curline(),
2063 curwin->w_cursor.col, options, count, curbuf->b_ffname,
2064 file_lnum);
2065}
2066
2067/*
2068 * Return the name of the file under or after ptr[col].
2069 * Otherwise like file_name_at_cursor().
2070 */
2071 char_u *
2072file_name_in_line(
2073 char_u *line,
2074 int col,
2075 int options,
2076 long count,
2077 char_u *rel_fname, // file we are searching relative to
2078 linenr_T *file_lnum) // line number after the file name
2079{
2080 char_u *ptr;
2081 int len;
2082 int in_type = TRUE;
2083 int is_url = FALSE;
2084
2085 /*
2086 * search forward for what could be the start of a file name
2087 */
2088 ptr = line + col;
2089 while (*ptr != NUL && !vim_isfilec(*ptr))
2090 MB_PTR_ADV(ptr);
2091 if (*ptr == NUL) // nothing found
2092 {
2093 if (options & FNAME_MESS)
Bram Moolenaarac78dd42022-01-02 19:25:26 +00002094 emsg(_(e_no_file_name_under_cursor));
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002095 return NULL;
2096 }
2097
2098 /*
2099 * Search backward for first char of the file name.
2100 * Go one char back to ":" before "//" even when ':' is not in 'isfname'.
2101 */
2102 while (ptr > line)
2103 {
2104 if (has_mbyte && (len = (*mb_head_off)(line, ptr - 1)) > 0)
2105 ptr -= len + 1;
2106 else if (vim_isfilec(ptr[-1])
2107 || ((options & FNAME_HYP) && path_is_url(ptr - 1)))
2108 --ptr;
2109 else
2110 break;
2111 }
2112
2113 /*
2114 * Search forward for the last char of the file name.
2115 * Also allow "://" when ':' is not in 'isfname'.
2116 */
2117 len = 0;
2118 while (vim_isfilec(ptr[len]) || (ptr[len] == '\\' && ptr[len + 1] == ' ')
Bram Moolenaar747f1102022-09-18 13:06:41 +01002119 || ((options & FNAME_HYP) && path_is_url(ptr + len))
2120 || (is_url && vim_strchr((char_u *)":?&=", ptr[len]) != NULL))
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002121 {
Bram Moolenaar747f1102022-09-18 13:06:41 +01002122 // After type:// we also include :, ?, & and = as valid characters, so
2123 // that http://google.com:8080?q=this&that=ok works.
2124 if ((ptr[len] >= 'A' && ptr[len] <= 'Z')
2125 || (ptr[len] >= 'a' && ptr[len] <= 'z'))
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002126 {
2127 if (in_type && path_is_url(ptr + len + 1))
2128 is_url = TRUE;
2129 }
2130 else
2131 in_type = FALSE;
2132
2133 if (ptr[len] == '\\')
2134 // Skip over the "\" in "\ ".
2135 ++len;
2136 if (has_mbyte)
2137 len += (*mb_ptr2len)(ptr + len);
2138 else
2139 ++len;
2140 }
2141
2142 /*
2143 * If there is trailing punctuation, remove it.
2144 * But don't remove "..", could be a directory name.
2145 */
2146 if (len > 2 && vim_strchr((char_u *)".,:;!", ptr[len - 1]) != NULL
2147 && ptr[len - 2] != '.')
2148 --len;
2149
2150 if (file_lnum != NULL)
2151 {
John Marriottd6e3c902025-02-18 20:45:48 +01002152 char_u *p;
2153 char *match_text = " line "; // english
2154 size_t match_textlen = 6;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002155
Bram Moolenaar64e74c92019-12-22 15:38:06 +01002156 // Get the number after the file name and a separator character.
2157 // Also accept " line 999" with and without the same translation as
2158 // used in last_set_msg().
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002159 p = ptr + len;
John Marriottd6e3c902025-02-18 20:45:48 +01002160 if (STRNCMP(p, match_text, match_textlen) == 0)
2161 p += match_textlen;
Bram Moolenaar64e74c92019-12-22 15:38:06 +01002162 else
John Marriottd6e3c902025-02-18 20:45:48 +01002163 {
2164 // no match with english, try localized
2165 match_text = _(line_msg);
2166 match_textlen = STRLEN(match_text);
2167
2168 if (STRNCMP(p, match_text, match_textlen) == 0)
2169 p += match_textlen;
2170 else
2171 p = skipwhite(p);
2172 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002173 if (*p != NUL)
2174 {
Keith Thompson184f71c2024-01-04 21:19:04 +01002175 if (!SAFE_isdigit(*p))
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002176 ++p; // skip the separator
2177 p = skipwhite(p);
Keith Thompson184f71c2024-01-04 21:19:04 +01002178 if (SAFE_isdigit(*p))
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002179 *file_lnum = (int)getdigits(&p);
2180 }
2181 }
2182
2183 return find_file_name_in_path(ptr, len, options, count, rel_fname);
2184}
2185
2186# if defined(FEAT_FIND_ID) && defined(FEAT_EVAL)
2187 static char_u *
2188eval_includeexpr(char_u *ptr, int len)
2189{
2190 char_u *res;
Bram Moolenaar47bcc5f2022-01-22 20:19:22 +00002191 sctx_T save_sctx = current_sctx;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002192
2193 set_vim_var_string(VV_FNAME, ptr, len);
Bram Moolenaar47bcc5f2022-01-22 20:19:22 +00002194 current_sctx = curbuf->b_p_script_ctx[BV_INEX];
2195
Bram Moolenaarb171fb12020-06-24 20:34:03 +02002196 res = eval_to_string_safe(curbuf->b_p_inex,
Bram Moolenaara4e0b972022-10-01 19:43:52 +01002197 was_set_insecurely((char_u *)"includeexpr", OPT_LOCAL),
2198 TRUE, TRUE);
Bram Moolenaar47bcc5f2022-01-22 20:19:22 +00002199
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002200 set_vim_var_string(VV_FNAME, NULL, 0);
Bram Moolenaar47bcc5f2022-01-22 20:19:22 +00002201 current_sctx = save_sctx;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002202 return res;
2203}
2204# endif
2205
2206/*
2207 * Return the name of the file ptr[len] in 'path'.
2208 * Otherwise like file_name_at_cursor().
2209 */
2210 char_u *
2211find_file_name_in_path(
2212 char_u *ptr,
2213 int len,
2214 int options,
2215 long count,
2216 char_u *rel_fname) // file we are searching relative to
2217{
2218 char_u *file_name;
2219 int c;
2220# if defined(FEAT_FIND_ID) && defined(FEAT_EVAL)
2221 char_u *tofree = NULL;
Bram Moolenaar615ddd52021-11-17 18:00:31 +00002222# endif
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002223
Bram Moolenaar615ddd52021-11-17 18:00:31 +00002224 if (len == 0)
2225 return NULL;
2226
2227# if defined(FEAT_FIND_ID) && defined(FEAT_EVAL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002228 if ((options & FNAME_INCL) && *curbuf->b_p_inex != NUL)
2229 {
2230 tofree = eval_includeexpr(ptr, len);
2231 if (tofree != NULL)
2232 {
2233 ptr = tofree;
2234 len = (int)STRLEN(ptr);
2235 }
2236 }
2237# endif
2238
2239 if (options & FNAME_EXP)
2240 {
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00002241 char_u *file_to_find = NULL;
2242 char *search_ctx = NULL;
2243
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002244 file_name = find_file_in_path(ptr, len, options & ~FNAME_MESS,
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00002245 TRUE, rel_fname, &file_to_find, &search_ctx);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002246
2247# if defined(FEAT_FIND_ID) && defined(FEAT_EVAL)
2248 /*
2249 * If the file could not be found in a normal way, try applying
2250 * 'includeexpr' (unless done already).
2251 */
2252 if (file_name == NULL
2253 && !(options & FNAME_INCL) && *curbuf->b_p_inex != NUL)
2254 {
2255 tofree = eval_includeexpr(ptr, len);
2256 if (tofree != NULL)
2257 {
2258 ptr = tofree;
2259 len = (int)STRLEN(ptr);
2260 file_name = find_file_in_path(ptr, len, options & ~FNAME_MESS,
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00002261 TRUE, rel_fname, &file_to_find, &search_ctx);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002262 }
2263 }
2264# endif
2265 if (file_name == NULL && (options & FNAME_MESS))
2266 {
2267 c = ptr[len];
2268 ptr[len] = NUL;
Bram Moolenaarac78dd42022-01-02 19:25:26 +00002269 semsg(_(e_cant_find_file_str_in_path_2), ptr);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002270 ptr[len] = c;
2271 }
2272
2273 // Repeat finding the file "count" times. This matters when it
2274 // appears several times in the path.
2275 while (file_name != NULL && --count > 0)
2276 {
2277 vim_free(file_name);
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00002278 file_name = find_file_in_path(ptr, len, options, FALSE, rel_fname,
2279 &file_to_find, &search_ctx);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002280 }
Bram Moolenaar5145c9a2023-03-11 13:55:53 +00002281
2282 vim_free(file_to_find);
2283 vim_findfile_cleanup(search_ctx);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002284 }
2285 else
2286 file_name = vim_strnsave(ptr, len);
2287
2288# if defined(FEAT_FIND_ID) && defined(FEAT_EVAL)
2289 vim_free(tofree);
2290# endif
2291
2292 return file_name;
2293}
2294
2295/*
2296 * Return the end of the directory name, on the first path
2297 * separator:
2298 * "/path/file", "/path/dir/", "/path//dir", "/file"
2299 * ^ ^ ^ ^
2300 */
2301 static char_u *
2302gettail_dir(char_u *fname)
2303{
2304 char_u *dir_end = fname;
2305 char_u *next_dir_end = fname;
2306 int look_for_sep = TRUE;
2307 char_u *p;
2308
2309 for (p = fname; *p != NUL; )
2310 {
2311 if (vim_ispathsep(*p))
2312 {
2313 if (look_for_sep)
2314 {
2315 next_dir_end = p;
2316 look_for_sep = FALSE;
2317 }
2318 }
2319 else
2320 {
2321 if (!look_for_sep)
2322 dir_end = next_dir_end;
2323 look_for_sep = TRUE;
2324 }
2325 MB_PTR_ADV(p);
2326 }
2327 return dir_end;
2328}
2329
2330/*
2331 * return TRUE if 'c' is a path list separator.
2332 */
2333 int
2334vim_ispathlistsep(int c)
2335{
2336# ifdef UNIX
2337 return (c == ':');
2338# else
2339 return (c == ';'); // might not be right for every system...
2340# endif
2341}
2342
2343/*
2344 * Moves "*psep" back to the previous path separator in "path".
2345 * Returns FAIL is "*psep" ends up at the beginning of "path".
2346 */
2347 static int
2348find_previous_pathsep(char_u *path, char_u **psep)
2349{
2350 // skip the current separator
2351 if (*psep > path && vim_ispathsep(**psep))
2352 --*psep;
2353
2354 // find the previous separator
2355 while (*psep > path)
2356 {
2357 if (vim_ispathsep(**psep))
2358 return OK;
2359 MB_PTR_BACK(path, *psep);
2360 }
2361
2362 return FAIL;
2363}
2364
2365/*
2366 * Returns TRUE if "maybe_unique" is unique wrt other_paths in "gap".
2367 * "maybe_unique" is the end portion of "((char_u **)gap->ga_data)[i]".
2368 */
2369 static int
2370is_unique(char_u *maybe_unique, garray_T *gap, int i)
2371{
2372 int j;
John Marriottd6e3c902025-02-18 20:45:48 +01002373 int candidate_len = (int)STRLEN(maybe_unique);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002374 int other_path_len;
2375 char_u **other_paths = (char_u **)gap->ga_data;
2376 char_u *rival;
2377
2378 for (j = 0; j < gap->ga_len; j++)
2379 {
2380 if (j == i)
2381 continue; // don't compare it with itself
2382
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002383 other_path_len = (int)STRLEN(other_paths[j]);
2384 if (other_path_len < candidate_len)
2385 continue; // it's different when it's shorter
2386
2387 rival = other_paths[j] + other_path_len - candidate_len;
2388 if (fnamecmp(maybe_unique, rival) == 0
2389 && (rival == other_paths[j] || vim_ispathsep(*(rival - 1))))
2390 return FALSE; // match
2391 }
2392
2393 return TRUE; // no match found
2394}
2395
2396/*
2397 * Split the 'path' option into an array of strings in garray_T. Relative
2398 * paths are expanded to their equivalent fullpath. This includes the "."
2399 * (relative to current buffer directory) and empty path (relative to current
2400 * directory) notations.
2401 *
2402 * TODO: handle upward search (;) and path limiter (**N) notations by
2403 * expanding each into their equivalent path(s).
2404 */
2405 static void
LemonBoya20bf692024-07-11 22:35:53 +02002406expand_path_option(
2407 char_u *curdir,
2408 char_u *path_option, // p_path or p_cdpath
2409 garray_T *gap)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002410{
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002411 char_u *buf;
John Marriottd6e3c902025-02-18 20:45:48 +01002412 size_t buflen;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002413 char_u *p;
John Marriottd6e3c902025-02-18 20:45:48 +01002414 size_t curdirlen = 0;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002415
Bram Moolenaar51e14382019-05-25 20:21:28 +02002416 if ((buf = alloc(MAXPATHL)) == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002417 return;
2418
2419 while (*path_option != NUL)
2420 {
John Marriottd6e3c902025-02-18 20:45:48 +01002421 buflen = copy_option_part(&path_option, buf, MAXPATHL, " ,");
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002422
2423 if (buf[0] == '.' && (buf[1] == NUL || vim_ispathsep(buf[1])))
2424 {
John Marriottd6e3c902025-02-18 20:45:48 +01002425 size_t plen;
2426
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002427 // Relative to current buffer:
2428 // "/path/file" + "." -> "/path/"
2429 // "/path/file" + "./subdir" -> "/path/subdir"
2430 if (curbuf->b_ffname == NULL)
2431 continue;
2432 p = gettail(curbuf->b_ffname);
John Marriottd6e3c902025-02-18 20:45:48 +01002433 plen = (size_t)(p - curbuf->b_ffname);
2434 if (plen + buflen >= MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002435 continue;
2436 if (buf[1] == NUL)
John Marriottd6e3c902025-02-18 20:45:48 +01002437 buf[plen] = NUL;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002438 else
John Marriottd6e3c902025-02-18 20:45:48 +01002439 mch_memmove(buf + plen, buf + 2, (buflen - 2) + 1); // +1 for NUL
2440 mch_memmove(buf, curbuf->b_ffname, plen);
2441 buflen = simplify_filename(buf);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002442 }
2443 else if (buf[0] == NUL)
John Marriottd6e3c902025-02-18 20:45:48 +01002444 {
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002445 // relative to current directory
2446 STRCPY(buf, curdir);
John Marriottd6e3c902025-02-18 20:45:48 +01002447 if (curdirlen == 0)
2448 curdirlen = STRLEN(curdir);
2449 buflen = curdirlen;
2450 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002451 else if (path_with_url(buf))
2452 // URL can't be used here
2453 continue;
2454 else if (!mch_isFullName(buf))
2455 {
2456 // Expand relative path to their full path equivalent
John Marriottd6e3c902025-02-18 20:45:48 +01002457 if (curdirlen == 0)
2458 curdirlen = STRLEN(curdir);
2459 if (curdirlen + buflen + 3 > MAXPATHL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002460 continue;
John Marriottd6e3c902025-02-18 20:45:48 +01002461
2462 mch_memmove(buf + curdirlen + 1, buf, buflen + 1); // +1 for NUL
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002463 STRCPY(buf, curdir);
John Marriottd6e3c902025-02-18 20:45:48 +01002464 buf[curdirlen] = PATHSEP;
2465 buflen = simplify_filename(buf);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002466 }
2467
2468 if (ga_grow(gap, 1) == FAIL)
2469 break;
2470
2471# if defined(MSWIN)
2472 // Avoid the path ending in a backslash, it fails when a comma is
2473 // appended.
John Marriottd6e3c902025-02-18 20:45:48 +01002474 if (buf[buflen - 1] == '\\')
2475 buf[buflen - 1] = '/';
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002476# endif
2477
John Marriottd6e3c902025-02-18 20:45:48 +01002478 p = vim_strnsave(buf, buflen);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002479 if (p == NULL)
2480 break;
2481 ((char_u **)gap->ga_data)[gap->ga_len++] = p;
2482 }
2483
2484 vim_free(buf);
2485}
2486
2487/*
2488 * Returns a pointer to the file or directory name in "fname" that matches the
2489 * longest path in "ga"p, or NULL if there is no match. For example:
2490 *
2491 * path: /foo/bar/baz
2492 * fname: /foo/bar/baz/quux.txt
2493 * returns: ^this
2494 */
2495 static char_u *
2496get_path_cutoff(char_u *fname, garray_T *gap)
2497{
2498 int i;
2499 int maxlen = 0;
2500 char_u **path_part = (char_u **)gap->ga_data;
2501 char_u *cutoff = NULL;
2502
2503 for (i = 0; i < gap->ga_len; i++)
2504 {
2505 int j = 0;
2506
2507 while ((fname[j] == path_part[i][j]
2508# if defined(MSWIN)
2509 || (vim_ispathsep(fname[j]) && vim_ispathsep(path_part[i][j]))
2510# endif
2511 ) && fname[j] != NUL && path_part[i][j] != NUL)
2512 j++;
2513 if (j > maxlen)
2514 {
2515 maxlen = j;
2516 cutoff = &fname[j];
2517 }
2518 }
2519
2520 // skip to the file or directory name
2521 if (cutoff != NULL)
2522 while (vim_ispathsep(*cutoff))
2523 MB_PTR_ADV(cutoff);
2524
2525 return cutoff;
2526}
2527
2528/*
2529 * Sorts, removes duplicates and modifies all the fullpath names in "gap" so
2530 * that they are unique with respect to each other while conserving the part
2531 * that matches the pattern. Beware, this is at least O(n^2) wrt "gap->ga_len".
2532 */
2533 void
LemonBoya20bf692024-07-11 22:35:53 +02002534uniquefy_paths(
2535 garray_T *gap,
2536 char_u *pattern,
2537 char_u *path_option) // p_path or p_cdpath
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002538{
2539 int i;
2540 int len;
2541 char_u **fnames = (char_u **)gap->ga_data;
2542 int sort_again = FALSE;
2543 char_u *pat;
2544 char_u *file_pattern;
2545 char_u *curdir;
2546 regmatch_T regmatch;
2547 garray_T path_ga;
2548 char_u **in_curdir = NULL;
2549 char_u *short_name;
2550
2551 remove_duplicates(gap);
Bram Moolenaar04935fb2022-01-08 16:19:22 +00002552 ga_init2(&path_ga, sizeof(char_u *), 1);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002553
2554 /*
2555 * We need to prepend a '*' at the beginning of file_pattern so that the
2556 * regex matches anywhere in the path. FIXME: is this valid for all
2557 * possible patterns?
2558 */
2559 len = (int)STRLEN(pattern);
2560 file_pattern = alloc(len + 2);
2561 if (file_pattern == NULL)
2562 return;
2563 file_pattern[0] = '*';
John Marriottd6e3c902025-02-18 20:45:48 +01002564 STRCPY(file_pattern + 1, pattern);
Christian Brabandt1a31c432024-10-06 16:34:20 +02002565 pat = file_pat_to_reg_pat(file_pattern, NULL, NULL, FALSE);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002566 vim_free(file_pattern);
2567 if (pat == NULL)
2568 return;
2569
2570 regmatch.rm_ic = TRUE; // always ignore case
2571 regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
2572 vim_free(pat);
2573 if (regmatch.regprog == NULL)
2574 return;
2575
Bram Moolenaar51e14382019-05-25 20:21:28 +02002576 if ((curdir = alloc(MAXPATHL)) == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002577 goto theend;
2578 mch_dirname(curdir, MAXPATHL);
LemonBoya20bf692024-07-11 22:35:53 +02002579 expand_path_option(curdir, path_option, &path_ga);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002580
Bram Moolenaarc799fe22019-05-28 23:08:19 +02002581 in_curdir = ALLOC_CLEAR_MULT(char_u *, gap->ga_len);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002582 if (in_curdir == NULL)
2583 goto theend;
2584
2585 for (i = 0; i < gap->ga_len && !got_int; i++)
2586 {
2587 char_u *path = fnames[i];
2588 int is_in_curdir;
2589 char_u *dir_end = gettail_dir(path);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002590 char_u *path_cutoff;
2591
2592 len = (int)STRLEN(path);
2593 is_in_curdir = fnamencmp(curdir, path, dir_end - path) == 0
2594 && curdir[dir_end - path] == NUL;
2595 if (is_in_curdir)
John Marriottd6e3c902025-02-18 20:45:48 +01002596 in_curdir[i] = vim_strnsave(path, len);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002597
2598 // Shorten the filename while maintaining its uniqueness
2599 path_cutoff = get_path_cutoff(path, &path_ga);
2600
2601 // Don't assume all files can be reached without path when search
2602 // pattern starts with star star slash, so only remove path_cutoff
2603 // when possible.
2604 if (pattern[0] == '*' && pattern[1] == '*'
2605 && vim_ispathsep_nocolon(pattern[2])
2606 && path_cutoff != NULL
2607 && vim_regexec(&regmatch, path_cutoff, (colnr_T)0)
2608 && is_unique(path_cutoff, gap, i))
2609 {
2610 sort_again = TRUE;
2611 mch_memmove(path, path_cutoff, STRLEN(path_cutoff) + 1);
2612 }
2613 else
2614 {
2615 // Here all files can be reached without path, so get shortest
2616 // unique path. We start at the end of the path.
John Marriottd6e3c902025-02-18 20:45:48 +01002617 char_u *pathsep_p = path + len - 1;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002618
2619 while (find_previous_pathsep(path, &pathsep_p))
John Marriottd6e3c902025-02-18 20:45:48 +01002620 {
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002621 if (vim_regexec(&regmatch, pathsep_p + 1, (colnr_T)0)
2622 && is_unique(pathsep_p + 1, gap, i)
2623 && path_cutoff != NULL && pathsep_p + 1 >= path_cutoff)
2624 {
2625 sort_again = TRUE;
John Marriottd6e3c902025-02-18 20:45:48 +01002626 mch_memmove(path, pathsep_p + 1,
2627 (size_t)((path + len) - (pathsep_p + 1)) + 1); // +1 for NUL
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002628 break;
2629 }
John Marriottd6e3c902025-02-18 20:45:48 +01002630 }
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002631 }
2632
2633 if (mch_isFullName(path))
2634 {
2635 /*
2636 * Last resort: shorten relative to curdir if possible.
2637 * 'possible' means:
2638 * 1. It is under the current directory.
2639 * 2. The result is actually shorter than the original.
2640 *
2641 * Before curdir After
2642 * /foo/bar/file.txt /foo/bar ./file.txt
2643 * c:\foo\bar\file.txt c:\foo\bar .\file.txt
2644 * /file.txt / /file.txt
2645 * c:\file.txt c:\ .\file.txt
2646 */
2647 short_name = shorten_fname(path, curdir);
2648 if (short_name != NULL && short_name > path + 1
2649# if defined(MSWIN)
2650 // On windows,
2651 // shorten_fname("c:\a\a.txt", "c:\a\b")
2652 // returns "\a\a.txt", which is not really the short
2653 // name, hence:
2654 && !vim_ispathsep(*short_name)
2655# endif
2656 )
2657 {
John Marriottd6e3c902025-02-18 20:45:48 +01002658 vim_snprintf((char *)path, MAXPATHL, ".%s%s", PATHSEPSTR, short_name);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002659 }
2660 }
2661 ui_breakcheck();
2662 }
2663
2664 // Shorten filenames in /in/current/directory/{filename}
2665 for (i = 0; i < gap->ga_len && !got_int; i++)
2666 {
John Marriottd6e3c902025-02-18 20:45:48 +01002667 size_t rel_pathsize;
2668 char_u *rel_path;
2669 char_u *path = in_curdir[i];
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002670
2671 if (path == NULL)
2672 continue;
2673
2674 // If the {filename} is not unique, change it to ./{filename}.
2675 // Else reduce it to {filename}
2676 short_name = shorten_fname(path, curdir);
2677 if (short_name == NULL)
2678 short_name = path;
2679 if (is_unique(short_name, gap, i))
2680 {
2681 STRCPY(fnames[i], short_name);
2682 continue;
2683 }
2684
John Marriottd6e3c902025-02-18 20:45:48 +01002685 rel_pathsize = 1 + STRLEN_LITERAL(PATHSEPSTR) + STRLEN(short_name) + 1;
2686 rel_path = alloc(rel_pathsize);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002687 if (rel_path == NULL)
2688 goto theend;
John Marriottd6e3c902025-02-18 20:45:48 +01002689
2690 vim_snprintf((char *)rel_path, rel_pathsize, ".%s%s", PATHSEPSTR, short_name);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002691
2692 vim_free(fnames[i]);
2693 fnames[i] = rel_path;
2694 sort_again = TRUE;
2695 ui_breakcheck();
2696 }
2697
2698theend:
2699 vim_free(curdir);
2700 if (in_curdir != NULL)
2701 {
2702 for (i = 0; i < gap->ga_len; i++)
2703 vim_free(in_curdir[i]);
2704 vim_free(in_curdir);
2705 }
2706 ga_clear_strings(&path_ga);
2707 vim_regfree(regmatch.regprog);
2708
2709 if (sort_again)
2710 remove_duplicates(gap);
2711}
2712
2713/*
2714 * Calls globpath() with 'path' values for the given pattern and stores the
2715 * result in "gap".
2716 * Returns the total number of matches.
2717 */
2718 int
2719expand_in_path(
2720 garray_T *gap,
2721 char_u *pattern,
2722 int flags) // EW_* flags
2723{
2724 char_u *curdir;
2725 garray_T path_ga;
2726 char_u *paths = NULL;
2727 int glob_flags = 0;
LemonBoya20bf692024-07-11 22:35:53 +02002728 char_u *path_option = *curbuf->b_p_path == NUL ? p_path : curbuf->b_p_path;
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002729
Bram Moolenaar964b3742019-05-24 18:54:09 +02002730 if ((curdir = alloc(MAXPATHL)) == NULL)
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002731 return 0;
2732 mch_dirname(curdir, MAXPATHL);
2733
Bram Moolenaar04935fb2022-01-08 16:19:22 +00002734 ga_init2(&path_ga, sizeof(char_u *), 1);
LemonBoya20bf692024-07-11 22:35:53 +02002735 if (flags & EW_CDPATH)
2736 expand_path_option(curdir, p_cdpath, &path_ga);
2737 else
2738 expand_path_option(curdir, path_option, &path_ga);
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002739 vim_free(curdir);
2740 if (path_ga.ga_len == 0)
2741 return 0;
2742
2743 paths = ga_concat_strings(&path_ga, ",");
2744 ga_clear_strings(&path_ga);
2745 if (paths == NULL)
2746 return 0;
2747
2748 if (flags & EW_ICASE)
2749 glob_flags |= WILD_ICASE;
2750 if (flags & EW_ADDSLASH)
2751 glob_flags |= WILD_ADD_SLASH;
LemonBoya20bf692024-07-11 22:35:53 +02002752 globpath(paths, pattern, gap, glob_flags, !!(flags & EW_CDPATH));
Bram Moolenaar5fd0f502019-02-13 23:13:28 +01002753 vim_free(paths);
2754
2755 return gap->ga_len;
2756}
2757
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002758
2759/*
2760 * Converts a file name into a canonical form. It simplifies a file name into
2761 * its simplest form by stripping out unneeded components, if any. The
2762 * resulting file name is simplified in place and will either be the same
2763 * length as that supplied, or shorter.
2764 */
John Marriottd6e3c902025-02-18 20:45:48 +01002765 size_t
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002766simplify_filename(char_u *filename)
2767{
2768#ifndef AMIGA // Amiga doesn't have "..", it uses "/"
2769 int components = 0;
2770 char_u *p, *tail, *start;
John Marriottd6e3c902025-02-18 20:45:48 +01002771 char_u *p_end; // point to NUL at end of string "p"
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002772 int stripping_disabled = FALSE;
2773 int relative = TRUE;
2774
2775 p = filename;
2776# ifdef BACKSLASH_IN_FILENAME
Yegappan Lakshmanan6df0f272021-12-16 13:06:10 +00002777 if (p[0] != NUL && p[1] == ':') // skip "x:"
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002778 p += 2;
2779# endif
2780
2781 if (vim_ispathsep(*p))
2782 {
2783 relative = FALSE;
2784 do
2785 ++p;
2786 while (vim_ispathsep(*p));
2787 }
2788 start = p; // remember start after "c:/" or "/" or "///"
John Marriottd6e3c902025-02-18 20:45:48 +01002789 p_end = p + STRLEN(p);
Bram Moolenaarfdcbe3c2020-06-15 21:41:56 +02002790#ifdef UNIX
2791 // Posix says that "//path" is unchanged but "///path" is "/path".
2792 if (start > filename + 2)
2793 {
John Marriottd6e3c902025-02-18 20:45:48 +01002794 mch_memmove(filename + 1, p, (size_t)(p_end - p) + 1); // +1 for NUL
2795 p_end -= (size_t)(p - (filename + 1));
Bram Moolenaarfdcbe3c2020-06-15 21:41:56 +02002796 start = p = filename + 1;
2797 }
2798#endif
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002799
2800 do
2801 {
2802 // At this point "p" is pointing to the char following a single "/"
2803 // or "p" is at the "start" of the (absolute or relative) path name.
2804# ifdef VMS
2805 // VMS allows device:[path] - don't strip the [ in directory
2806 if ((*p == '[' || *p == '<') && p > filename && p[-1] == ':')
2807 {
2808 // :[ or :< composition: vms directory component
2809 ++components;
2810 p = getnextcomp(p + 1);
2811 }
2812 // allow remote calls as host"user passwd"::device:[path]
2813 else if (p[0] == ':' && p[1] == ':' && p > filename && p[-1] == '"' )
2814 {
2815 // ":: composition: vms host/passwd component
2816 ++components;
2817 p = getnextcomp(p + 2);
2818 }
2819 else
2820# endif
John Marriottd6e3c902025-02-18 20:45:48 +01002821 if (vim_ispathsep(*p))
2822 {
2823 mch_memmove(p, p + 1, (size_t)(p_end - (p + 1)) + 1); // remove duplicate "/"
2824 --p_end;
2825 }
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002826 else if (p[0] == '.' && (vim_ispathsep(p[1]) || p[1] == NUL))
2827 {
2828 if (p == start && relative)
2829 p += 1 + (p[1] != NUL); // keep single "." or leading "./"
2830 else
2831 {
2832 // Strip "./" or ".///". If we are at the end of the file name
2833 // and there is no trailing path separator, either strip "/." if
2834 // we are after "start", or strip "." if we are at the beginning
2835 // of an absolute path name .
2836 tail = p + 1;
2837 if (p[1] != NUL)
2838 while (vim_ispathsep(*tail))
2839 MB_PTR_ADV(tail);
2840 else if (p > start)
2841 --p; // strip preceding path separator
John Marriottd6e3c902025-02-18 20:45:48 +01002842
2843 mch_memmove(p, tail, (size_t)(p_end - tail) + 1);
2844 p_end -= (size_t)(tail - p);
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002845 }
2846 }
2847 else if (p[0] == '.' && p[1] == '.' &&
2848 (vim_ispathsep(p[2]) || p[2] == NUL))
2849 {
2850 // Skip to after ".." or "../" or "..///".
2851 tail = p + 2;
2852 while (vim_ispathsep(*tail))
2853 MB_PTR_ADV(tail);
2854
2855 if (components > 0) // strip one preceding component
2856 {
2857 int do_strip = FALSE;
2858 char_u saved_char;
2859 stat_T st;
2860
Bram Moolenaar217e1b82019-12-01 21:41:28 +01002861 // Don't strip for an erroneous file name.
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002862 if (!stripping_disabled)
2863 {
2864 // If the preceding component does not exist in the file
2865 // system, we strip it. On Unix, we don't accept a symbolic
2866 // link that refers to a non-existent file.
2867 saved_char = p[-1];
2868 p[-1] = NUL;
2869# ifdef UNIX
2870 if (mch_lstat((char *)filename, &st) < 0)
2871# else
2872 if (mch_stat((char *)filename, &st) < 0)
2873# endif
2874 do_strip = TRUE;
2875 p[-1] = saved_char;
2876
2877 --p;
2878 // Skip back to after previous '/'.
2879 while (p > start && !after_pathsep(start, p))
2880 MB_PTR_BACK(start, p);
2881
2882 if (!do_strip)
2883 {
2884 // If the component exists in the file system, check
2885 // that stripping it won't change the meaning of the
2886 // file name. First get information about the
2887 // unstripped file name. This may fail if the component
2888 // to strip is not a searchable directory (but a regular
2889 // file, for instance), since the trailing "/.." cannot
2890 // be applied then. We don't strip it then since we
2891 // don't want to replace an erroneous file name by
2892 // a valid one, and we disable stripping of later
2893 // components.
2894 saved_char = *tail;
2895 *tail = NUL;
2896 if (mch_stat((char *)filename, &st) >= 0)
2897 do_strip = TRUE;
2898 else
2899 stripping_disabled = TRUE;
2900 *tail = saved_char;
2901# ifdef UNIX
2902 if (do_strip)
2903 {
2904 stat_T new_st;
2905
2906 // On Unix, the check for the unstripped file name
2907 // above works also for a symbolic link pointing to
2908 // a searchable directory. But then the parent of
2909 // the directory pointed to by the link must be the
2910 // same as the stripped file name. (The latter
2911 // exists in the file system since it is the
2912 // component's parent directory.)
2913 if (p == start && relative)
2914 (void)mch_stat(".", &new_st);
2915 else
2916 {
2917 saved_char = *p;
2918 *p = NUL;
2919 (void)mch_stat((char *)filename, &new_st);
2920 *p = saved_char;
2921 }
2922
2923 if (new_st.st_ino != st.st_ino ||
2924 new_st.st_dev != st.st_dev)
2925 {
2926 do_strip = FALSE;
2927 // We don't disable stripping of later
2928 // components since the unstripped path name is
2929 // still valid.
2930 }
2931 }
2932# endif
2933 }
2934 }
2935
2936 if (!do_strip)
2937 {
2938 // Skip the ".." or "../" and reset the counter for the
2939 // components that might be stripped later on.
2940 p = tail;
2941 components = 0;
2942 }
2943 else
2944 {
2945 // Strip previous component. If the result would get empty
2946 // and there is no trailing path separator, leave a single
2947 // "." instead. If we are at the end of the file name and
2948 // there is no trailing path separator and a preceding
2949 // component is left after stripping, strip its trailing
2950 // path separator as well.
2951 if (p == start && relative && tail[-1] == '.')
2952 {
2953 *p++ = '.';
2954 *p = NUL;
2955 }
2956 else
2957 {
2958 if (p > start && tail[-1] == '.')
2959 --p;
John Marriottd6e3c902025-02-18 20:45:48 +01002960
2961 mch_memmove(p, tail, (size_t)(p_end - tail) + 1); // strip previous component
2962 p_end -= (size_t)(tail - p);
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002963 }
2964
2965 --components;
2966 }
2967 }
2968 else if (p == start && !relative) // leading "/.." or "/../"
John Marriottd6e3c902025-02-18 20:45:48 +01002969 {
2970 mch_memmove(p, tail, (size_t)(p_end - tail) + 1); // strip ".." or "../"
2971 p_end -= (size_t)(tail - p);
2972 }
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002973 else
2974 {
2975 if (p == start + 2 && p[-2] == '.') // leading "./../"
2976 {
John Marriottd6e3c902025-02-18 20:45:48 +01002977 mch_memmove(p - 2, p, (size_t)(p_end - p) + 1); // strip leading "./"
2978 p_end -= 2;
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002979 tail -= 2;
2980 }
2981 p = tail; // skip to char after ".." or "../"
2982 }
2983 }
2984 else
2985 {
2986 ++components; // simple path component
2987 p = getnextcomp(p);
2988 }
2989 } while (*p != NUL);
2990#endif // !AMIGA
John Marriottd6e3c902025-02-18 20:45:48 +01002991
2992 return (size_t)(p_end - filename);
Bram Moolenaarb4a60202019-03-31 19:40:07 +02002993}
Bram Moolenaarb005cd82019-09-04 15:54:55 +02002994
2995#if defined(FEAT_EVAL) || defined(PROTO)
2996/*
2997 * "simplify()" function
2998 */
2999 void
3000f_simplify(typval_T *argvars, typval_T *rettv)
3001{
3002 char_u *p;
3003
Yegappan Lakshmanan4490ec42021-07-27 22:00:44 +02003004 if (in_vim9script() && check_for_string_arg(argvars, 0) == FAIL)
3005 return;
3006
Bram Moolenaar3cfa5b12021-06-06 14:14:39 +02003007 p = tv_get_string_strict(&argvars[0]);
Bram Moolenaarb005cd82019-09-04 15:54:55 +02003008 rettv->vval.v_string = vim_strsave(p);
Bram Moolenaar217e1b82019-12-01 21:41:28 +01003009 simplify_filename(rettv->vval.v_string); // simplify in place
Bram Moolenaarb005cd82019-09-04 15:54:55 +02003010 rettv->v_type = VAR_STRING;
3011}
3012#endif // FEAT_EVAL