1 | // This is an open source non-commercial project. Dear PVS-Studio, please check |
2 | // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com |
3 | |
4 | // File searching functions for 'path', 'tags' and 'cdpath' options. |
5 | // |
6 | // External visible functions: |
7 | // vim_findfile_init() creates/initialises the search context |
8 | // vim_findfile_free_visited() free list of visited files/dirs of search |
9 | // context |
10 | // vim_findfile() find a file in the search context |
11 | // vim_findfile_cleanup() cleanup/free search context created by |
12 | // vim_findfile_init() |
13 | // |
14 | // All static functions and variables start with 'ff_' |
15 | // |
16 | // In general it works like this: |
17 | // First you create yourself a search context by calling vim_findfile_init(). |
18 | // It is possible to give a search context from a previous call to |
19 | // vim_findfile_init(), so it can be reused. After this you call vim_findfile() |
20 | // until you are satisfied with the result or it returns NULL. On every call it |
21 | // returns the next file which matches the conditions given to |
22 | // vim_findfile_init(). If it doesn't find a next file it returns NULL. |
23 | // |
24 | // It is possible to call vim_findfile_init() again to reinitialise your search |
25 | // with some new parameters. Don't forget to pass your old search context to |
26 | // it, so it can reuse it and especially reuse the list of already visited |
27 | // directories. If you want to delete the list of already visited directories |
28 | // simply call vim_findfile_free_visited(). |
29 | // |
30 | // When you are done call vim_findfile_cleanup() to free the search context. |
31 | // |
32 | // The function vim_findfile_init() has a long comment, which describes the |
33 | // needed parameters. |
34 | // |
35 | // |
36 | // |
37 | // ATTENTION: |
38 | // ========== |
39 | // We use an allocated search context, these functions are NOT thread-safe!!!!! |
40 | // |
41 | // To minimize parameter passing (or because I'm too lazy), only the |
42 | // external visible functions get a search context as a parameter. This is |
43 | // then assigned to a static global, which is used throughout the local |
44 | // functions. |
45 | |
46 | #include <assert.h> |
47 | #include <string.h> |
48 | #include <stdbool.h> |
49 | #include <inttypes.h> |
50 | #include <limits.h> |
51 | |
52 | #include "nvim/vim.h" |
53 | #include "nvim/eval.h" |
54 | #include "nvim/ascii.h" |
55 | #include "nvim/file_search.h" |
56 | #include "nvim/charset.h" |
57 | #include "nvim/fileio.h" |
58 | #include "nvim/memory.h" |
59 | #include "nvim/message.h" |
60 | #include "nvim/misc1.h" |
61 | #include "nvim/option.h" |
62 | #include "nvim/os_unix.h" |
63 | #include "nvim/path.h" |
64 | #include "nvim/strings.h" |
65 | #include "nvim/tag.h" |
66 | #include "nvim/window.h" |
67 | #include "nvim/os/os.h" |
68 | #include "nvim/os/input.h" |
69 | #include "nvim/os/fs_defs.h" |
70 | |
71 | static char_u *ff_expand_buffer = NULL; /* used for expanding filenames */ |
72 | |
73 | /* |
74 | * type for the directory search stack |
75 | */ |
76 | typedef struct ff_stack { |
77 | struct ff_stack *ffs_prev; |
78 | |
79 | /* the fix part (no wildcards) and the part containing the wildcards |
80 | * of the search path |
81 | */ |
82 | char_u *ffs_fix_path; |
83 | char_u *ffs_wc_path; |
84 | |
85 | /* files/dirs found in the above directory, matched by the first wildcard |
86 | * of wc_part |
87 | */ |
88 | char_u **ffs_filearray; |
89 | int ffs_filearray_size; |
90 | char_u ffs_filearray_cur; /* needed for partly handled dirs */ |
91 | |
92 | /* to store status of partly handled directories |
93 | * 0: we work on this directory for the first time |
94 | * 1: this directory was partly searched in an earlier step |
95 | */ |
96 | int ffs_stage; |
97 | |
98 | /* How deep are we in the directory tree? |
99 | * Counts backward from value of level parameter to vim_findfile_init |
100 | */ |
101 | int ffs_level; |
102 | |
103 | /* Did we already expand '**' to an empty string? */ |
104 | int ffs_star_star_empty; |
105 | } ff_stack_T; |
106 | |
107 | /* |
108 | * type for already visited directories or files. |
109 | */ |
110 | typedef struct ff_visited { |
111 | struct ff_visited *ffv_next; |
112 | |
113 | /* Visited directories are different if the wildcard string are |
114 | * different. So we have to save it. |
115 | */ |
116 | char_u *ffv_wc_path; |
117 | // use FileID for comparison (needed because of links), else use filename. |
118 | bool file_id_valid; |
119 | FileID file_id; |
120 | /* The memory for this struct is allocated according to the length of |
121 | * ffv_fname. |
122 | */ |
123 | char_u ffv_fname[1]; /* actually longer */ |
124 | } ff_visited_T; |
125 | |
126 | /* |
127 | * We might have to manage several visited lists during a search. |
128 | * This is especially needed for the tags option. If tags is set to: |
129 | * "./++/tags,./++/TAGS,++/tags" (replace + with *) |
130 | * So we have to do 3 searches: |
131 | * 1) search from the current files directory downward for the file "tags" |
132 | * 2) search from the current files directory downward for the file "TAGS" |
133 | * 3) search from Vims current directory downwards for the file "tags" |
134 | * As you can see, the first and the third search are for the same file, so for |
135 | * the third search we can use the visited list of the first search. For the |
136 | * second search we must start from an empty visited list. |
137 | * The struct ff_visited_list_hdr is used to manage a linked list of already |
138 | * visited lists. |
139 | */ |
140 | typedef struct ff_visited_list_hdr { |
141 | struct ff_visited_list_hdr *ffvl_next; |
142 | |
143 | /* the filename the attached visited list is for */ |
144 | char_u *ffvl_filename; |
145 | |
146 | ff_visited_T *ffvl_visited_list; |
147 | |
148 | } ff_visited_list_hdr_T; |
149 | |
150 | |
151 | /* |
152 | * '**' can be expanded to several directory levels. |
153 | * Set the default maximum depth. |
154 | */ |
155 | #define FF_MAX_STAR_STAR_EXPAND ((char_u)30) |
156 | |
157 | /* |
158 | * The search context: |
159 | * ffsc_stack_ptr: the stack for the dirs to search |
160 | * ffsc_visited_list: the currently active visited list |
161 | * ffsc_dir_visited_list: the currently active visited list for search dirs |
162 | * ffsc_visited_lists_list: the list of all visited lists |
163 | * ffsc_dir_visited_lists_list: the list of all visited lists for search dirs |
164 | * ffsc_file_to_search: the file to search for |
165 | * ffsc_start_dir: the starting directory, if search path was relative |
166 | * ffsc_fix_path: the fix part of the given path (without wildcards) |
167 | * Needed for upward search. |
168 | * ffsc_wc_path: the part of the given path containing wildcards |
169 | * ffsc_level: how many levels of dirs to search downwards |
170 | * ffsc_stopdirs_v: array of stop directories for upward search |
171 | * ffsc_find_what: FINDFILE_BOTH, FINDFILE_DIR or FINDFILE_FILE |
172 | * ffsc_tagfile: searching for tags file, don't use 'suffixesadd' |
173 | */ |
174 | typedef struct ff_search_ctx_T { |
175 | ff_stack_T *ffsc_stack_ptr; |
176 | ff_visited_list_hdr_T *ffsc_visited_list; |
177 | ff_visited_list_hdr_T *ffsc_dir_visited_list; |
178 | ff_visited_list_hdr_T *ffsc_visited_lists_list; |
179 | ff_visited_list_hdr_T *ffsc_dir_visited_lists_list; |
180 | char_u *ffsc_file_to_search; |
181 | char_u *ffsc_start_dir; |
182 | char_u *ffsc_fix_path; |
183 | char_u *ffsc_wc_path; |
184 | int ffsc_level; |
185 | char_u **ffsc_stopdirs_v; |
186 | int ffsc_find_what; |
187 | int ffsc_tagfile; |
188 | } ff_search_ctx_T; |
189 | |
190 | /* locally needed functions */ |
191 | |
192 | #ifdef INCLUDE_GENERATED_DECLARATIONS |
193 | # include "file_search.c.generated.h" |
194 | #endif |
195 | |
196 | static char_u e_pathtoolong[] = N_("E854: path too long for completion" ); |
197 | |
198 | /* |
199 | * Initialization routine for vim_findfile(). |
200 | * |
201 | * Returns the newly allocated search context or NULL if an error occurred. |
202 | * |
203 | * Don't forget to clean up by calling vim_findfile_cleanup() if you are done |
204 | * with the search context. |
205 | * |
206 | * Find the file 'filename' in the directory 'path'. |
207 | * The parameter 'path' may contain wildcards. If so only search 'level' |
208 | * directories deep. The parameter 'level' is the absolute maximum and is |
209 | * not related to restricts given to the '**' wildcard. If 'level' is 100 |
210 | * and you use '**200' vim_findfile() will stop after 100 levels. |
211 | * |
212 | * 'filename' cannot contain wildcards! It is used as-is, no backslashes to |
213 | * escape special characters. |
214 | * |
215 | * If 'stopdirs' is not NULL and nothing is found downward, the search is |
216 | * restarted on the next higher directory level. This is repeated until the |
217 | * start-directory of a search is contained in 'stopdirs'. 'stopdirs' has the |
218 | * format ";*<dirname>*\(;<dirname>\)*;\=$". |
219 | * |
220 | * If the 'path' is relative, the starting dir for the search is either VIM's |
221 | * current dir or if the path starts with "./" the current files dir. |
222 | * If the 'path' is absolute, the starting dir is that part of the path before |
223 | * the first wildcard. |
224 | * |
225 | * Upward search is only done on the starting dir. |
226 | * |
227 | * If 'free_visited' is TRUE the list of already visited files/directories is |
228 | * cleared. Set this to FALSE if you just want to search from another |
229 | * directory, but want to be sure that no directory from a previous search is |
230 | * searched again. This is useful if you search for a file at different places. |
231 | * The list of visited files/dirs can also be cleared with the function |
232 | * vim_findfile_free_visited(). |
233 | * |
234 | * Set the parameter 'find_what' to FINDFILE_DIR if you want to search for |
235 | * directories only, FINDFILE_FILE for files only, FINDFILE_BOTH for both. |
236 | * |
237 | * A search context returned by a previous call to vim_findfile_init() can be |
238 | * passed in the parameter "search_ctx_arg". This context is reused and |
239 | * reinitialized with the new parameters. The list of already visited |
240 | * directories from this context is only deleted if the parameter |
241 | * "free_visited" is true. Be aware that the passed "search_ctx_arg" is freed |
242 | * if the reinitialization fails. |
243 | * |
244 | * If you don't have a search context from a previous call "search_ctx_arg" |
245 | * must be NULL. |
246 | * |
247 | * This function silently ignores a few errors, vim_findfile() will have |
248 | * limited functionality then. |
249 | */ |
250 | void * |
251 | vim_findfile_init ( |
252 | char_u *path, |
253 | char_u *filename, |
254 | char_u *stopdirs, |
255 | int level, |
256 | int free_visited, |
257 | int find_what, |
258 | void *search_ctx_arg, |
259 | int tagfile, /* expanding names of tags files */ |
260 | char_u *rel_fname /* file name to use for "." */ |
261 | ) |
262 | { |
263 | char_u *wc_part; |
264 | ff_stack_T *sptr; |
265 | ff_search_ctx_T *search_ctx; |
266 | |
267 | /* If a search context is given by the caller, reuse it, else allocate a |
268 | * new one. |
269 | */ |
270 | if (search_ctx_arg != NULL) |
271 | search_ctx = search_ctx_arg; |
272 | else { |
273 | search_ctx = xcalloc(1, sizeof(ff_search_ctx_T)); |
274 | } |
275 | search_ctx->ffsc_find_what = find_what; |
276 | search_ctx->ffsc_tagfile = tagfile; |
277 | |
278 | /* clear the search context, but NOT the visited lists */ |
279 | ff_clear(search_ctx); |
280 | |
281 | /* clear visited list if wanted */ |
282 | if (free_visited == TRUE) |
283 | vim_findfile_free_visited(search_ctx); |
284 | else { |
285 | /* Reuse old visited lists. Get the visited list for the given |
286 | * filename. If no list for the current filename exists, creates a new |
287 | * one. */ |
288 | search_ctx->ffsc_visited_list = ff_get_visited_list(filename, |
289 | &search_ctx->ffsc_visited_lists_list); |
290 | if (search_ctx->ffsc_visited_list == NULL) |
291 | goto error_return; |
292 | search_ctx->ffsc_dir_visited_list = ff_get_visited_list(filename, |
293 | &search_ctx->ffsc_dir_visited_lists_list); |
294 | if (search_ctx->ffsc_dir_visited_list == NULL) |
295 | goto error_return; |
296 | } |
297 | |
298 | if (ff_expand_buffer == NULL) { |
299 | ff_expand_buffer = xmalloc(MAXPATHL); |
300 | } |
301 | |
302 | /* Store information on starting dir now if path is relative. |
303 | * If path is absolute, we do that later. */ |
304 | if (path[0] == '.' |
305 | && (vim_ispathsep(path[1]) || path[1] == NUL) |
306 | && (!tagfile || vim_strchr(p_cpo, CPO_DOTTAG) == NULL) |
307 | && rel_fname != NULL) { |
308 | size_t len = (size_t)(path_tail(rel_fname) - rel_fname); |
309 | |
310 | if (!vim_isAbsName(rel_fname) && len + 1 < MAXPATHL) { |
311 | /* Make the start dir an absolute path name. */ |
312 | STRLCPY(ff_expand_buffer, rel_fname, len + 1); |
313 | search_ctx->ffsc_start_dir = (char_u *)FullName_save((char *)ff_expand_buffer, FALSE); |
314 | } else |
315 | search_ctx->ffsc_start_dir = vim_strnsave(rel_fname, len); |
316 | if (*++path != NUL) |
317 | ++path; |
318 | } else if (*path == NUL || !vim_isAbsName(path)) { |
319 | #ifdef BACKSLASH_IN_FILENAME |
320 | /* "c:dir" needs "c:" to be expanded, otherwise use current dir */ |
321 | if (*path != NUL && path[1] == ':') { |
322 | char_u drive[3]; |
323 | |
324 | drive[0] = path[0]; |
325 | drive[1] = ':'; |
326 | drive[2] = NUL; |
327 | if (vim_FullName((const char *)drive, (char *)ff_expand_buffer, MAXPATHL, |
328 | true) |
329 | == FAIL) { |
330 | goto error_return; |
331 | } |
332 | path += 2; |
333 | } else |
334 | #endif |
335 | if (os_dirname(ff_expand_buffer, MAXPATHL) == FAIL) |
336 | goto error_return; |
337 | |
338 | search_ctx->ffsc_start_dir = vim_strsave(ff_expand_buffer); |
339 | |
340 | #ifdef BACKSLASH_IN_FILENAME |
341 | /* A path that starts with "/dir" is relative to the drive, not to the |
342 | * directory (but not for "//machine/dir"). Only use the drive name. */ |
343 | if ((*path == '/' || *path == '\\') |
344 | && path[1] != path[0] |
345 | && search_ctx->ffsc_start_dir[1] == ':') |
346 | search_ctx->ffsc_start_dir[2] = NUL; |
347 | #endif |
348 | } |
349 | |
350 | /* |
351 | * If stopdirs are given, split them into an array of pointers. |
352 | * If this fails (mem allocation), there is no upward search at all or a |
353 | * stop directory is not recognized -> continue silently. |
354 | * If stopdirs just contains a ";" or is empty, |
355 | * search_ctx->ffsc_stopdirs_v will only contain a NULL pointer. This |
356 | * is handled as unlimited upward search. See function |
357 | * ff_path_in_stoplist() for details. |
358 | */ |
359 | if (stopdirs != NULL) { |
360 | char_u *walker = stopdirs; |
361 | |
362 | while (*walker == ';') |
363 | walker++; |
364 | |
365 | size_t dircount = 1; |
366 | search_ctx->ffsc_stopdirs_v = xmalloc(sizeof(char_u *)); |
367 | |
368 | do { |
369 | char_u *helper; |
370 | void *ptr; |
371 | |
372 | helper = walker; |
373 | ptr = xrealloc(search_ctx->ffsc_stopdirs_v, |
374 | (dircount + 1) * sizeof(char_u *)); |
375 | search_ctx->ffsc_stopdirs_v = ptr; |
376 | walker = vim_strchr(walker, ';'); |
377 | if (walker) { |
378 | assert(walker - helper >= 0); |
379 | search_ctx->ffsc_stopdirs_v[dircount-1] = |
380 | vim_strnsave(helper, (size_t)(walker - helper)); |
381 | walker++; |
382 | } else |
383 | /* this might be "", which means ascent till top |
384 | * of directory tree. |
385 | */ |
386 | search_ctx->ffsc_stopdirs_v[dircount-1] = |
387 | vim_strsave(helper); |
388 | |
389 | dircount++; |
390 | |
391 | } while (walker != NULL); |
392 | search_ctx->ffsc_stopdirs_v[dircount-1] = NULL; |
393 | } |
394 | |
395 | search_ctx->ffsc_level = level; |
396 | |
397 | /* split into: |
398 | * -fix path |
399 | * -wildcard_stuff (might be NULL) |
400 | */ |
401 | wc_part = vim_strchr(path, '*'); |
402 | if (wc_part != NULL) { |
403 | int64_t llevel; |
404 | int len; |
405 | char *errpt; |
406 | |
407 | /* save the fix part of the path */ |
408 | assert(wc_part - path >= 0); |
409 | search_ctx->ffsc_fix_path = vim_strnsave(path, (size_t)(wc_part - path)); |
410 | |
411 | /* |
412 | * copy wc_path and add restricts to the '**' wildcard. |
413 | * The octet after a '**' is used as a (binary) counter. |
414 | * So '**3' is transposed to '**^C' ('^C' is ASCII value 3) |
415 | * or '**76' is transposed to '**N'( 'N' is ASCII value 76). |
416 | * If no restrict is given after '**' the default is used. |
417 | * Due to this technique the path looks awful if you print it as a |
418 | * string. |
419 | */ |
420 | len = 0; |
421 | while (*wc_part != NUL) { |
422 | if (len + 5 >= MAXPATHL) { |
423 | EMSG(_(e_pathtoolong)); |
424 | break; |
425 | } |
426 | if (STRNCMP(wc_part, "**" , 2) == 0) { |
427 | ff_expand_buffer[len++] = *wc_part++; |
428 | ff_expand_buffer[len++] = *wc_part++; |
429 | |
430 | llevel = strtol((char *)wc_part, &errpt, 10); |
431 | if ((char_u *)errpt != wc_part && llevel > 0 && llevel < 255) |
432 | ff_expand_buffer[len++] = (char_u)llevel; |
433 | else if ((char_u *)errpt != wc_part && llevel == 0) |
434 | /* restrict is 0 -> remove already added '**' */ |
435 | len -= 2; |
436 | else |
437 | ff_expand_buffer[len++] = FF_MAX_STAR_STAR_EXPAND; |
438 | wc_part = (char_u *)errpt; |
439 | if (*wc_part != NUL && !vim_ispathsep(*wc_part)) { |
440 | EMSG2(_( |
441 | "E343: Invalid path: '**[number]' must be at the end of the path or be followed by '%s'." ), |
442 | PATHSEPSTR); |
443 | goto error_return; |
444 | } |
445 | } else |
446 | ff_expand_buffer[len++] = *wc_part++; |
447 | } |
448 | ff_expand_buffer[len] = NUL; |
449 | search_ctx->ffsc_wc_path = vim_strsave(ff_expand_buffer); |
450 | } else |
451 | search_ctx->ffsc_fix_path = vim_strsave(path); |
452 | |
453 | if (search_ctx->ffsc_start_dir == NULL) { |
454 | /* store the fix part as startdir. |
455 | * This is needed if the parameter path is fully qualified. |
456 | */ |
457 | search_ctx->ffsc_start_dir = vim_strsave(search_ctx->ffsc_fix_path); |
458 | search_ctx->ffsc_fix_path[0] = NUL; |
459 | } |
460 | |
461 | /* create an absolute path */ |
462 | if (STRLEN(search_ctx->ffsc_start_dir) |
463 | + STRLEN(search_ctx->ffsc_fix_path) + 3 >= MAXPATHL) { |
464 | EMSG(_(e_pathtoolong)); |
465 | goto error_return; |
466 | } |
467 | STRCPY(ff_expand_buffer, search_ctx->ffsc_start_dir); |
468 | add_pathsep((char *)ff_expand_buffer); |
469 | { |
470 | size_t eb_len = STRLEN(ff_expand_buffer); |
471 | char_u *buf = xmalloc(eb_len + STRLEN(search_ctx->ffsc_fix_path) + 1); |
472 | |
473 | STRCPY(buf, ff_expand_buffer); |
474 | STRCPY(buf + eb_len, search_ctx->ffsc_fix_path); |
475 | if (os_isdir(buf)) { |
476 | STRCAT(ff_expand_buffer, search_ctx->ffsc_fix_path); |
477 | add_pathsep((char *)ff_expand_buffer); |
478 | } else { |
479 | char_u *p = path_tail(search_ctx->ffsc_fix_path); |
480 | char_u *wc_path = NULL; |
481 | char_u *temp = NULL; |
482 | int len = 0; |
483 | |
484 | if (p > search_ctx->ffsc_fix_path) { |
485 | len = (int)(p - search_ctx->ffsc_fix_path) - 1; |
486 | STRNCAT(ff_expand_buffer, search_ctx->ffsc_fix_path, len); |
487 | add_pathsep((char *)ff_expand_buffer); |
488 | } else { |
489 | len = (int)STRLEN(search_ctx->ffsc_fix_path); |
490 | } |
491 | |
492 | if (search_ctx->ffsc_wc_path != NULL) { |
493 | wc_path = vim_strsave(search_ctx->ffsc_wc_path); |
494 | temp = xmalloc(STRLEN(search_ctx->ffsc_wc_path) |
495 | + STRLEN(search_ctx->ffsc_fix_path + len) |
496 | + 1); |
497 | STRCPY(temp, search_ctx->ffsc_fix_path + len); |
498 | STRCAT(temp, search_ctx->ffsc_wc_path); |
499 | xfree(search_ctx->ffsc_wc_path); |
500 | xfree(wc_path); |
501 | search_ctx->ffsc_wc_path = temp; |
502 | } |
503 | } |
504 | xfree(buf); |
505 | } |
506 | |
507 | sptr = ff_create_stack_element(ff_expand_buffer, |
508 | search_ctx->ffsc_wc_path, |
509 | level, 0); |
510 | |
511 | ff_push(search_ctx, sptr); |
512 | search_ctx->ffsc_file_to_search = vim_strsave(filename); |
513 | return search_ctx; |
514 | |
515 | error_return: |
516 | /* |
517 | * We clear the search context now! |
518 | * Even when the caller gave us a (perhaps valid) context we free it here, |
519 | * as we might have already destroyed it. |
520 | */ |
521 | vim_findfile_cleanup(search_ctx); |
522 | return NULL; |
523 | } |
524 | |
525 | /* |
526 | * Get the stopdir string. Check that ';' is not escaped. |
527 | */ |
528 | char_u *vim_findfile_stopdir(char_u *buf) |
529 | { |
530 | char_u *r_ptr = buf; |
531 | |
532 | while (*r_ptr != NUL && *r_ptr != ';') { |
533 | if (r_ptr[0] == '\\' && r_ptr[1] == ';') { |
534 | /* Overwrite the escape char, |
535 | * use STRLEN(r_ptr) to move the trailing '\0'. */ |
536 | STRMOVE(r_ptr, r_ptr + 1); |
537 | r_ptr++; |
538 | } |
539 | r_ptr++; |
540 | } |
541 | if (*r_ptr == ';') { |
542 | *r_ptr = 0; |
543 | r_ptr++; |
544 | } else if (*r_ptr == NUL) |
545 | r_ptr = NULL; |
546 | return r_ptr; |
547 | } |
548 | |
549 | /* |
550 | * Clean up the given search context. Can handle a NULL pointer. |
551 | */ |
552 | void vim_findfile_cleanup(void *ctx) |
553 | { |
554 | if (ctx == NULL) |
555 | return; |
556 | |
557 | vim_findfile_free_visited(ctx); |
558 | ff_clear(ctx); |
559 | xfree(ctx); |
560 | } |
561 | |
562 | /* |
563 | * Find a file in a search context. |
564 | * The search context was created with vim_findfile_init() above. |
565 | * Return a pointer to an allocated file name or NULL if nothing found. |
566 | * To get all matching files call this function until you get NULL. |
567 | * |
568 | * If the passed search_context is NULL, NULL is returned. |
569 | * |
570 | * The search algorithm is depth first. To change this replace the |
571 | * stack with a list (don't forget to leave partly searched directories on the |
572 | * top of the list). |
573 | */ |
574 | char_u *vim_findfile(void *search_ctx_arg) |
575 | { |
576 | char_u *file_path; |
577 | char_u *rest_of_wildcards; |
578 | char_u *path_end = NULL; |
579 | ff_stack_T *stackp = NULL; |
580 | size_t len; |
581 | char_u *p; |
582 | char_u *suf; |
583 | ff_search_ctx_T *search_ctx; |
584 | |
585 | if (search_ctx_arg == NULL) |
586 | return NULL; |
587 | |
588 | search_ctx = (ff_search_ctx_T *)search_ctx_arg; |
589 | |
590 | /* |
591 | * filepath is used as buffer for various actions and as the storage to |
592 | * return a found filename. |
593 | */ |
594 | file_path = xmalloc(MAXPATHL); |
595 | |
596 | /* store the end of the start dir -- needed for upward search */ |
597 | if (search_ctx->ffsc_start_dir != NULL) |
598 | path_end = &search_ctx->ffsc_start_dir[ |
599 | STRLEN(search_ctx->ffsc_start_dir)]; |
600 | |
601 | /* upward search loop */ |
602 | for (;; ) { |
603 | /* downward search loop */ |
604 | for (;; ) { |
605 | /* check if user user wants to stop the search*/ |
606 | os_breakcheck(); |
607 | if (got_int) |
608 | break; |
609 | |
610 | /* get directory to work on from stack */ |
611 | stackp = ff_pop(search_ctx); |
612 | if (stackp == NULL) |
613 | break; |
614 | |
615 | /* |
616 | * TODO: decide if we leave this test in |
617 | * |
618 | * GOOD: don't search a directory(-tree) twice. |
619 | * BAD: - check linked list for every new directory entered. |
620 | * - check for double files also done below |
621 | * |
622 | * Here we check if we already searched this directory. |
623 | * We already searched a directory if: |
624 | * 1) The directory is the same. |
625 | * 2) We would use the same wildcard string. |
626 | * |
627 | * Good if you have links on same directory via several ways |
628 | * or you have selfreferences in directories (e.g. SuSE Linux 6.3: |
629 | * /etc/rc.d/init.d is linked to /etc/rc.d -> endless loop) |
630 | * |
631 | * This check is only needed for directories we work on for the |
632 | * first time (hence stackp->ff_filearray == NULL) |
633 | */ |
634 | if (stackp->ffs_filearray == NULL |
635 | && ff_check_visited(&search_ctx->ffsc_dir_visited_list |
636 | ->ffvl_visited_list, |
637 | stackp->ffs_fix_path |
638 | , stackp->ffs_wc_path |
639 | ) == FAIL) { |
640 | #ifdef FF_VERBOSE |
641 | if (p_verbose >= 5) { |
642 | verbose_enter_scroll(); |
643 | smsg("Already Searched: %s (%s)" , |
644 | stackp->ffs_fix_path, stackp->ffs_wc_path); |
645 | msg_puts("\n" ); // don't overwrite this either |
646 | verbose_leave_scroll(); |
647 | } |
648 | #endif |
649 | ff_free_stack_element(stackp); |
650 | continue; |
651 | } |
652 | #ifdef FF_VERBOSE |
653 | else if (p_verbose >= 5) { |
654 | verbose_enter_scroll(); |
655 | smsg("Searching: %s (%s)" , |
656 | stackp->ffs_fix_path, stackp->ffs_wc_path); |
657 | msg_puts("\n" ); // don't overwrite this either |
658 | verbose_leave_scroll(); |
659 | } |
660 | #endif |
661 | |
662 | /* check depth */ |
663 | if (stackp->ffs_level <= 0) { |
664 | ff_free_stack_element(stackp); |
665 | continue; |
666 | } |
667 | |
668 | file_path[0] = NUL; |
669 | |
670 | /* |
671 | * If no filearray till now expand wildcards |
672 | * The function expand_wildcards() can handle an array of paths |
673 | * and all possible expands are returned in one array. We use this |
674 | * to handle the expansion of '**' into an empty string. |
675 | */ |
676 | if (stackp->ffs_filearray == NULL) { |
677 | char_u *dirptrs[2]; |
678 | |
679 | /* we use filepath to build the path expand_wildcards() should |
680 | * expand. |
681 | */ |
682 | dirptrs[0] = file_path; |
683 | dirptrs[1] = NULL; |
684 | |
685 | // if we have a start dir copy it in |
686 | if (!vim_isAbsName(stackp->ffs_fix_path) |
687 | && search_ctx->ffsc_start_dir) { |
688 | if (STRLEN(search_ctx->ffsc_start_dir) + 1 >= MAXPATHL) { |
689 | ff_free_stack_element(stackp); |
690 | goto fail; |
691 | } |
692 | STRCPY(file_path, search_ctx->ffsc_start_dir); |
693 | if (!add_pathsep((char *)file_path)) { |
694 | ff_free_stack_element(stackp); |
695 | goto fail; |
696 | } |
697 | } |
698 | |
699 | // append the fix part of the search path |
700 | if (STRLEN(file_path) + STRLEN(stackp->ffs_fix_path) + 1 >= MAXPATHL) { |
701 | ff_free_stack_element(stackp); |
702 | goto fail; |
703 | } |
704 | STRCAT(file_path, stackp->ffs_fix_path); |
705 | if (!add_pathsep((char *)file_path)) { |
706 | ff_free_stack_element(stackp); |
707 | goto fail; |
708 | } |
709 | |
710 | rest_of_wildcards = stackp->ffs_wc_path; |
711 | if (*rest_of_wildcards != NUL) { |
712 | len = STRLEN(file_path); |
713 | if (STRNCMP(rest_of_wildcards, "**" , 2) == 0) { |
714 | // pointer to the restrict byte |
715 | // The restrict byte is not a character! |
716 | p = rest_of_wildcards + 2; |
717 | |
718 | if (*p > 0) { |
719 | (*p)--; |
720 | if (len + 1 >= MAXPATHL) { |
721 | ff_free_stack_element(stackp); |
722 | goto fail; |
723 | } |
724 | file_path[len++] = '*'; |
725 | } |
726 | |
727 | if (*p == 0) { |
728 | /* remove '**<numb> from wildcards */ |
729 | STRMOVE(rest_of_wildcards, rest_of_wildcards + 3); |
730 | } else |
731 | rest_of_wildcards += 3; |
732 | |
733 | if (stackp->ffs_star_star_empty == 0) { |
734 | /* if not done before, expand '**' to empty */ |
735 | stackp->ffs_star_star_empty = 1; |
736 | dirptrs[1] = stackp->ffs_fix_path; |
737 | } |
738 | } |
739 | |
740 | /* |
741 | * Here we copy until the next path separator or the end of |
742 | * the path. If we stop at a path separator, there is |
743 | * still something else left. This is handled below by |
744 | * pushing every directory returned from expand_wildcards() |
745 | * on the stack again for further search. |
746 | */ |
747 | while (*rest_of_wildcards |
748 | && !vim_ispathsep(*rest_of_wildcards)) { |
749 | if (len + 1 >= MAXPATHL) { |
750 | ff_free_stack_element(stackp); |
751 | goto fail; |
752 | } |
753 | file_path[len++] = *rest_of_wildcards++; |
754 | } |
755 | |
756 | file_path[len] = NUL; |
757 | if (vim_ispathsep(*rest_of_wildcards)) |
758 | rest_of_wildcards++; |
759 | } |
760 | |
761 | /* |
762 | * Expand wildcards like "*" and "$VAR". |
763 | * If the path is a URL don't try this. |
764 | */ |
765 | if (path_with_url((char *)dirptrs[0])) { |
766 | stackp->ffs_filearray = xmalloc(sizeof(char *)); |
767 | stackp->ffs_filearray[0] = vim_strsave(dirptrs[0]); |
768 | stackp->ffs_filearray_size = 1; |
769 | } else |
770 | /* Add EW_NOTWILD because the expanded path may contain |
771 | * wildcard characters that are to be taken literally. |
772 | * This is a bit of a hack. */ |
773 | expand_wildcards((dirptrs[1] == NULL) ? 1 : 2, dirptrs, |
774 | &stackp->ffs_filearray_size, |
775 | &stackp->ffs_filearray, |
776 | EW_DIR|EW_ADDSLASH|EW_SILENT|EW_NOTWILD); |
777 | |
778 | stackp->ffs_filearray_cur = 0; |
779 | stackp->ffs_stage = 0; |
780 | } else |
781 | rest_of_wildcards = &stackp->ffs_wc_path[ |
782 | STRLEN(stackp->ffs_wc_path)]; |
783 | |
784 | if (stackp->ffs_stage == 0) { |
785 | /* this is the first time we work on this directory */ |
786 | if (*rest_of_wildcards == NUL) { |
787 | /* |
788 | * We don't have further wildcards to expand, so we have to |
789 | * check for the final file now. |
790 | */ |
791 | for (int i = stackp->ffs_filearray_cur; |
792 | i < stackp->ffs_filearray_size; ++i) { |
793 | if (!path_with_url((char *)stackp->ffs_filearray[i]) |
794 | && !os_isdir(stackp->ffs_filearray[i])) |
795 | continue; /* not a directory */ |
796 | |
797 | // prepare the filename to be checked for existence below |
798 | if (STRLEN(stackp->ffs_filearray[i]) + 1 |
799 | + STRLEN(search_ctx->ffsc_file_to_search) >= MAXPATHL) { |
800 | ff_free_stack_element(stackp); |
801 | goto fail; |
802 | } |
803 | STRCPY(file_path, stackp->ffs_filearray[i]); |
804 | if (!add_pathsep((char *)file_path)) { |
805 | ff_free_stack_element(stackp); |
806 | goto fail; |
807 | } |
808 | STRCAT(file_path, search_ctx->ffsc_file_to_search); |
809 | |
810 | /* |
811 | * Try without extra suffix and then with suffixes |
812 | * from 'suffixesadd'. |
813 | */ |
814 | len = STRLEN(file_path); |
815 | if (search_ctx->ffsc_tagfile) |
816 | suf = (char_u *)"" ; |
817 | else |
818 | suf = curbuf->b_p_sua; |
819 | for (;; ) { |
820 | /* if file exists and we didn't already find it */ |
821 | if ((path_with_url((char *)file_path) |
822 | || (os_path_exists(file_path) |
823 | && (search_ctx->ffsc_find_what |
824 | == FINDFILE_BOTH |
825 | || ((search_ctx->ffsc_find_what |
826 | == FINDFILE_DIR) |
827 | == os_isdir(file_path))))) |
828 | #ifndef FF_VERBOSE |
829 | && (ff_check_visited( |
830 | &search_ctx->ffsc_visited_list->ffvl_visited_list, |
831 | file_path |
832 | , (char_u *)"" |
833 | ) == OK) |
834 | #endif |
835 | ) { |
836 | #ifdef FF_VERBOSE |
837 | if (ff_check_visited( |
838 | &search_ctx->ffsc_visited_list->ffvl_visited_list, |
839 | file_path |
840 | , (char_u *)"" |
841 | ) == FAIL) { |
842 | if (p_verbose >= 5) { |
843 | verbose_enter_scroll(); |
844 | smsg("Already: %s" , file_path); |
845 | msg_puts("\n" ); // don't overwrite this either |
846 | verbose_leave_scroll(); |
847 | } |
848 | continue; |
849 | } |
850 | #endif |
851 | |
852 | /* push dir to examine rest of subdirs later */ |
853 | assert(i < UCHAR_MAX - 1); |
854 | stackp->ffs_filearray_cur = (char_u)(i + 1); |
855 | ff_push(search_ctx, stackp); |
856 | |
857 | if (!path_with_url((char *)file_path)) |
858 | simplify_filename(file_path); |
859 | if (os_dirname(ff_expand_buffer, MAXPATHL) |
860 | == OK) { |
861 | p = path_shorten_fname(file_path, |
862 | ff_expand_buffer); |
863 | if (p != NULL) |
864 | STRMOVE(file_path, p); |
865 | } |
866 | #ifdef FF_VERBOSE |
867 | if (p_verbose >= 5) { |
868 | verbose_enter_scroll(); |
869 | smsg("HIT: %s" , file_path); |
870 | msg_puts("\n" ); // don't overwrite this either |
871 | verbose_leave_scroll(); |
872 | } |
873 | #endif |
874 | return file_path; |
875 | } |
876 | |
877 | /* Not found or found already, try next suffix. */ |
878 | if (*suf == NUL) |
879 | break; |
880 | assert(MAXPATHL >= len); |
881 | copy_option_part(&suf, file_path + len, |
882 | MAXPATHL - len, "," ); |
883 | } |
884 | } |
885 | } else { |
886 | /* |
887 | * still wildcards left, push the directories for further |
888 | * search |
889 | */ |
890 | for (int i = stackp->ffs_filearray_cur; |
891 | i < stackp->ffs_filearray_size; ++i) { |
892 | if (!os_isdir(stackp->ffs_filearray[i])) |
893 | continue; /* not a directory */ |
894 | |
895 | ff_push(search_ctx, |
896 | ff_create_stack_element( |
897 | stackp->ffs_filearray[i], |
898 | rest_of_wildcards, |
899 | stackp->ffs_level - 1, 0)); |
900 | } |
901 | } |
902 | stackp->ffs_filearray_cur = 0; |
903 | stackp->ffs_stage = 1; |
904 | } |
905 | |
906 | /* |
907 | * if wildcards contains '**' we have to descent till we reach the |
908 | * leaves of the directory tree. |
909 | */ |
910 | if (STRNCMP(stackp->ffs_wc_path, "**" , 2) == 0) { |
911 | for (int i = stackp->ffs_filearray_cur; |
912 | i < stackp->ffs_filearray_size; ++i) { |
913 | if (fnamecmp(stackp->ffs_filearray[i], |
914 | stackp->ffs_fix_path) == 0) |
915 | continue; /* don't repush same directory */ |
916 | if (!os_isdir(stackp->ffs_filearray[i])) |
917 | continue; /* not a directory */ |
918 | ff_push(search_ctx, |
919 | ff_create_stack_element(stackp->ffs_filearray[i], |
920 | stackp->ffs_wc_path, stackp->ffs_level - 1, 1)); |
921 | } |
922 | } |
923 | |
924 | /* we are done with the current directory */ |
925 | ff_free_stack_element(stackp); |
926 | |
927 | } |
928 | |
929 | /* If we reached this, we didn't find anything downwards. |
930 | * Let's check if we should do an upward search. |
931 | */ |
932 | if (search_ctx->ffsc_start_dir |
933 | && search_ctx->ffsc_stopdirs_v != NULL && !got_int) { |
934 | ff_stack_T *sptr; |
935 | |
936 | /* is the last starting directory in the stop list? */ |
937 | if (ff_path_in_stoplist(search_ctx->ffsc_start_dir, |
938 | (int)(path_end - search_ctx->ffsc_start_dir), |
939 | search_ctx->ffsc_stopdirs_v) == TRUE) |
940 | break; |
941 | |
942 | /* cut of last dir */ |
943 | while (path_end > search_ctx->ffsc_start_dir |
944 | && vim_ispathsep(*path_end)) |
945 | path_end--; |
946 | while (path_end > search_ctx->ffsc_start_dir |
947 | && !vim_ispathsep(path_end[-1])) |
948 | path_end--; |
949 | *path_end = 0; |
950 | path_end--; |
951 | |
952 | if (*search_ctx->ffsc_start_dir == 0) |
953 | break; |
954 | |
955 | if (STRLEN(search_ctx->ffsc_start_dir) + 1 |
956 | + STRLEN(search_ctx->ffsc_fix_path) >= MAXPATHL) { |
957 | goto fail; |
958 | } |
959 | STRCPY(file_path, search_ctx->ffsc_start_dir); |
960 | if (!add_pathsep((char *)file_path)) { |
961 | goto fail; |
962 | } |
963 | STRCAT(file_path, search_ctx->ffsc_fix_path); |
964 | |
965 | /* create a new stack entry */ |
966 | sptr = ff_create_stack_element(file_path, |
967 | search_ctx->ffsc_wc_path, search_ctx->ffsc_level, 0); |
968 | ff_push(search_ctx, sptr); |
969 | } else |
970 | break; |
971 | } |
972 | |
973 | fail: |
974 | xfree(file_path); |
975 | return NULL; |
976 | } |
977 | |
978 | /* |
979 | * Free the list of lists of visited files and directories |
980 | * Can handle it if the passed search_context is NULL; |
981 | */ |
982 | void vim_findfile_free_visited(void *search_ctx_arg) |
983 | { |
984 | ff_search_ctx_T *search_ctx; |
985 | |
986 | if (search_ctx_arg == NULL) |
987 | return; |
988 | |
989 | search_ctx = (ff_search_ctx_T *)search_ctx_arg; |
990 | vim_findfile_free_visited_list(&search_ctx->ffsc_visited_lists_list); |
991 | vim_findfile_free_visited_list(&search_ctx->ffsc_dir_visited_lists_list); |
992 | } |
993 | |
994 | static void vim_findfile_free_visited_list(ff_visited_list_hdr_T **list_headp) |
995 | { |
996 | ff_visited_list_hdr_T *vp; |
997 | |
998 | while (*list_headp != NULL) { |
999 | vp = (*list_headp)->ffvl_next; |
1000 | ff_free_visited_list((*list_headp)->ffvl_visited_list); |
1001 | |
1002 | xfree((*list_headp)->ffvl_filename); |
1003 | xfree(*list_headp); |
1004 | *list_headp = vp; |
1005 | } |
1006 | *list_headp = NULL; |
1007 | } |
1008 | |
1009 | static void ff_free_visited_list(ff_visited_T *vl) |
1010 | { |
1011 | ff_visited_T *vp; |
1012 | |
1013 | while (vl != NULL) { |
1014 | vp = vl->ffv_next; |
1015 | xfree(vl->ffv_wc_path); |
1016 | xfree(vl); |
1017 | vl = vp; |
1018 | } |
1019 | vl = NULL; |
1020 | } |
1021 | |
1022 | /* |
1023 | * Returns the already visited list for the given filename. If none is found it |
1024 | * allocates a new one. |
1025 | */ |
1026 | static ff_visited_list_hdr_T *ff_get_visited_list(char_u *filename, ff_visited_list_hdr_T **list_headp) |
1027 | { |
1028 | ff_visited_list_hdr_T *retptr = NULL; |
1029 | |
1030 | /* check if a visited list for the given filename exists */ |
1031 | if (*list_headp != NULL) { |
1032 | retptr = *list_headp; |
1033 | while (retptr != NULL) { |
1034 | if (fnamecmp(filename, retptr->ffvl_filename) == 0) { |
1035 | #ifdef FF_VERBOSE |
1036 | if (p_verbose >= 5) { |
1037 | verbose_enter_scroll(); |
1038 | smsg("ff_get_visited_list: FOUND list for %s" , filename); |
1039 | msg_puts("\n" ); // don't overwrite this either |
1040 | verbose_leave_scroll(); |
1041 | } |
1042 | #endif |
1043 | return retptr; |
1044 | } |
1045 | retptr = retptr->ffvl_next; |
1046 | } |
1047 | } |
1048 | |
1049 | #ifdef FF_VERBOSE |
1050 | if (p_verbose >= 5) { |
1051 | verbose_enter_scroll(); |
1052 | smsg("ff_get_visited_list: new list for %s" , filename); |
1053 | msg_puts("\n" ); // don't overwrite this either |
1054 | verbose_leave_scroll(); |
1055 | } |
1056 | #endif |
1057 | |
1058 | /* |
1059 | * if we reach this we didn't find a list and we have to allocate new list |
1060 | */ |
1061 | retptr = xmalloc(sizeof(*retptr)); |
1062 | |
1063 | retptr->ffvl_visited_list = NULL; |
1064 | retptr->ffvl_filename = vim_strsave(filename); |
1065 | retptr->ffvl_next = *list_headp; |
1066 | *list_headp = retptr; |
1067 | |
1068 | return retptr; |
1069 | } |
1070 | |
1071 | // Check if two wildcard paths are equal. |
1072 | // They are equal if: |
1073 | // - both paths are NULL |
1074 | // - they have the same length |
1075 | // - char by char comparison is OK |
1076 | // - the only differences are in the counters behind a '**', so |
1077 | // '**\20' is equal to '**\24' |
1078 | static bool ff_wc_equal(char_u *s1, char_u *s2) |
1079 | { |
1080 | int i, j; |
1081 | int c1 = NUL; |
1082 | int c2 = NUL; |
1083 | int prev1 = NUL; |
1084 | int prev2 = NUL; |
1085 | |
1086 | if (s1 == s2) { |
1087 | return true; |
1088 | } |
1089 | |
1090 | if (s1 == NULL || s2 == NULL) { |
1091 | return false; |
1092 | } |
1093 | |
1094 | for (i = 0, j = 0; s1[i] != NUL && s2[j] != NUL;) { |
1095 | c1 = PTR2CHAR(s1 + i); |
1096 | c2 = PTR2CHAR(s2 + j); |
1097 | |
1098 | if ((p_fic ? mb_tolower(c1) != mb_tolower(c2) : c1 != c2) |
1099 | && (prev1 != '*' || prev2 != '*')) { |
1100 | return false; |
1101 | } |
1102 | prev2 = prev1; |
1103 | prev1 = c1; |
1104 | |
1105 | i += MB_PTR2LEN(s1 + i); |
1106 | j += MB_PTR2LEN(s2 + j); |
1107 | } |
1108 | return s1[i] == s2[j]; |
1109 | } |
1110 | |
1111 | /* |
1112 | * maintains the list of already visited files and dirs |
1113 | * returns FAIL if the given file/dir is already in the list |
1114 | * returns OK if it is newly added |
1115 | */ |
1116 | static int ff_check_visited(ff_visited_T **visited_list, char_u *fname, char_u *wc_path) |
1117 | { |
1118 | ff_visited_T *vp; |
1119 | bool url = false; |
1120 | |
1121 | FileID file_id; |
1122 | // For an URL we only compare the name, otherwise we compare the |
1123 | // device/inode. |
1124 | if (path_with_url((char *)fname)) { |
1125 | STRLCPY(ff_expand_buffer, fname, MAXPATHL); |
1126 | url = true; |
1127 | } else { |
1128 | ff_expand_buffer[0] = NUL; |
1129 | if (!os_fileid((char *)fname, &file_id)) { |
1130 | return FAIL; |
1131 | } |
1132 | } |
1133 | |
1134 | /* check against list of already visited files */ |
1135 | for (vp = *visited_list; vp != NULL; vp = vp->ffv_next) { |
1136 | if ((url && fnamecmp(vp->ffv_fname, ff_expand_buffer) == 0) |
1137 | || (!url && vp->file_id_valid |
1138 | && os_fileid_equal(&(vp->file_id), &file_id))) { |
1139 | // are the wildcard parts equal |
1140 | if (ff_wc_equal(vp->ffv_wc_path, wc_path)) { |
1141 | // already visited |
1142 | return FAIL; |
1143 | } |
1144 | } |
1145 | } |
1146 | |
1147 | /* |
1148 | * New file/dir. Add it to the list of visited files/dirs. |
1149 | */ |
1150 | vp = xmalloc(sizeof(ff_visited_T) + STRLEN(ff_expand_buffer)); |
1151 | |
1152 | if (!url) { |
1153 | vp->file_id_valid = true; |
1154 | vp->file_id = file_id; |
1155 | vp->ffv_fname[0] = NUL; |
1156 | } else { |
1157 | vp->file_id_valid = false; |
1158 | STRCPY(vp->ffv_fname, ff_expand_buffer); |
1159 | } |
1160 | |
1161 | if (wc_path != NULL) |
1162 | vp->ffv_wc_path = vim_strsave(wc_path); |
1163 | else |
1164 | vp->ffv_wc_path = NULL; |
1165 | |
1166 | vp->ffv_next = *visited_list; |
1167 | *visited_list = vp; |
1168 | |
1169 | return OK; |
1170 | } |
1171 | |
1172 | /* |
1173 | * create stack element from given path pieces |
1174 | */ |
1175 | static ff_stack_T *ff_create_stack_element(char_u *fix_part, char_u *wc_part, int level, int star_star_empty) |
1176 | { |
1177 | ff_stack_T *new = xmalloc(sizeof(ff_stack_T)); |
1178 | |
1179 | new->ffs_prev = NULL; |
1180 | new->ffs_filearray = NULL; |
1181 | new->ffs_filearray_size = 0; |
1182 | new->ffs_filearray_cur = 0; |
1183 | new->ffs_stage = 0; |
1184 | new->ffs_level = level; |
1185 | new->ffs_star_star_empty = star_star_empty; |
1186 | |
1187 | /* the following saves NULL pointer checks in vim_findfile */ |
1188 | if (fix_part == NULL) |
1189 | fix_part = (char_u *)"" ; |
1190 | new->ffs_fix_path = vim_strsave(fix_part); |
1191 | |
1192 | if (wc_part == NULL) |
1193 | wc_part = (char_u *)"" ; |
1194 | new->ffs_wc_path = vim_strsave(wc_part); |
1195 | |
1196 | return new; |
1197 | } |
1198 | |
1199 | /* |
1200 | * Push a dir on the directory stack. |
1201 | */ |
1202 | static void ff_push(ff_search_ctx_T *search_ctx, ff_stack_T *stack_ptr) |
1203 | { |
1204 | /* check for NULL pointer, not to return an error to the user, but |
1205 | * to prevent a crash */ |
1206 | if (stack_ptr != NULL) { |
1207 | stack_ptr->ffs_prev = search_ctx->ffsc_stack_ptr; |
1208 | search_ctx->ffsc_stack_ptr = stack_ptr; |
1209 | } |
1210 | } |
1211 | |
1212 | /* |
1213 | * Pop a dir from the directory stack. |
1214 | * Returns NULL if stack is empty. |
1215 | */ |
1216 | static ff_stack_T *ff_pop(ff_search_ctx_T *search_ctx) |
1217 | { |
1218 | ff_stack_T *sptr; |
1219 | |
1220 | sptr = search_ctx->ffsc_stack_ptr; |
1221 | if (search_ctx->ffsc_stack_ptr != NULL) |
1222 | search_ctx->ffsc_stack_ptr = search_ctx->ffsc_stack_ptr->ffs_prev; |
1223 | |
1224 | return sptr; |
1225 | } |
1226 | |
1227 | /* |
1228 | * free the given stack element |
1229 | */ |
1230 | static void ff_free_stack_element(ff_stack_T *const stack_ptr) |
1231 | { |
1232 | if (stack_ptr == NULL) { |
1233 | return; |
1234 | } |
1235 | |
1236 | // free handles possible NULL pointers |
1237 | xfree(stack_ptr->ffs_fix_path); |
1238 | xfree(stack_ptr->ffs_wc_path); |
1239 | |
1240 | if (stack_ptr->ffs_filearray != NULL) { |
1241 | FreeWild(stack_ptr->ffs_filearray_size, stack_ptr->ffs_filearray); |
1242 | } |
1243 | |
1244 | xfree(stack_ptr); |
1245 | } |
1246 | |
1247 | /* |
1248 | * Clear the search context, but NOT the visited list. |
1249 | */ |
1250 | static void ff_clear(ff_search_ctx_T *search_ctx) |
1251 | { |
1252 | ff_stack_T *sptr; |
1253 | |
1254 | /* clear up stack */ |
1255 | while ((sptr = ff_pop(search_ctx)) != NULL) |
1256 | ff_free_stack_element(sptr); |
1257 | |
1258 | xfree(search_ctx->ffsc_file_to_search); |
1259 | xfree(search_ctx->ffsc_start_dir); |
1260 | xfree(search_ctx->ffsc_fix_path); |
1261 | xfree(search_ctx->ffsc_wc_path); |
1262 | |
1263 | if (search_ctx->ffsc_stopdirs_v != NULL) { |
1264 | int i = 0; |
1265 | |
1266 | while (search_ctx->ffsc_stopdirs_v[i] != NULL) { |
1267 | xfree(search_ctx->ffsc_stopdirs_v[i]); |
1268 | i++; |
1269 | } |
1270 | xfree(search_ctx->ffsc_stopdirs_v); |
1271 | } |
1272 | search_ctx->ffsc_stopdirs_v = NULL; |
1273 | |
1274 | /* reset everything */ |
1275 | search_ctx->ffsc_file_to_search = NULL; |
1276 | search_ctx->ffsc_start_dir = NULL; |
1277 | search_ctx->ffsc_fix_path = NULL; |
1278 | search_ctx->ffsc_wc_path = NULL; |
1279 | search_ctx->ffsc_level = 0; |
1280 | } |
1281 | |
1282 | /* |
1283 | * check if the given path is in the stopdirs |
1284 | * returns TRUE if yes else FALSE |
1285 | */ |
1286 | static int ff_path_in_stoplist(char_u *path, int path_len, char_u **stopdirs_v) |
1287 | { |
1288 | int i = 0; |
1289 | |
1290 | /* eat up trailing path separators, except the first */ |
1291 | while (path_len > 1 && vim_ispathsep(path[path_len - 1])) |
1292 | path_len--; |
1293 | |
1294 | /* if no path consider it as match */ |
1295 | if (path_len == 0) |
1296 | return TRUE; |
1297 | |
1298 | for (i = 0; stopdirs_v[i] != NULL; i++) { |
1299 | if ((int)STRLEN(stopdirs_v[i]) > path_len) { |
1300 | /* match for parent directory. So '/home' also matches |
1301 | * '/home/rks'. Check for PATHSEP in stopdirs_v[i], else |
1302 | * '/home/r' would also match '/home/rks' |
1303 | */ |
1304 | if (fnamencmp(stopdirs_v[i], path, path_len) == 0 |
1305 | && vim_ispathsep(stopdirs_v[i][path_len])) |
1306 | return TRUE; |
1307 | } else { |
1308 | if (fnamecmp(stopdirs_v[i], path) == 0) |
1309 | return TRUE; |
1310 | } |
1311 | } |
1312 | return FALSE; |
1313 | } |
1314 | |
1315 | /* |
1316 | * Find the file name "ptr[len]" in the path. Also finds directory names. |
1317 | * |
1318 | * On the first call set the parameter 'first' to TRUE to initialize |
1319 | * the search. For repeating calls to FALSE. |
1320 | * |
1321 | * Repeating calls will return other files called 'ptr[len]' from the path. |
1322 | * |
1323 | * Only on the first call 'ptr' and 'len' are used. For repeating calls they |
1324 | * don't need valid values. |
1325 | * |
1326 | * If nothing found on the first call the option FNAME_MESS will issue the |
1327 | * message: |
1328 | * 'Can't find file "<file>" in path' |
1329 | * On repeating calls: |
1330 | * 'No more file "<file>" found in path' |
1331 | * |
1332 | * options: |
1333 | * FNAME_MESS give error message when not found |
1334 | * |
1335 | * Uses NameBuff[]! |
1336 | * |
1337 | * Returns an allocated string for the file name. NULL for error. |
1338 | * |
1339 | */ |
1340 | char_u * |
1341 | find_file_in_path ( |
1342 | char_u *ptr, /* file name */ |
1343 | size_t len, /* length of file name */ |
1344 | int options, |
1345 | int first, /* use count'th matching file name */ |
1346 | char_u *rel_fname /* file name searching relative to */ |
1347 | ) |
1348 | { |
1349 | return find_file_in_path_option(ptr, len, options, first, |
1350 | (*curbuf->b_p_path == NUL |
1351 | ? p_path |
1352 | : curbuf->b_p_path), |
1353 | FINDFILE_BOTH, rel_fname, curbuf->b_p_sua); |
1354 | } |
1355 | |
1356 | static char_u *ff_file_to_find = NULL; |
1357 | static void *fdip_search_ctx = NULL; |
1358 | |
1359 | #if defined(EXITFREE) |
1360 | void free_findfile(void) |
1361 | { |
1362 | xfree(ff_file_to_find); |
1363 | vim_findfile_cleanup(fdip_search_ctx); |
1364 | xfree(ff_expand_buffer); |
1365 | } |
1366 | |
1367 | #endif |
1368 | |
1369 | /* |
1370 | * Find the directory name "ptr[len]" in the path. |
1371 | * |
1372 | * options: |
1373 | * FNAME_MESS give error message when not found |
1374 | * FNAME_UNESC unescape backslashes |
1375 | * |
1376 | * Uses NameBuff[]! |
1377 | * |
1378 | * Returns an allocated string for the file name. NULL for error. |
1379 | */ |
1380 | char_u * |
1381 | find_directory_in_path ( |
1382 | char_u *ptr, /* file name */ |
1383 | size_t len, /* length of file name */ |
1384 | int options, |
1385 | char_u *rel_fname /* file name searching relative to */ |
1386 | ) |
1387 | { |
1388 | return find_file_in_path_option(ptr, len, options, TRUE, p_cdpath, |
1389 | FINDFILE_DIR, rel_fname, (char_u *)"" ); |
1390 | } |
1391 | |
1392 | char_u * |
1393 | find_file_in_path_option ( |
1394 | char_u *ptr, /* file name */ |
1395 | size_t len, /* length of file name */ |
1396 | int options, |
1397 | int first, /* use count'th matching file name */ |
1398 | char_u *path_option, /* p_path or p_cdpath */ |
1399 | int find_what, /* FINDFILE_FILE, _DIR or _BOTH */ |
1400 | char_u *rel_fname, /* file name we are looking relative to. */ |
1401 | char_u *suffixes /* list of suffixes, 'suffixesadd' option */ |
1402 | ) |
1403 | { |
1404 | static char_u *dir; |
1405 | static int did_findfile_init = FALSE; |
1406 | char_u save_char; |
1407 | char_u *file_name = NULL; |
1408 | char_u *buf = NULL; |
1409 | int rel_to_curdir; |
1410 | |
1411 | if (rel_fname != NULL && path_with_url((const char *)rel_fname)) { |
1412 | // Do not attempt to search "relative" to a URL. #6009 |
1413 | rel_fname = NULL; |
1414 | } |
1415 | |
1416 | if (first == TRUE) { |
1417 | /* copy file name into NameBuff, expanding environment variables */ |
1418 | save_char = ptr[len]; |
1419 | ptr[len] = NUL; |
1420 | expand_env_esc(ptr, NameBuff, MAXPATHL, false, true, NULL); |
1421 | ptr[len] = save_char; |
1422 | |
1423 | xfree(ff_file_to_find); |
1424 | ff_file_to_find = vim_strsave(NameBuff); |
1425 | if (options & FNAME_UNESC) { |
1426 | // Change all "\ " to " ". |
1427 | for (ptr = ff_file_to_find; *ptr != NUL; ++ptr) { |
1428 | if (ptr[0] == '\\' && ptr[1] == ' ') { |
1429 | memmove(ptr, ptr + 1, STRLEN(ptr)); |
1430 | } |
1431 | } |
1432 | } |
1433 | } |
1434 | |
1435 | rel_to_curdir = (ff_file_to_find[0] == '.' |
1436 | && (ff_file_to_find[1] == NUL |
1437 | || vim_ispathsep(ff_file_to_find[1]) |
1438 | || (ff_file_to_find[1] == '.' |
1439 | && (ff_file_to_find[2] == NUL |
1440 | || vim_ispathsep(ff_file_to_find[2]))))); |
1441 | if (vim_isAbsName(ff_file_to_find) |
1442 | // "..", "../path", "." and "./path": don't use the path_option |
1443 | || rel_to_curdir |
1444 | #if defined(WIN32) |
1445 | // handle "\tmp" as absolute path |
1446 | || vim_ispathsep(ff_file_to_find[0]) |
1447 | // handle "c:name" as absolute path |
1448 | || (ff_file_to_find[0] != NUL && ff_file_to_find[1] == ':') |
1449 | #endif |
1450 | ) { |
1451 | /* |
1452 | * Absolute path, no need to use "path_option". |
1453 | * If this is not a first call, return NULL. We already returned a |
1454 | * filename on the first call. |
1455 | */ |
1456 | if (first == TRUE) { |
1457 | if (path_with_url((char *)ff_file_to_find)) { |
1458 | file_name = vim_strsave(ff_file_to_find); |
1459 | goto theend; |
1460 | } |
1461 | |
1462 | /* When FNAME_REL flag given first use the directory of the file. |
1463 | * Otherwise or when this fails use the current directory. */ |
1464 | for (int run = 1; run <= 2; ++run) { |
1465 | size_t l = STRLEN(ff_file_to_find); |
1466 | if (run == 1 |
1467 | && rel_to_curdir |
1468 | && (options & FNAME_REL) |
1469 | && rel_fname != NULL |
1470 | && STRLEN(rel_fname) + l < MAXPATHL) { |
1471 | STRCPY(NameBuff, rel_fname); |
1472 | STRCPY(path_tail(NameBuff), ff_file_to_find); |
1473 | l = STRLEN(NameBuff); |
1474 | } else { |
1475 | STRCPY(NameBuff, ff_file_to_find); |
1476 | run = 2; |
1477 | } |
1478 | |
1479 | /* When the file doesn't exist, try adding parts of |
1480 | * 'suffixesadd'. */ |
1481 | buf = suffixes; |
1482 | for (;; ) { |
1483 | if ( |
1484 | (os_path_exists(NameBuff) |
1485 | && (find_what == FINDFILE_BOTH |
1486 | || ((find_what == FINDFILE_DIR) |
1487 | == os_isdir(NameBuff))))) { |
1488 | file_name = vim_strsave(NameBuff); |
1489 | goto theend; |
1490 | } |
1491 | if (*buf == NUL) |
1492 | break; |
1493 | assert(MAXPATHL >= l); |
1494 | copy_option_part(&buf, NameBuff + l, MAXPATHL - l, "," ); |
1495 | } |
1496 | } |
1497 | } |
1498 | } else { |
1499 | /* |
1500 | * Loop over all paths in the 'path' or 'cdpath' option. |
1501 | * When "first" is set, first setup to the start of the option. |
1502 | * Otherwise continue to find the next match. |
1503 | */ |
1504 | if (first == TRUE) { |
1505 | /* vim_findfile_free_visited can handle a possible NULL pointer */ |
1506 | vim_findfile_free_visited(fdip_search_ctx); |
1507 | dir = path_option; |
1508 | did_findfile_init = FALSE; |
1509 | } |
1510 | |
1511 | for (;; ) { |
1512 | if (did_findfile_init) { |
1513 | file_name = vim_findfile(fdip_search_ctx); |
1514 | if (file_name != NULL) |
1515 | break; |
1516 | |
1517 | did_findfile_init = FALSE; |
1518 | } else { |
1519 | char_u *r_ptr; |
1520 | |
1521 | if (dir == NULL || *dir == NUL) { |
1522 | /* We searched all paths of the option, now we can |
1523 | * free the search context. */ |
1524 | vim_findfile_cleanup(fdip_search_ctx); |
1525 | fdip_search_ctx = NULL; |
1526 | break; |
1527 | } |
1528 | |
1529 | buf = xmalloc(MAXPATHL); |
1530 | |
1531 | /* copy next path */ |
1532 | buf[0] = 0; |
1533 | copy_option_part(&dir, buf, MAXPATHL, " ," ); |
1534 | |
1535 | /* get the stopdir string */ |
1536 | r_ptr = vim_findfile_stopdir(buf); |
1537 | fdip_search_ctx = vim_findfile_init(buf, ff_file_to_find, |
1538 | r_ptr, 100, FALSE, find_what, |
1539 | fdip_search_ctx, FALSE, rel_fname); |
1540 | if (fdip_search_ctx != NULL) |
1541 | did_findfile_init = TRUE; |
1542 | xfree(buf); |
1543 | } |
1544 | } |
1545 | } |
1546 | if (file_name == NULL && (options & FNAME_MESS)) { |
1547 | if (first == TRUE) { |
1548 | if (find_what == FINDFILE_DIR) |
1549 | EMSG2(_("E344: Can't find directory \"%s\" in cdpath" ), |
1550 | ff_file_to_find); |
1551 | else |
1552 | EMSG2(_("E345: Can't find file \"%s\" in path" ), |
1553 | ff_file_to_find); |
1554 | } else { |
1555 | if (find_what == FINDFILE_DIR) |
1556 | EMSG2(_("E346: No more directory \"%s\" found in cdpath" ), |
1557 | ff_file_to_find); |
1558 | else |
1559 | EMSG2(_("E347: No more file \"%s\" found in path" ), |
1560 | ff_file_to_find); |
1561 | } |
1562 | } |
1563 | |
1564 | theend: |
1565 | return file_name; |
1566 | } |
1567 | |
1568 | void do_autocmd_dirchanged(char *new_dir, CdScope scope) |
1569 | { |
1570 | static bool recursive = false; |
1571 | |
1572 | if (recursive || !has_event(EVENT_DIRCHANGED)) { |
1573 | // No autocommand was defined or we changed |
1574 | // the directory from this autocommand. |
1575 | return; |
1576 | } |
1577 | |
1578 | recursive = true; |
1579 | |
1580 | dict_T *dict = get_vim_var_dict(VV_EVENT); |
1581 | char buf[8]; |
1582 | |
1583 | switch (scope) { |
1584 | case kCdScopeGlobal: { |
1585 | snprintf(buf, sizeof(buf), "global" ); |
1586 | break; |
1587 | } |
1588 | case kCdScopeTab: { |
1589 | snprintf(buf, sizeof(buf), "tab" ); |
1590 | break; |
1591 | } |
1592 | case kCdScopeWindow: { |
1593 | snprintf(buf, sizeof(buf), "window" ); |
1594 | break; |
1595 | } |
1596 | case kCdScopeInvalid: { |
1597 | // Should never happen. |
1598 | assert(false); |
1599 | } |
1600 | } |
1601 | |
1602 | tv_dict_add_str(dict, S_LEN("scope" ), buf); // -V614 |
1603 | tv_dict_add_str(dict, S_LEN("cwd" ), new_dir); |
1604 | tv_dict_set_keys_readonly(dict); |
1605 | |
1606 | apply_autocmds(EVENT_DIRCHANGED, (char_u *)buf, (char_u *)new_dir, false, |
1607 | curbuf); |
1608 | |
1609 | tv_dict_clear(dict); |
1610 | |
1611 | recursive = false; |
1612 | } |
1613 | |
1614 | /// Change to a file's directory. |
1615 | /// Caller must call shorten_fnames()! |
1616 | /// @return OK or FAIL |
1617 | int vim_chdirfile(char_u *fname) |
1618 | { |
1619 | char dir[MAXPATHL]; |
1620 | |
1621 | STRLCPY(dir, fname, MAXPATHL); |
1622 | *path_tail_with_sep((char_u *)dir) = NUL; |
1623 | |
1624 | if (os_dirname(NameBuff, sizeof(NameBuff)) != OK) { |
1625 | NameBuff[0] = NUL; |
1626 | } |
1627 | |
1628 | if (os_chdir(dir) != 0) { |
1629 | return FAIL; |
1630 | } |
1631 | |
1632 | #ifdef BACKSLASH_IN_FILENAME |
1633 | slash_adjust((char_u *)dir); |
1634 | #endif |
1635 | if (!strequal(dir, (char *)NameBuff)) { |
1636 | do_autocmd_dirchanged(dir, kCdScopeWindow); |
1637 | } |
1638 | |
1639 | return OK; |
1640 | } |
1641 | |
1642 | /// Change directory to "new_dir". Search 'cdpath' for relative directory names. |
1643 | int vim_chdir(char_u *new_dir) |
1644 | { |
1645 | char_u *dir_name = find_directory_in_path(new_dir, STRLEN(new_dir), |
1646 | FNAME_MESS, curbuf->b_ffname); |
1647 | if (dir_name == NULL) { |
1648 | return -1; |
1649 | } |
1650 | |
1651 | int r = os_chdir((char *)dir_name); |
1652 | xfree(dir_name); |
1653 | return r; |
1654 | } |
1655 | |
1656 | |