// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

// File searching functions for 'path', 'tags' and 'cdpath' options.
//
// External visible functions:
//   vim_findfile_init()          creates/initialises the search context
//   vim_findfile_free_visited()  free list of visited files/dirs of search
//                                context
//   vim_findfile()               find a file in the search context
//   vim_findfile_cleanup()       cleanup/free search context created by
//                                vim_findfile_init()
//
// All static functions and variables start with 'ff_'
//
// In general it works like this:
// First you create yourself a search context by calling vim_findfile_init().
// It is possible to give a search context from a previous call to
// vim_findfile_init(), so it can be reused. After this you call vim_findfile()
// until you are satisfied with the result or it returns NULL. On every call it
// returns the next file which matches the conditions given to
// vim_findfile_init(). If it doesn't find a next file it returns NULL.
//
// It is possible to call vim_findfile_init() again to reinitialise your search
// with some new parameters. Don't forget to pass your old search context to
// it, so it can reuse it and especially reuse the list of already visited
// directories. If you want to delete the list of already visited directories
// simply call vim_findfile_free_visited().
//
// When you are done call vim_findfile_cleanup() to free the search context.
//
// The function vim_findfile_init() has a long comment, which describes the
// needed parameters.
//
//
//
// ATTENTION:
// ==========
// We use an allocated search context, these functions are NOT thread-safe!!!!!
//
// To minimize parameter passing (or because I'm too lazy), only the
// external visible functions get a search context as a parameter. This is
// then assigned to a static global, which is used throughout the local
// functions.

#include <assert.h>
#include <inttypes.h>
#include <limits.h>
#include <stdbool.h>
#include <string.h>

#include "nvim/ascii.h"
#include "nvim/charset.h"
#include "nvim/eval.h"
#include "nvim/file_search.h"
#include "nvim/fileio.h"
#include "nvim/globals.h"
#include "nvim/memory.h"
#include "nvim/message.h"
#include "nvim/option.h"
#include "nvim/os/fs_defs.h"
#include "nvim/os/input.h"
#include "nvim/os/os.h"
#include "nvim/os_unix.h"
#include "nvim/path.h"
#include "nvim/strings.h"
#include "nvim/tag.h"
#include "nvim/vim.h"
#include "nvim/window.h"

static char_u *ff_expand_buffer = NULL;  // used for expanding filenames

/*
 * type for the directory search stack
 */
typedef struct ff_stack {
  struct ff_stack *ffs_prev;

  /* the fix part (no wildcards) and the part containing the wildcards
   * of the search path
   */
  char_u *ffs_fix_path;
  char_u *ffs_wc_path;

  /* files/dirs found in the above directory, matched by the first wildcard
   * of wc_part
   */
  char_u **ffs_filearray;
  int ffs_filearray_size;
  char_u ffs_filearray_cur;                  // needed for partly handled dirs

  /* to store status of partly handled directories
   * 0: we work on this directory for the first time
   * 1: this directory was partly searched in an earlier step
   */
  int ffs_stage;

  /* How deep are we in the directory tree?
   * Counts backward from value of level parameter to vim_findfile_init
   */
  int ffs_level;

  // Did we already expand '**' to an empty string?
  int ffs_star_star_empty;
} ff_stack_T;

/*
 * type for already visited directories or files.
 */
typedef struct ff_visited {
  struct ff_visited *ffv_next;

  /* Visited directories are different if the wildcard string are
   * different. So we have to save it.
   */
  char_u *ffv_wc_path;
  // use FileID for comparison (needed because of links), else use filename.
  bool file_id_valid;
  FileID file_id;
  /* The memory for this struct is allocated according to the length of
   * ffv_fname.
   */
  char_u ffv_fname[1];                  // actually longer
} ff_visited_T;

/*
 * We might have to manage several visited lists during a search.
 * This is especially needed for the tags option. If tags is set to:
 *      "./++/tags,./++/TAGS,++/tags"  (replace + with *)
 * So we have to do 3 searches:
 *   1) search from the current files directory downward for the file "tags"
 *   2) search from the current files directory downward for the file "TAGS"
 *   3) search from Vims current directory downwards for the file "tags"
 * As you can see, the first and the third search are for the same file, so for
 * the third search we can use the visited list of the first search. For the
 * second search we must start from an empty visited list.
 * The struct ff_visited_list_hdr is used to manage a linked list of already
 * visited lists.
 */
typedef struct ff_visited_list_hdr {
  struct ff_visited_list_hdr *ffvl_next;

  // the filename the attached visited list is for
  char_u *ffvl_filename;

  ff_visited_T *ffvl_visited_list;
} ff_visited_list_hdr_T;


/*
 * '**' can be expanded to several directory levels.
 * Set the default maximum depth.
 */
#define FF_MAX_STAR_STAR_EXPAND ((char_u)30)

/*
 * The search context:
 *   ffsc_stack_ptr:    the stack for the dirs to search
 *   ffsc_visited_list: the currently active visited list
 *   ffsc_dir_visited_list: the currently active visited list for search dirs
 *   ffsc_visited_lists_list: the list of all visited lists
 *   ffsc_dir_visited_lists_list: the list of all visited lists for search dirs
 *   ffsc_file_to_search:     the file to search for
 *   ffsc_start_dir:    the starting directory, if search path was relative
 *   ffsc_fix_path:     the fix part of the given path (without wildcards)
 *                      Needed for upward search.
 *   ffsc_wc_path:      the part of the given path containing wildcards
 *   ffsc_level:        how many levels of dirs to search downwards
 *   ffsc_stopdirs_v:   array of stop directories for upward search
 *   ffsc_find_what:    FINDFILE_BOTH, FINDFILE_DIR or FINDFILE_FILE
 *   ffsc_tagfile:      searching for tags file, don't use 'suffixesadd'
 */
typedef struct ff_search_ctx_T {
  ff_stack_T *ffsc_stack_ptr;
  ff_visited_list_hdr_T *ffsc_visited_list;
  ff_visited_list_hdr_T *ffsc_dir_visited_list;
  ff_visited_list_hdr_T *ffsc_visited_lists_list;
  ff_visited_list_hdr_T *ffsc_dir_visited_lists_list;
  char_u *ffsc_file_to_search;
  char_u *ffsc_start_dir;
  char_u *ffsc_fix_path;
  char_u *ffsc_wc_path;
  int ffsc_level;
  char_u **ffsc_stopdirs_v;
  int ffsc_find_what;
  int ffsc_tagfile;
} ff_search_ctx_T;

// locally needed functions

#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "file_search.c.generated.h"
#endif

static char_u e_pathtoolong[] = N_("E854: path too long for completion");

/// Initialization routine for vim_findfile().
///
/// Returns the newly allocated search context or NULL if an error occurred.
///
/// Don't forget to clean up by calling vim_findfile_cleanup() if you are done
/// with the search context.
///
/// Find the file 'filename' in the directory 'path'.
/// The parameter 'path' may contain wildcards. If so only search 'level'
/// directories deep. The parameter 'level' is the absolute maximum and is
/// not related to restricts given to the '**' wildcard. If 'level' is 100
/// and you use '**200' vim_findfile() will stop after 100 levels.
///
/// 'filename' cannot contain wildcards!  It is used as-is, no backslashes to
/// escape special characters.
///
/// If 'stopdirs' is not NULL and nothing is found downward, the search is
/// restarted on the next higher directory level. This is repeated until the
/// start-directory of a search is contained in 'stopdirs'. 'stopdirs' has the
/// format ";*<dirname>*\(;<dirname>\)*;\=$".
///
/// If the 'path' is relative, the starting dir for the search is either VIM's
/// current dir or if the path starts with "./" the current files dir.
/// If the 'path' is absolute, the starting dir is that part of the path before
/// the first wildcard.
///
/// Upward search is only done on the starting dir.
///
/// If 'free_visited' is TRUE the list of already visited files/directories is
/// cleared. Set this to FALSE if you just want to search from another
/// directory, but want to be sure that no directory from a previous search is
/// searched again. This is useful if you search for a file at different places.
/// The list of visited files/dirs can also be cleared with the function
/// vim_findfile_free_visited().
///
/// Set the parameter 'find_what' to FINDFILE_DIR if you want to search for
/// directories only, FINDFILE_FILE for files only, FINDFILE_BOTH for both.
///
/// A search context returned by a previous call to vim_findfile_init() can be
/// passed in the parameter "search_ctx_arg".  This context is reused and
/// reinitialized with the new parameters.  The list of already visited
/// directories from this context is only deleted if the parameter
/// "free_visited" is true.  Be aware that the passed "search_ctx_arg" is freed
/// if the reinitialization fails.
///
/// If you don't have a search context from a previous call "search_ctx_arg"
/// must be NULL.
///
/// This function silently ignores a few errors, vim_findfile() will have
/// limited functionality then.
///
/// @param tagfile  expanding names of tags files
/// @param rel_fname  file name to use for "."
void *vim_findfile_init(char_u *path, char_u *filename, char_u *stopdirs, int level,
                        int free_visited, int find_what, void *search_ctx_arg, int tagfile,
                        char_u *rel_fname)
{
  char_u *wc_part;
  ff_stack_T *sptr;
  ff_search_ctx_T *search_ctx;

  /* If a search context is given by the caller, reuse it, else allocate a
   * new one.
   */
  if (search_ctx_arg != NULL) {
    search_ctx = search_ctx_arg;
  } else {
    search_ctx = xcalloc(1, sizeof(ff_search_ctx_T));
  }
  search_ctx->ffsc_find_what = find_what;
  search_ctx->ffsc_tagfile = tagfile;

  // clear the search context, but NOT the visited lists
  ff_clear(search_ctx);

  // clear visited list if wanted
  if (free_visited == TRUE) {
    vim_findfile_free_visited(search_ctx);
  } else {
    /* Reuse old visited lists. Get the visited list for the given
     * filename. If no list for the current filename exists, creates a new
     * one. */
    search_ctx->ffsc_visited_list = ff_get_visited_list(filename,
                                                        &search_ctx->ffsc_visited_lists_list);
    if (search_ctx->ffsc_visited_list == NULL) {
      goto error_return;
    }
    search_ctx->ffsc_dir_visited_list = ff_get_visited_list(filename,
                                                            &search_ctx->ffsc_dir_visited_lists_list);
    if (search_ctx->ffsc_dir_visited_list == NULL) {
      goto error_return;
    }
  }

  if (ff_expand_buffer == NULL) {
    ff_expand_buffer = xmalloc(MAXPATHL);
  }

  /* Store information on starting dir now if path is relative.
   * If path is absolute, we do that later.  */
  if (path[0] == '.'
      && (vim_ispathsep(path[1]) || path[1] == NUL)
      && (!tagfile || vim_strchr(p_cpo, CPO_DOTTAG) == NULL)
      && rel_fname != NULL) {
    size_t len = (size_t)(path_tail(rel_fname) - rel_fname);

    if (!vim_isAbsName(rel_fname) && len + 1 < MAXPATHL) {
      // Make the start dir an absolute path name.
      STRLCPY(ff_expand_buffer, rel_fname, len + 1);
      search_ctx->ffsc_start_dir = (char_u *)FullName_save((char *)ff_expand_buffer, FALSE);
    } else {
      search_ctx->ffsc_start_dir = vim_strnsave(rel_fname, len);
    }
    if (*++path != NUL) {
      ++path;
    }
  } else if (*path == NUL || !vim_isAbsName(path)) {
#ifdef BACKSLASH_IN_FILENAME
    // "c:dir" needs "c:" to be expanded, otherwise use current dir
    if (*path != NUL && path[1] == ':') {
      char_u drive[3];

      drive[0] = path[0];
      drive[1] = ':';
      drive[2] = NUL;
      if (vim_FullName((const char *)drive, (char *)ff_expand_buffer, MAXPATHL,
                       true)
          == FAIL) {
        goto error_return;
      }
      path += 2;
    } else
#endif
    if (os_dirname(ff_expand_buffer, MAXPATHL) == FAIL) {
      goto error_return;
    }

    search_ctx->ffsc_start_dir = vim_strsave(ff_expand_buffer);

#ifdef BACKSLASH_IN_FILENAME
    /* A path that starts with "/dir" is relative to the drive, not to the
     * directory (but not for "//machine/dir").  Only use the drive name. */
    if ((*path == '/' || *path == '\\')
        && path[1] != path[0]
        && search_ctx->ffsc_start_dir[1] == ':') {
      search_ctx->ffsc_start_dir[2] = NUL;
    }
#endif
  }

  /*
   * If stopdirs are given, split them into an array of pointers.
   * If this fails (mem allocation), there is no upward search at all or a
   * stop directory is not recognized -> continue silently.
   * If stopdirs just contains a ";" or is empty,
   * search_ctx->ffsc_stopdirs_v will only contain a  NULL pointer. This
   * is handled as unlimited upward search.  See function
   * ff_path_in_stoplist() for details.
   */
  if (stopdirs != NULL) {
    char_u *walker = stopdirs;

    while (*walker == ';') {
      walker++;
    }

    size_t dircount = 1;
    search_ctx->ffsc_stopdirs_v = xmalloc(sizeof(char_u *));

    do {
      char_u *helper;
      void *ptr;

      helper = walker;
      ptr = xrealloc(search_ctx->ffsc_stopdirs_v,
                     (dircount + 1) * sizeof(char_u *));
      search_ctx->ffsc_stopdirs_v = ptr;
      walker = vim_strchr(walker, ';');
      if (walker) {
        assert(walker - helper >= 0);
        search_ctx->ffsc_stopdirs_v[dircount-1] =
          vim_strnsave(helper, (size_t)(walker - helper));
        walker++;
      } else {
        /* this might be "", which means ascent till top
         * of directory tree.
         */
        search_ctx->ffsc_stopdirs_v[dircount-1] =
          vim_strsave(helper);
      }

      dircount++;
    } while (walker != NULL);
    search_ctx->ffsc_stopdirs_v[dircount-1] = NULL;
  }

  search_ctx->ffsc_level = level;

  /* split into:
   *  -fix path
   *  -wildcard_stuff (might be NULL)
   */
  wc_part = vim_strchr(path, '*');
  if (wc_part != NULL) {
    int64_t llevel;
    int len;
    char *errpt;

    // save the fix part of the path
    assert(wc_part - path >= 0);
    search_ctx->ffsc_fix_path = vim_strnsave(path, (size_t)(wc_part - path));

    /*
     * copy wc_path and add restricts to the '**' wildcard.
     * The octet after a '**' is used as a (binary) counter.
     * So '**3' is transposed to '**^C' ('^C' is ASCII value 3)
     * or '**76' is transposed to '**N'( 'N' is ASCII value 76).
     * If no restrict is given after '**' the default is used.
     * Due to this technique the path looks awful if you print it as a
     * string.
     */
    len = 0;
    while (*wc_part != NUL) {
      if (len + 5 >= MAXPATHL) {
        emsg(_(e_pathtoolong));
        break;
      }
      if (STRNCMP(wc_part, "**", 2) == 0) {
        ff_expand_buffer[len++] = *wc_part++;
        ff_expand_buffer[len++] = *wc_part++;

        llevel = strtol((char *)wc_part, &errpt, 10);
        if ((char_u *)errpt != wc_part && llevel > 0 && llevel < 255) {
          ff_expand_buffer[len++] = (char_u)llevel;
        } else if ((char_u *)errpt != wc_part && llevel == 0) {
          // restrict is 0 -> remove already added '**'
          len -= 2;
        } else {
          ff_expand_buffer[len++] = FF_MAX_STAR_STAR_EXPAND;
        }
        wc_part = (char_u *)errpt;
        if (*wc_part != NUL && !vim_ispathsep(*wc_part)) {
          semsg(_(
                 "E343: Invalid path: '**[number]' must be at the end of the path or be followed by '%s'."),
                PATHSEPSTR);
          goto error_return;
        }
      } else {
        ff_expand_buffer[len++] = *wc_part++;
      }
    }
    ff_expand_buffer[len] = NUL;
    search_ctx->ffsc_wc_path = vim_strsave(ff_expand_buffer);
  } else {
    search_ctx->ffsc_fix_path = vim_strsave(path);
  }

  if (search_ctx->ffsc_start_dir == NULL) {
    /* store the fix part as startdir.
     * This is needed if the parameter path is fully qualified.
     */
    search_ctx->ffsc_start_dir = vim_strsave(search_ctx->ffsc_fix_path);
    search_ctx->ffsc_fix_path[0] = NUL;
  }

  // create an absolute path
  if (STRLEN(search_ctx->ffsc_start_dir)
      + STRLEN(search_ctx->ffsc_fix_path) + 3 >= MAXPATHL) {
    emsg(_(e_pathtoolong));
    goto error_return;
  }
  STRCPY(ff_expand_buffer, search_ctx->ffsc_start_dir);
  add_pathsep((char *)ff_expand_buffer);
  {
    size_t eb_len = STRLEN(ff_expand_buffer);
    char_u *buf = xmalloc(eb_len + STRLEN(search_ctx->ffsc_fix_path) + 1);

    STRCPY(buf, ff_expand_buffer);
    STRCPY(buf + eb_len, search_ctx->ffsc_fix_path);
    if (os_isdir(buf)) {
      STRCAT(ff_expand_buffer, search_ctx->ffsc_fix_path);
      add_pathsep((char *)ff_expand_buffer);
    } else {
      char_u *p =  path_tail(search_ctx->ffsc_fix_path);
      char_u *wc_path = NULL;
      char_u *temp = NULL;
      int len = 0;

      if (p > search_ctx->ffsc_fix_path) {
        // do not add '..' to the path and start upwards searching
        len = (int)(p - search_ctx->ffsc_fix_path) - 1;
        if ((len >= 2 && STRNCMP(search_ctx->ffsc_fix_path, "..", 2) == 0)
            && (len == 2 || search_ctx->ffsc_fix_path[2] == PATHSEP)) {
          xfree(buf);
          goto error_return;
        }
        STRLCAT(ff_expand_buffer, search_ctx->ffsc_fix_path, eb_len + (size_t)len + 1);
        add_pathsep((char *)ff_expand_buffer);
      } else {
        len = (int)STRLEN(search_ctx->ffsc_fix_path);
      }

      if (search_ctx->ffsc_wc_path != NULL) {
        wc_path = vim_strsave(search_ctx->ffsc_wc_path);
        temp = xmalloc(STRLEN(search_ctx->ffsc_wc_path)
                       + STRLEN(search_ctx->ffsc_fix_path + len)
                       + 1);
        STRCPY(temp, search_ctx->ffsc_fix_path + len);
        STRCAT(temp, search_ctx->ffsc_wc_path);
        xfree(search_ctx->ffsc_wc_path);
        xfree(wc_path);
        search_ctx->ffsc_wc_path = temp;
      }
    }
    xfree(buf);
  }

  sptr = ff_create_stack_element(ff_expand_buffer,
                                 search_ctx->ffsc_wc_path,
                                 level, 0);

  ff_push(search_ctx, sptr);
  search_ctx->ffsc_file_to_search = vim_strsave(filename);
  return search_ctx;

error_return:
  /*
   * We clear the search context now!
   * Even when the caller gave us a (perhaps valid) context we free it here,
   * as we might have already destroyed it.
   */
  vim_findfile_cleanup(search_ctx);
  return NULL;
}

/// @return  the stopdir string.  Check that ';' is not escaped.
char_u *vim_findfile_stopdir(char_u *buf)
{
  char_u *r_ptr = buf;

  while (*r_ptr != NUL && *r_ptr != ';') {
    if (r_ptr[0] == '\\' && r_ptr[1] == ';') {
      /* Overwrite the escape char,
       * use STRLEN(r_ptr) to move the trailing '\0'. */
      STRMOVE(r_ptr, r_ptr + 1);
      r_ptr++;
    }
    r_ptr++;
  }
  if (*r_ptr == ';') {
    *r_ptr = 0;
    r_ptr++;
  } else if (*r_ptr == NUL) {
    r_ptr = NULL;
  }
  return r_ptr;
}

/// Clean up the given search context. Can handle a NULL pointer.
void vim_findfile_cleanup(void *ctx)
{
  if (ctx == NULL) {
    return;
  }

  vim_findfile_free_visited(ctx);
  ff_clear(ctx);
  xfree(ctx);
}

/// Find a file in a search context.
/// The search context was created with vim_findfile_init() above.
///
/// To get all matching files call this function until you get NULL.
///
/// If the passed search_context is NULL, NULL is returned.
///
/// The search algorithm is depth first. To change this replace the
/// stack with a list (don't forget to leave partly searched directories on the
/// top of the list).
///
/// @return  a pointer to an allocated file name or,
///          NULL if nothing found.
char_u *vim_findfile(void *search_ctx_arg)
{
  char_u *file_path;
  char_u *rest_of_wildcards;
  char_u *path_end = NULL;
  ff_stack_T *stackp = NULL;
  size_t len;
  char_u *p;
  char_u *suf;
  ff_search_ctx_T *search_ctx;

  if (search_ctx_arg == NULL) {
    return NULL;
  }

  search_ctx = (ff_search_ctx_T *)search_ctx_arg;

  /*
   * filepath is used as buffer for various actions and as the storage to
   * return a found filename.
   */
  file_path = xmalloc(MAXPATHL);

  // store the end of the start dir -- needed for upward search
  if (search_ctx->ffsc_start_dir != NULL) {
    path_end = &search_ctx->ffsc_start_dir[
                                           STRLEN(search_ctx->ffsc_start_dir)];
  }

  // upward search loop
  for (;;) {
    // downward search loop
    for (;;) {
      // check if user wants to stop the search
      os_breakcheck();
      if (got_int) {
        break;
      }

      // get directory to work on from stack
      stackp = ff_pop(search_ctx);
      if (stackp == NULL) {
        break;
      }

      /*
       * TODO: decide if we leave this test in
       *
       * GOOD: don't search a directory(-tree) twice.
       * BAD:  - check linked list for every new directory entered.
       *       - check for double files also done below
       *
       * Here we check if we already searched this directory.
       * We already searched a directory if:
       * 1) The directory is the same.
       * 2) We would use the same wildcard string.
       *
       * Good if you have links on same directory via several ways
       *  or you have selfreferences in directories (e.g. SuSE Linux 6.3:
       *  /etc/rc.d/init.d is linked to /etc/rc.d -> endless loop)
       *
       * This check is only needed for directories we work on for the
       * first time (hence stackp->ff_filearray == NULL)
       */
      if (stackp->ffs_filearray == NULL
          && ff_check_visited(&search_ctx->ffsc_dir_visited_list
                              ->ffvl_visited_list,
                              stackp->ffs_fix_path,
                              stackp->ffs_wc_path
                              ) == FAIL) {
#ifdef FF_VERBOSE
        if (p_verbose >= 5) {
          verbose_enter_scroll();
          smsg("Already Searched: %s (%s)",
               stackp->ffs_fix_path, stackp->ffs_wc_path);
          msg_puts("\n");  // don't overwrite this either
          verbose_leave_scroll();
        }
#endif
        ff_free_stack_element(stackp);
        continue;
      }
#ifdef FF_VERBOSE
      else if (p_verbose >= 5) {
        verbose_enter_scroll();
        smsg("Searching: %s (%s)",
             stackp->ffs_fix_path, stackp->ffs_wc_path);
        msg_puts("\n");  // don't overwrite this either
        verbose_leave_scroll();
      }
#endif

      // check depth
      if (stackp->ffs_level <= 0) {
        ff_free_stack_element(stackp);
        continue;
      }

      file_path[0] = NUL;

      /*
       * If no filearray till now expand wildcards
       * The function expand_wildcards() can handle an array of paths
       * and all possible expands are returned in one array. We use this
       * to handle the expansion of '**' into an empty string.
       */
      if (stackp->ffs_filearray == NULL) {
        char_u *dirptrs[2];

        /* we use filepath to build the path expand_wildcards() should
         * expand.
         */
        dirptrs[0] = file_path;
        dirptrs[1] = NULL;

        // if we have a start dir copy it in
        if (!vim_isAbsName(stackp->ffs_fix_path)
            && search_ctx->ffsc_start_dir) {
          if (STRLEN(search_ctx->ffsc_start_dir) + 1 >= MAXPATHL) {
            ff_free_stack_element(stackp);
            goto fail;
          }
          STRCPY(file_path, search_ctx->ffsc_start_dir);
          if (!add_pathsep((char *)file_path)) {
            ff_free_stack_element(stackp);
            goto fail;
          }
        }

        // append the fix part of the search path
        if (STRLEN(file_path) + STRLEN(stackp->ffs_fix_path) + 1 >= MAXPATHL) {
          ff_free_stack_element(stackp);
          goto fail;
        }
        STRCAT(file_path, stackp->ffs_fix_path);
        if (!add_pathsep((char *)file_path)) {
          ff_free_stack_element(stackp);
          goto fail;
        }

        rest_of_wildcards = stackp->ffs_wc_path;
        if (*rest_of_wildcards != NUL) {
          len = STRLEN(file_path);
          if (STRNCMP(rest_of_wildcards, "**", 2) == 0) {
            // pointer to the restrict byte
            // The restrict byte is not a character!
            p = rest_of_wildcards + 2;

            if (*p > 0) {
              (*p)--;
              if (len + 1 >= MAXPATHL) {
                ff_free_stack_element(stackp);
                goto fail;
              }
              file_path[len++] = '*';
            }

            if (*p == 0) {
              // remove '**<numb> from wildcards
              STRMOVE(rest_of_wildcards, rest_of_wildcards + 3);
            } else {
              rest_of_wildcards += 3;
            }

            if (stackp->ffs_star_star_empty == 0) {
              // if not done before, expand '**' to empty
              stackp->ffs_star_star_empty = 1;
              dirptrs[1] = stackp->ffs_fix_path;
            }
          }

          /*
           * Here we copy until the next path separator or the end of
           * the path. If we stop at a path separator, there is
           * still something else left. This is handled below by
           * pushing every directory returned from expand_wildcards()
           * on the stack again for further search.
           */
          while (*rest_of_wildcards
                 && !vim_ispathsep(*rest_of_wildcards)) {
            if (len + 1 >= MAXPATHL) {
              ff_free_stack_element(stackp);
              goto fail;
            }
            file_path[len++] = *rest_of_wildcards++;
          }

          file_path[len] = NUL;
          if (vim_ispathsep(*rest_of_wildcards)) {
            rest_of_wildcards++;
          }
        }

        /*
         * Expand wildcards like "*" and "$VAR".
         * If the path is a URL don't try this.
         */
        if (path_with_url((char *)dirptrs[0])) {
          stackp->ffs_filearray = xmalloc(sizeof(char *));
          stackp->ffs_filearray[0] = vim_strsave(dirptrs[0]);
          stackp->ffs_filearray_size = 1;
        } else {
          /* Add EW_NOTWILD because the expanded path may contain
           * wildcard characters that are to be taken literally.
           * This is a bit of a hack. */
          expand_wildcards((dirptrs[1] == NULL) ? 1 : 2, dirptrs,
                           &stackp->ffs_filearray_size,
                           &stackp->ffs_filearray,
                           EW_DIR|EW_ADDSLASH|EW_SILENT|EW_NOTWILD);
        }

        stackp->ffs_filearray_cur = 0;
        stackp->ffs_stage = 0;
      } else {
        rest_of_wildcards = &stackp->ffs_wc_path[
                                                 STRLEN(stackp->ffs_wc_path)];
      }

      if (stackp->ffs_stage == 0) {
        // this is the first time we work on this directory
        if (*rest_of_wildcards == NUL) {
          /*
           * We don't have further wildcards to expand, so we have to
           * check for the final file now.
           */
          for (int i = stackp->ffs_filearray_cur;
               i < stackp->ffs_filearray_size; ++i) {
            if (!path_with_url((char *)stackp->ffs_filearray[i])
                && !os_isdir(stackp->ffs_filearray[i])) {
              continue;                 // not a directory
            }
            // prepare the filename to be checked for existence below
            if (STRLEN(stackp->ffs_filearray[i]) + 1
                + STRLEN(search_ctx->ffsc_file_to_search) >= MAXPATHL) {
              ff_free_stack_element(stackp);
              goto fail;
            }
            STRCPY(file_path, stackp->ffs_filearray[i]);
            if (!add_pathsep((char *)file_path)) {
              ff_free_stack_element(stackp);
              goto fail;
            }
            STRCAT(file_path, search_ctx->ffsc_file_to_search);

            /*
             * Try without extra suffix and then with suffixes
             * from 'suffixesadd'.
             */
            len = STRLEN(file_path);
            if (search_ctx->ffsc_tagfile) {
              suf = (char_u *)"";
            } else {
              suf = curbuf->b_p_sua;
            }
            for (;;) {
              // if file exists and we didn't already find it
              if ((path_with_url((char *)file_path)
                   || (os_path_exists(file_path)
                       && (search_ctx->ffsc_find_what
                           == FINDFILE_BOTH
                           || ((search_ctx->ffsc_find_what
                                == FINDFILE_DIR)
                               == os_isdir(file_path)))))
#ifndef FF_VERBOSE
                  && (ff_check_visited(&search_ctx->ffsc_visited_list->ffvl_visited_list,
                                       file_path,
                                       (char_u *)""
                                       ) == OK)
#endif
                  ) {
#ifdef FF_VERBOSE
                if (ff_check_visited(&search_ctx->ffsc_visited_list->ffvl_visited_list,
                                     file_path,
                                     (char_u *)""
                                     ) == FAIL) {
                  if (p_verbose >= 5) {
                    verbose_enter_scroll();
                    smsg("Already: %s", file_path);
                    msg_puts("\n");  // don't overwrite this either
                    verbose_leave_scroll();
                  }
                  continue;
                }
#endif

                // push dir to examine rest of subdirs later
                assert(i < UCHAR_MAX - 1);
                stackp->ffs_filearray_cur = (char_u)(i + 1);
                ff_push(search_ctx, stackp);

                if (!path_with_url((char *)file_path)) {
                  simplify_filename(file_path);
                }
                if (os_dirname(ff_expand_buffer, MAXPATHL)
                    == OK) {
                  p = path_shorten_fname(file_path,
                                         ff_expand_buffer);
                  if (p != NULL) {
                    STRMOVE(file_path, p);
                  }
                }
#ifdef FF_VERBOSE
                if (p_verbose >= 5) {
                  verbose_enter_scroll();
                  smsg("HIT: %s", file_path);
                  msg_puts("\n");  // don't overwrite this either
                  verbose_leave_scroll();
                }
#endif
                return file_path;
              }

              // Not found or found already, try next suffix.
              if (*suf == NUL) {
                break;
              }
              assert(MAXPATHL >= len);
              copy_option_part(&suf, file_path + len,
                               MAXPATHL - len, ",");
            }
          }
        } else {
          /*
           * still wildcards left, push the directories for further
           * search
           */
          for (int i = stackp->ffs_filearray_cur;
               i < stackp->ffs_filearray_size; ++i) {
            if (!os_isdir(stackp->ffs_filearray[i])) {
              continue;                 // not a directory
            }
            ff_push(search_ctx,
                    ff_create_stack_element(stackp->ffs_filearray[i],
                                            rest_of_wildcards,
                                            stackp->ffs_level - 1, 0));
          }
        }
        stackp->ffs_filearray_cur = 0;
        stackp->ffs_stage = 1;
      }

      /*
       * if wildcards contains '**' we have to descent till we reach the
       * leaves of the directory tree.
       */
      if (STRNCMP(stackp->ffs_wc_path, "**", 2) == 0) {
        for (int i = stackp->ffs_filearray_cur;
             i < stackp->ffs_filearray_size; ++i) {
          if (fnamecmp(stackp->ffs_filearray[i],
                       stackp->ffs_fix_path) == 0) {
            continue;             // don't repush same directory
          }
          if (!os_isdir(stackp->ffs_filearray[i])) {
            continue;               // not a directory
          }
          ff_push(search_ctx,
                  ff_create_stack_element(stackp->ffs_filearray[i],
                                          stackp->ffs_wc_path, stackp->ffs_level - 1, 1));
        }
      }

      // we are done with the current directory
      ff_free_stack_element(stackp);
    }

    /* If we reached this, we didn't find anything downwards.
     * Let's check if we should do an upward search.
     */
    if (search_ctx->ffsc_start_dir
        && search_ctx->ffsc_stopdirs_v != NULL && !got_int) {
      ff_stack_T *sptr;

      // is the last starting directory in the stop list?
      if (ff_path_in_stoplist(search_ctx->ffsc_start_dir,
                              (int)(path_end - search_ctx->ffsc_start_dir),
                              search_ctx->ffsc_stopdirs_v) == TRUE) {
        break;
      }

      // cut of last dir
      while (path_end > search_ctx->ffsc_start_dir
             && vim_ispathsep(*path_end)) {
        path_end--;
      }
      while (path_end > search_ctx->ffsc_start_dir
             && !vim_ispathsep(path_end[-1])) {
        path_end--;
      }
      *path_end = 0;
      path_end--;

      if (*search_ctx->ffsc_start_dir == 0) {
        break;
      }

      if (STRLEN(search_ctx->ffsc_start_dir) + 1
          + STRLEN(search_ctx->ffsc_fix_path) >= MAXPATHL) {
        goto fail;
      }
      STRCPY(file_path, search_ctx->ffsc_start_dir);
      if (!add_pathsep((char *)file_path)) {
        goto fail;
      }
      STRCAT(file_path, search_ctx->ffsc_fix_path);

      // create a new stack entry
      sptr = ff_create_stack_element(file_path,
                                     search_ctx->ffsc_wc_path, search_ctx->ffsc_level, 0);
      ff_push(search_ctx, sptr);
    } else {
      break;
    }
  }

fail:
  xfree(file_path);
  return NULL;
}

/// Free the list of lists of visited files and directories
/// Can handle it if the passed search_context is NULL;
void vim_findfile_free_visited(void *search_ctx_arg)
{
  ff_search_ctx_T *search_ctx;

  if (search_ctx_arg == NULL) {
    return;
  }

  search_ctx = (ff_search_ctx_T *)search_ctx_arg;
  vim_findfile_free_visited_list(&search_ctx->ffsc_visited_lists_list);
  vim_findfile_free_visited_list(&search_ctx->ffsc_dir_visited_lists_list);
}

static void vim_findfile_free_visited_list(ff_visited_list_hdr_T **list_headp)
{
  ff_visited_list_hdr_T *vp;

  while (*list_headp != NULL) {
    vp = (*list_headp)->ffvl_next;
    ff_free_visited_list((*list_headp)->ffvl_visited_list);

    xfree((*list_headp)->ffvl_filename);
    xfree(*list_headp);
    *list_headp = vp;
  }
  *list_headp = NULL;
}

static void ff_free_visited_list(ff_visited_T *vl)
{
  ff_visited_T *vp;

  while (vl != NULL) {
    vp = vl->ffv_next;
    xfree(vl->ffv_wc_path);
    xfree(vl);
    vl = vp;
  }
  vl = NULL;
}

/// @return  the already visited list for the given filename. If none is found it
///          allocates a new one.
static ff_visited_list_hdr_T *ff_get_visited_list(char_u *filename,
                                                  ff_visited_list_hdr_T **list_headp)
{
  ff_visited_list_hdr_T *retptr = NULL;

  // check if a visited list for the given filename exists
  if (*list_headp != NULL) {
    retptr = *list_headp;
    while (retptr != NULL) {
      if (fnamecmp(filename, retptr->ffvl_filename) == 0) {
#ifdef FF_VERBOSE
        if (p_verbose >= 5) {
          verbose_enter_scroll();
          smsg("ff_get_visited_list: FOUND list for %s", filename);
          msg_puts("\n");  // don't overwrite this either
          verbose_leave_scroll();
        }
#endif
        return retptr;
      }
      retptr = retptr->ffvl_next;
    }
  }

#ifdef FF_VERBOSE
  if (p_verbose >= 5) {
    verbose_enter_scroll();
    smsg("ff_get_visited_list: new list for %s", filename);
    msg_puts("\n");  // don't overwrite this either
    verbose_leave_scroll();
  }
#endif

  /*
   * if we reach this we didn't find a list and we have to allocate new list
   */
  retptr = xmalloc(sizeof(*retptr));

  retptr->ffvl_visited_list = NULL;
  retptr->ffvl_filename = vim_strsave(filename);
  retptr->ffvl_next = *list_headp;
  *list_headp = retptr;

  return retptr;
}

/// Check if two wildcard paths are equal.
/// They are equal if:
///  - both paths are NULL
///  - they have the same length
///  - char by char comparison is OK
///  - the only differences are in the counters behind a '**', so
///    '**\20' is equal to '**\24'
static bool ff_wc_equal(char_u *s1, char_u *s2)
{
  int i, j;
  int c1 = NUL;
  int c2 = NUL;
  int prev1 = NUL;
  int prev2 = NUL;

  if (s1 == s2) {
    return true;
  }

  if (s1 == NULL || s2 == NULL) {
    return false;
  }

  for (i = 0, j = 0; s1[i] != NUL && s2[j] != NUL;) {
    c1 = utf_ptr2char(s1 + i);
    c2 = utf_ptr2char(s2 + j);

    if ((p_fic ? mb_tolower(c1) != mb_tolower(c2) : c1 != c2)
        && (prev1 != '*' || prev2 != '*')) {
      return false;
    }
    prev2 = prev1;
    prev1 = c1;

    i += utfc_ptr2len(s1 + i);
    j += utfc_ptr2len(s2 + j);
  }
  return s1[i] == s2[j];
}

/// maintains the list of already visited files and dirs
///
/// @return  FAIL if the given file/dir is already in the list or,
///          OK if it is newly added
static int ff_check_visited(ff_visited_T **visited_list, char_u *fname, char_u *wc_path)
{
  ff_visited_T *vp;
  bool url = false;

  FileID file_id;
  // For a URL we only compare the name, otherwise we compare the
  // device/inode.
  if (path_with_url((char *)fname)) {
    STRLCPY(ff_expand_buffer, fname, MAXPATHL);
    url = true;
  } else {
    ff_expand_buffer[0] = NUL;
    if (!os_fileid((char *)fname, &file_id)) {
      return FAIL;
    }
  }

  // check against list of already visited files
  for (vp = *visited_list; vp != NULL; vp = vp->ffv_next) {
    if ((url && fnamecmp(vp->ffv_fname, ff_expand_buffer) == 0)
        || (!url && vp->file_id_valid
            && os_fileid_equal(&(vp->file_id), &file_id))) {
      // are the wildcard parts equal
      if (ff_wc_equal(vp->ffv_wc_path, wc_path)) {
        // already visited
        return FAIL;
      }
    }
  }

  /*
   * New file/dir.  Add it to the list of visited files/dirs.
   */
  vp = xmalloc(sizeof(ff_visited_T) + STRLEN(ff_expand_buffer));

  if (!url) {
    vp->file_id_valid = true;
    vp->file_id = file_id;
    vp->ffv_fname[0] = NUL;
  } else {
    vp->file_id_valid = false;
    STRCPY(vp->ffv_fname, ff_expand_buffer);
  }

  if (wc_path != NULL) {
    vp->ffv_wc_path = vim_strsave(wc_path);
  } else {
    vp->ffv_wc_path = NULL;
  }

  vp->ffv_next = *visited_list;
  *visited_list = vp;

  return OK;
}

/// create stack element from given path pieces
static ff_stack_T *ff_create_stack_element(char_u *fix_part, char_u *wc_part, int level,
                                           int star_star_empty)
{
  ff_stack_T *new = xmalloc(sizeof(ff_stack_T));

  new->ffs_prev          = NULL;
  new->ffs_filearray     = NULL;
  new->ffs_filearray_size = 0;
  new->ffs_filearray_cur  = 0;
  new->ffs_stage         = 0;
  new->ffs_level         = level;
  new->ffs_star_star_empty = star_star_empty;

  // the following saves NULL pointer checks in vim_findfile
  if (fix_part == NULL) {
    fix_part = (char_u *)"";
  }
  new->ffs_fix_path = vim_strsave(fix_part);

  if (wc_part == NULL) {
    wc_part  = (char_u *)"";
  }
  new->ffs_wc_path = vim_strsave(wc_part);

  return new;
}

/// Push a dir on the directory stack.
static void ff_push(ff_search_ctx_T *search_ctx, ff_stack_T *stack_ptr)
{
  /* check for NULL pointer, not to return an error to the user, but
   * to prevent a crash */
  if (stack_ptr != NULL) {
    stack_ptr->ffs_prev = search_ctx->ffsc_stack_ptr;
    search_ctx->ffsc_stack_ptr = stack_ptr;
  }
}

/// Pop a dir from the directory stack.
///
/// @return  NULL if stack is empty.
static ff_stack_T *ff_pop(ff_search_ctx_T *search_ctx)
{
  ff_stack_T *sptr;

  sptr = search_ctx->ffsc_stack_ptr;
  if (search_ctx->ffsc_stack_ptr != NULL) {
    search_ctx->ffsc_stack_ptr = search_ctx->ffsc_stack_ptr->ffs_prev;
  }

  return sptr;
}

/// free the given stack element
static void ff_free_stack_element(ff_stack_T *const stack_ptr)
{
  if (stack_ptr == NULL) {
    return;
  }

  // free handles possible NULL pointers
  xfree(stack_ptr->ffs_fix_path);
  xfree(stack_ptr->ffs_wc_path);

  if (stack_ptr->ffs_filearray != NULL) {
    FreeWild(stack_ptr->ffs_filearray_size, stack_ptr->ffs_filearray);
  }

  xfree(stack_ptr);
}

/// Clear the search context, but NOT the visited list.
static void ff_clear(ff_search_ctx_T *search_ctx)
{
  ff_stack_T *sptr;

  // clear up stack
  while ((sptr = ff_pop(search_ctx)) != NULL) {
    ff_free_stack_element(sptr);
  }

  xfree(search_ctx->ffsc_file_to_search);
  xfree(search_ctx->ffsc_start_dir);
  xfree(search_ctx->ffsc_fix_path);
  xfree(search_ctx->ffsc_wc_path);

  if (search_ctx->ffsc_stopdirs_v != NULL) {
    int i = 0;

    while (search_ctx->ffsc_stopdirs_v[i] != NULL) {
      xfree(search_ctx->ffsc_stopdirs_v[i]);
      i++;
    }
    xfree(search_ctx->ffsc_stopdirs_v);
  }
  search_ctx->ffsc_stopdirs_v = NULL;

  // reset everything
  search_ctx->ffsc_file_to_search = NULL;
  search_ctx->ffsc_start_dir = NULL;
  search_ctx->ffsc_fix_path = NULL;
  search_ctx->ffsc_wc_path = NULL;
  search_ctx->ffsc_level = 0;
}

/// check if the given path is in the stopdirs
///
/// @return  TRUE if yes else FALSE
static int ff_path_in_stoplist(char_u *path, int path_len, char_u **stopdirs_v)
{
  int i = 0;

  // eat up trailing path separators, except the first
  while (path_len > 1 && vim_ispathsep(path[path_len - 1])) {
    path_len--;
  }

  // if no path consider it as match
  if (path_len == 0) {
    return TRUE;
  }

  for (i = 0; stopdirs_v[i] != NULL; i++) {
    if ((int)STRLEN(stopdirs_v[i]) > path_len) {
      /* match for parent directory. So '/home' also matches
       * '/home/rks'. Check for PATHSEP in stopdirs_v[i], else
       * '/home/r' would also match '/home/rks'
       */
      if (fnamencmp(stopdirs_v[i], path, path_len) == 0
          && vim_ispathsep(stopdirs_v[i][path_len])) {
        return TRUE;
      }
    } else {
      if (fnamecmp(stopdirs_v[i], path) == 0) {
        return TRUE;
      }
    }
  }
  return FALSE;
}

/// Find the file name "ptr[len]" in the path.  Also finds directory names.
///
/// On the first call set the parameter 'first' to TRUE to initialize
/// the search.  For repeating calls to FALSE.
///
/// Repeating calls will return other files called 'ptr[len]' from the path.
///
/// Only on the first call 'ptr' and 'len' are used.  For repeating calls they
/// don't need valid values.
///
/// If nothing found on the first call the option FNAME_MESS will issue the
/// message:
///          'Can't find file "<file>" in path'
/// On repeating calls:
///          'No more file "<file>" found in path'
///
/// options:
/// FNAME_MESS       give error message when not found
///
/// Uses NameBuff[]!
///
/// @param ptr  file name
/// @param len  length of file name
/// @param first  use count'th matching file name
/// @param rel_fname  file name searching relative to
///
/// @return  an allocated string for the file name.  NULL for error.
char_u *find_file_in_path(char_u *ptr, size_t len, int options, int first, char_u *rel_fname)
{
  return find_file_in_path_option(ptr, len, options, first,
                                  (*curbuf->b_p_path == NUL
                                   ? p_path
                                   : curbuf->b_p_path),
                                  FINDFILE_BOTH, rel_fname, curbuf->b_p_sua);
}

static char_u *ff_file_to_find = NULL;
static void *fdip_search_ctx = NULL;

#if defined(EXITFREE)
void free_findfile(void)
{
  xfree(ff_file_to_find);
  vim_findfile_cleanup(fdip_search_ctx);
  xfree(ff_expand_buffer);
}

#endif

/// Find the directory name "ptr[len]" in the path.
///
/// options:
/// FNAME_MESS       give error message when not found
/// FNAME_UNESC      unescape backslashes
///
/// Uses NameBuff[]!
///
/// @param ptr  file name
/// @param len  length of file name
/// @param rel_fname  file name searching relative to
///
/// @return  an allocated string for the file name.  NULL for error.
char_u *find_directory_in_path(char_u *ptr, size_t len, int options, char_u *rel_fname)
{
  return find_file_in_path_option(ptr, len, options, TRUE, p_cdpath,
                                  FINDFILE_DIR, rel_fname, (char_u *)"");
}

/// @param ptr  file name
/// @param len  length of file name
/// @param first  use count'th matching file name
/// @param path_option  p_path or p_cdpath
/// @param find_what  FINDFILE_FILE, _DIR or _BOTH
/// @param rel_fname  file name we are looking relative to.
/// @param suffixes  list of suffixes, 'suffixesadd' option
char_u *find_file_in_path_option(char_u *ptr, size_t len, int options, int first,
                                 char_u *path_option, int find_what, char_u *rel_fname,
                                 char_u *suffixes)
{
  static char_u *dir;
  static int did_findfile_init = FALSE;
  char_u save_char;
  char_u *file_name = NULL;
  char_u *buf = NULL;
  int rel_to_curdir;

  if (rel_fname != NULL && path_with_url((const char *)rel_fname)) {
    // Do not attempt to search "relative" to a URL. #6009
    rel_fname = NULL;
  }

  if (first == true) {
    if (len == 0) {
      return NULL;
    }

    // copy file name into NameBuff, expanding environment variables
    save_char = ptr[len];
    ptr[len] = NUL;
    expand_env_esc(ptr, NameBuff, MAXPATHL, false, true, NULL);
    ptr[len] = save_char;

    xfree(ff_file_to_find);
    ff_file_to_find = vim_strsave(NameBuff);
    if (options & FNAME_UNESC) {
      // Change all "\ " to " ".
      for (ptr = ff_file_to_find; *ptr != NUL; ++ptr) {
        if (ptr[0] == '\\' && ptr[1] == ' ') {
          memmove(ptr, ptr + 1, STRLEN(ptr));
        }
      }
    }
  }

  rel_to_curdir = (ff_file_to_find[0] == '.'
                   && (ff_file_to_find[1] == NUL
                       || vim_ispathsep(ff_file_to_find[1])
                       || (ff_file_to_find[1] == '.'
                           && (ff_file_to_find[2] == NUL
                               || vim_ispathsep(ff_file_to_find[2])))));
  if (vim_isAbsName(ff_file_to_find)
      // "..", "../path", "." and "./path": don't use the path_option
      || rel_to_curdir
#if defined(WIN32)
      // handle "\tmp" as absolute path
      || vim_ispathsep(ff_file_to_find[0])
      // handle "c:name" as absolute path
      || (ff_file_to_find[0] != NUL && ff_file_to_find[1] == ':')
#endif
      ) {
    /*
     * Absolute path, no need to use "path_option".
     * If this is not a first call, return NULL.  We already returned a
     * filename on the first call.
     */
    if (first == TRUE) {
      if (path_with_url((char *)ff_file_to_find)) {
        file_name = vim_strsave(ff_file_to_find);
        goto theend;
      }

      /* When FNAME_REL flag given first use the directory of the file.
       * Otherwise or when this fails use the current directory. */
      for (int run = 1; run <= 2; ++run) {
        size_t l = STRLEN(ff_file_to_find);
        if (run == 1
            && rel_to_curdir
            && (options & FNAME_REL)
            && rel_fname != NULL
            && STRLEN(rel_fname) + l < MAXPATHL) {
          STRCPY(NameBuff, rel_fname);
          STRCPY(path_tail(NameBuff), ff_file_to_find);
          l = STRLEN(NameBuff);
        } else {
          STRCPY(NameBuff, ff_file_to_find);
          run = 2;
        }

        /* When the file doesn't exist, try adding parts of
         * 'suffixesadd'. */
        buf = suffixes;
        for (;;) {
          if (
              (os_path_exists(NameBuff)
               && (find_what == FINDFILE_BOTH
                   || ((find_what == FINDFILE_DIR)
                       == os_isdir(NameBuff))))) {
            file_name = vim_strsave(NameBuff);
            goto theend;
          }
          if (*buf == NUL) {
            break;
          }
          assert(MAXPATHL >= l);
          copy_option_part(&buf, NameBuff + l, MAXPATHL - l, ",");
        }
      }
    }
  } else {
    /*
     * Loop over all paths in the 'path' or 'cdpath' option.
     * When "first" is set, first setup to the start of the option.
     * Otherwise continue to find the next match.
     */
    if (first == TRUE) {
      // vim_findfile_free_visited can handle a possible NULL pointer
      vim_findfile_free_visited(fdip_search_ctx);
      dir = path_option;
      did_findfile_init = FALSE;
    }

    for (;;) {
      if (did_findfile_init) {
        file_name = vim_findfile(fdip_search_ctx);
        if (file_name != NULL) {
          break;
        }

        did_findfile_init = FALSE;
      } else {
        char_u *r_ptr;

        if (dir == NULL || *dir == NUL) {
          /* We searched all paths of the option, now we can
           * free the search context. */
          vim_findfile_cleanup(fdip_search_ctx);
          fdip_search_ctx = NULL;
          break;
        }

        buf = xmalloc(MAXPATHL);

        // copy next path
        buf[0] = 0;
        copy_option_part(&dir, buf, MAXPATHL, " ,");

        // get the stopdir string
        r_ptr = vim_findfile_stopdir(buf);
        fdip_search_ctx = vim_findfile_init(buf, ff_file_to_find,
                                            r_ptr, 100, FALSE, find_what,
                                            fdip_search_ctx, FALSE, rel_fname);
        if (fdip_search_ctx != NULL) {
          did_findfile_init = TRUE;
        }
        xfree(buf);
      }
    }
  }
  if (file_name == NULL && (options & FNAME_MESS)) {
    if (first == TRUE) {
      if (find_what == FINDFILE_DIR) {
        semsg(_("E344: Can't find directory \"%s\" in cdpath"),
              ff_file_to_find);
      } else {
        semsg(_("E345: Can't find file \"%s\" in path"),
              ff_file_to_find);
      }
    } else {
      if (find_what == FINDFILE_DIR) {
        semsg(_("E346: No more directory \"%s\" found in cdpath"),
              ff_file_to_find);
      } else {
        semsg(_("E347: No more file \"%s\" found in path"),
              ff_file_to_find);
      }
    }
  }

theend:
  return file_name;
}

void do_autocmd_dirchanged(char *new_dir, CdScope scope, CdCause cause, bool pre)
{
  static bool recursive = false;

  event_T event = pre ? EVENT_DIRCHANGEDPRE : EVENT_DIRCHANGED;

  if (recursive || !has_event(event)) {
    // No autocommand was defined or we changed
    // the directory from this autocommand.
    return;
  }

  recursive = true;

  save_v_event_T save_v_event;
  dict_T *dict = get_v_event(&save_v_event);
  char buf[8];

  switch (scope) {
  case kCdScopeGlobal:
    snprintf(buf, sizeof(buf), "global");
    break;
  case kCdScopeTabpage:
    snprintf(buf, sizeof(buf), "tabpage");
    break;
  case kCdScopeWindow:
    snprintf(buf, sizeof(buf), "window");
    break;
  case kCdScopeInvalid:
    // Should never happen.
    abort();
  }

#ifdef BACKSLASH_IN_FILENAME
  char new_dir_buf[MAXPATHL];
  STRCPY(new_dir_buf, new_dir);
  slash_adjust(new_dir_buf);
  new_dir = new_dir_buf;
#endif

  if (pre) {
    tv_dict_add_str(dict, S_LEN("directory"), new_dir);
  } else {
    tv_dict_add_str(dict, S_LEN("cwd"), new_dir);
  }
  tv_dict_add_str(dict, S_LEN("scope"), buf);  // -V614
  tv_dict_add_bool(dict, S_LEN("changed_window"), cause == kCdCauseWindow);
  tv_dict_set_keys_readonly(dict);

  switch (cause) {
  case kCdCauseManual:
  case kCdCauseWindow:
    break;
  case kCdCauseAuto:
    snprintf(buf, sizeof(buf), "auto");
    break;
  case kCdCauseOther:
    // Should never happen.
    abort();
  }

  apply_autocmds(event, (char_u *)buf, (char_u *)new_dir, false, curbuf);

  restore_v_event(dict, &save_v_event);

  recursive = false;
}

/// Change to a file's directory.
/// Caller must call shorten_fnames()!
///
/// @return  OK or FAIL
int vim_chdirfile(char_u *fname, CdCause cause)
{
  char dir[MAXPATHL];

  STRLCPY(dir, fname, MAXPATHL);
  *path_tail_with_sep((char_u *)dir) = NUL;

  if (os_dirname(NameBuff, sizeof(NameBuff)) != OK) {
    NameBuff[0] = NUL;
  }

  if (pathcmp(dir, (char *)NameBuff, -1) == 0) {
    // nothing to do
    return OK;
  }

  if (cause != kCdCauseOther) {
    do_autocmd_dirchanged(dir, kCdScopeWindow, cause, true);
  }

  if (os_chdir(dir) != 0) {
    return FAIL;
  }

  if (cause != kCdCauseOther) {
    do_autocmd_dirchanged(dir, kCdScopeWindow, cause, false);
  }

  return OK;
}

/// Change directory to "new_dir". Search 'cdpath' for relative directory names.
int vim_chdir(char_u *new_dir)
{
  char *dir_name = (char *)find_directory_in_path(new_dir, STRLEN(new_dir),
                                                  FNAME_MESS, curbuf->b_ffname);
  if (dir_name == NULL) {
    return -1;
  }

  int r = os_chdir(dir_name);
  xfree(dir_name);
  return r;
}

